Skip to content

Commit 7dfba23

Browse files
authored
Merge pull request #2 from StudioLambda/chore/consumer-skills
Add consumer-facing agent skills for all modules
2 parents 29aae10 + abba0ab commit 7dfba23

6 files changed

Lines changed: 1498 additions & 0 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
---
2+
name: cosmos-contract
3+
description: >
4+
Interfaces and helpers for the Cosmos HTTP framework contract module
5+
(github.com/studiolambda/cosmos/contract). Use when consuming or
6+
implementing Cosmos contracts: Cache, Database, Encrypter, Hasher,
7+
Session, SessionDriver, Events, Hooks. Also covers the request and
8+
response helper packages for reading HTTP inputs and writing outputs.
9+
---
10+
11+
# Cosmos Contract
12+
13+
The contract module defines all service interfaces and HTTP helper functions
14+
for the Cosmos framework. It is the foundation layer — every other Cosmos
15+
module depends on it.
16+
17+
```
18+
go get github.com/studiolambda/cosmos/contract
19+
```
20+
21+
## Architecture
22+
23+
```
24+
contract/
25+
├── cache.go # Cache interface (10 methods)
26+
├── crypto.go # Encrypter interface
27+
├── database.go # Database interface (transactions, named queries)
28+
├── event.go # Events interface + type aliases
29+
├── hash.go # Hasher interface
30+
├── hooks.go # Hooks interface (lifecycle hooks)
31+
├── session.go # Session + SessionDriver interfaces
32+
├── request/ # HTTP request helpers (params, query, body, headers, cookies, sessions)
33+
├── response/ # HTTP response helpers (JSON, HTML, XML, SSE, redirects)
34+
└── mock/ # Mockery-generated mocks for all interfaces
35+
```
36+
37+
## Interfaces
38+
39+
### Cache
40+
41+
```go
42+
type Cache interface {
43+
Get(ctx context.Context, key string) (any, error)
44+
Put(ctx context.Context, key string, value any, ttl time.Duration) error
45+
Delete(ctx context.Context, key string) error
46+
Has(ctx context.Context, key string) (bool, error)
47+
Pull(ctx context.Context, key string) (any, error)
48+
Forever(ctx context.Context, key string, value any) error
49+
Increment(ctx context.Context, key string, by int64) (int64, error)
50+
Decrement(ctx context.Context, key string, by int64) (int64, error)
51+
Remember(ctx context.Context, key string, ttl time.Duration, compute func() (any, error)) (any, error)
52+
RememberForever(ctx context.Context, key string, compute func() (any, error)) (any, error)
53+
}
54+
```
55+
56+
Sentinel errors: `ErrCacheKeyNotFound`, `ErrCacheUnsupportedOperation`.
57+
58+
### Database
59+
60+
```go
61+
type Database interface {
62+
Close() error
63+
Ping(ctx context.Context) error
64+
Exec(ctx context.Context, query string, args ...any) (int64, error)
65+
ExecNamed(ctx context.Context, query string, arg any) (int64, error)
66+
Select(ctx context.Context, query string, dest any, args ...any) error
67+
SelectNamed(ctx context.Context, query string, dest any, arg any) error
68+
Find(ctx context.Context, query string, dest any, args ...any) error
69+
FindNamed(ctx context.Context, query string, dest any, arg any) error
70+
WithTransaction(ctx context.Context, fn func(Database) error) error
71+
}
72+
```
73+
74+
Sentinel errors: `ErrDatabaseNoRows`, `ErrDatabaseNestedTransaction`.
75+
Transactions cannot be nested. `Find` joins `sql.ErrNoRows` with
76+
`ErrDatabaseNoRows` — check with either via `errors.Is`.
77+
78+
### Encrypter
79+
80+
```go
81+
type Encrypter interface {
82+
Encrypt(value []byte) ([]byte, error)
83+
Decrypt(value []byte) ([]byte, error)
84+
}
85+
```
86+
87+
### Hasher
88+
89+
```go
90+
type Hasher interface {
91+
Hash(value []byte) ([]byte, error)
92+
Check(value, hash []byte) (bool, error)
93+
}
94+
```
95+
96+
### Session & SessionDriver
97+
98+
```go
99+
type Session interface {
100+
SessionID() string
101+
OriginalSessionID() string
102+
Get(key string) (any, bool)
103+
Put(key string, value any)
104+
Delete(key string)
105+
Extend(expiresAt time.Time)
106+
Regenerate() error
107+
Clear()
108+
ExpiresAt() time.Time
109+
HasExpired() bool
110+
ExpiresSoon(delta time.Duration) bool
111+
HasChanged() bool
112+
HasRegenerated() bool
113+
MarkAsUnchanged()
114+
}
115+
116+
type SessionDriver interface {
117+
Get(ctx context.Context, id string) (Session, error)
118+
Save(ctx context.Context, session Session, ttl time.Duration) error
119+
Delete(ctx context.Context, id string) error
120+
}
121+
```
122+
123+
### Events
124+
125+
```go
126+
type EventPayload = func(dest any) error
127+
type EventHandler = func(payload EventPayload)
128+
type EventUnsubscribeFunc = func() error
129+
130+
type Events interface {
131+
Publish(ctx context.Context, event string, payload any) error
132+
Subscribe(ctx context.Context, event string, handler EventHandler) (EventUnsubscribeFunc, error)
133+
Close() error
134+
}
135+
```
136+
137+
### Hooks
138+
139+
```go
140+
type AfterResponseHook = func(err error)
141+
type BeforeWriteHeaderHook = func(w http.ResponseWriter, status int)
142+
type BeforeWriteHook = func(w http.ResponseWriter, content []byte)
143+
144+
type Hooks interface {
145+
AfterResponse(hooks ...AfterResponseHook)
146+
AfterResponseFuncs() []AfterResponseHook
147+
BeforeWrite(hooks ...BeforeWriteHook)
148+
BeforeWriteFuncs() []BeforeWriteHook
149+
BeforeWriteHeader(hooks ...BeforeWriteHeaderHook)
150+
BeforeWriteHeaderFuncs() []BeforeWriteHeaderHook
151+
}
152+
```
153+
154+
Hooks are injected into request context by the framework. Access via
155+
`request.Hooks(r)`. Hook `*Funcs()` methods return reversed clones (LIFO
156+
execution order).
157+
158+
## Request Helpers
159+
160+
All functions in `contract/request` take `*http.Request` as first argument.
161+
162+
### Parameters & Query
163+
164+
```go
165+
request.Param(r, "id") // URL path parameter
166+
request.ParamOr(r, "id", "default") // with fallback
167+
request.Query(r, "page") // query string value
168+
request.QueryOr(r, "page", "1") // with fallback
169+
request.HasQuery(r, "verbose") // existence check
170+
```
171+
172+
### Headers & Cookies
173+
174+
```go
175+
request.Header(r, "Authorization")
176+
request.HeaderOr(r, "Accept", "application/json")
177+
request.HasHeader(r, "X-Custom")
178+
request.HeaderValues(r, "Accept") // []string
179+
request.Cookie(r, "session") // *http.Cookie
180+
request.CookieValue(r, "session") // string
181+
request.CookieValueOr(r, "session", "") // with fallback
182+
```
183+
184+
### Body Parsing (generics)
185+
186+
```go
187+
bytes, err := request.Bytes(r)
188+
text, err := request.String(r)
189+
user, err := request.JSON[User](r) // generic — type param required
190+
order, err := request.XML[Order](r) // generic — type param required
191+
```
192+
193+
Body is consumed on first read — call only once per request.
194+
195+
### Sessions
196+
197+
```go
198+
sess, ok := request.Session(r) // returns (Session, bool)
199+
sess := request.MustSession(r) // panics if no session middleware
200+
sess, ok := request.SessionKeyed(r, key) // custom context key
201+
sess := request.MustSessionKeyed(r, key) // panics variant
202+
```
203+
204+
`MustSession` and `MustSessionKeyed` panic when session middleware is not
205+
applied — these are programmer errors caught during development.
206+
207+
## Response Helpers
208+
209+
All functions in `contract/response` return `error` — usable as direct
210+
handler return values.
211+
212+
```go
213+
return response.Status(w, http.StatusNoContent)
214+
return response.String(w, http.StatusOK, "hello")
215+
return response.HTML(w, http.StatusOK, "<h1>hi</h1>")
216+
return response.JSON(w, http.StatusOK, user) // generic
217+
return response.XML(w, http.StatusOK, order)
218+
return response.Bytes(w, http.StatusOK, data)
219+
return response.Raw(w, http.StatusOK, rawBytes)
220+
return response.Redirect(w, http.StatusFound, "/login")
221+
222+
// Templates
223+
return response.StringTemplate(w, http.StatusOK, tmpl, data)
224+
return response.HTMLTemplate(w, http.StatusOK, tmpl, data)
225+
226+
// Streaming
227+
return response.Stream(w, r, dataChan) // raw streaming
228+
return response.SSE(w, r, eventChan) // Server-Sent Events
229+
```
230+
231+
`response.JSON` uses `json.NewEncoder` which appends a trailing newline.
232+
233+
## Mocks
234+
235+
Generated mocks in `contract/mock/` for all interfaces. Each has a
236+
`New<Name>Mock(t)` constructor that auto-registers cleanup.
237+
238+
```go
239+
cache := mock.NewCacheMock(t)
240+
cache.On("Get", mock.Anything, "key").Return("value", nil)
241+
```
242+
243+
## Gotchas
244+
245+
- `request.Hooks(r)` **panics** without hooks context (only present inside
246+
framework's handler pipeline).
247+
- `request.MustSession(r)` **panics** without session middleware.
248+
- Body parsing functions consume the request body — one call per request.
249+
- `response.JSON` appends a trailing newline (standard `json.NewEncoder`
250+
behavior).
251+
- `EventPayload` is a function — call it with a pointer to unmarshal:
252+
`payload(&user)`.
253+
- Context keys (`HooksKey`, `SessionKey`) are exported variables of
254+
unexported types — use the provided helpers, don't construct context
255+
values directly.

0 commit comments

Comments
 (0)