|
| 1 | +--- |
| 2 | +name: cosmos-go |
| 3 | +description: > |
| 4 | + Project-specific Go skill for the Cosmos monorepo. Must use when writing, |
| 5 | + editing, planning, or reviewing any Go (.go) files in this workspace. |
| 6 | + Covers code style, naming, documentation, error handling, immutability |
| 7 | + patterns, testing, and module architecture. Use this skill instead of the |
| 8 | + global Go skill whenever working inside the Cosmos codebase. |
| 9 | +--- |
| 10 | + |
| 11 | +# Cosmos Go |
| 12 | + |
| 13 | +Go conventions and patterns for the Cosmos HTTP framework monorepo. |
| 14 | +Go 1.25+ workspace with four modules: contract, router, problem, framework. |
| 15 | + |
| 16 | +## Project Structure |
| 17 | + |
| 18 | +``` |
| 19 | +cosmos/ |
| 20 | +├── go.work # Workspace: use + replace directives |
| 21 | +├── contract/ # Interfaces (zero deps) - the foundation |
| 22 | +├── router/ # Generic HTTP router (zero deps) |
| 23 | +├── problem/ # RFC 9457 problem details (zero deps) |
| 24 | +│ └── internal/ # Accept header parsing (not exported) |
| 25 | +└── framework/ # Full framework (depends on all above) |
| 26 | + ├── cache/ # contract.Cache implementations |
| 27 | + ├── crypto/ # contract.Encrypter implementations |
| 28 | + ├── database/ # contract.Database implementation |
| 29 | + ├── event/ # contract.EventBus implementations |
| 30 | + ├── hash/ # contract.Hasher implementations |
| 31 | + ├── middleware/ # Ready-made middleware |
| 32 | + └── session/ # Session management |
| 33 | +``` |
| 34 | + |
| 35 | +**Dependency direction:** contract (zero deps) -> router, problem (standalone) -> framework (uses all). |
| 36 | + |
| 37 | +Always run tests from workspace root: `go test ./...` |
| 38 | +Test a single module: `go test ./router/...` |
| 39 | + |
| 40 | +## Formatting |
| 41 | + |
| 42 | +Standard `gofmt`. No exceptions. |
| 43 | + |
| 44 | +### Import Groups |
| 45 | + |
| 46 | +Separate groups with a blank line, alphabetical within each group: |
| 47 | + |
| 48 | +```go |
| 49 | +import ( |
| 50 | + "encoding/json" |
| 51 | + "fmt" |
| 52 | + "net/http" |
| 53 | + |
| 54 | + "github.com/studiolambda/cosmos/problem/internal" |
| 55 | + |
| 56 | + "github.com/stretchr/testify/assert" |
| 57 | +) |
| 58 | +``` |
| 59 | + |
| 60 | +- Group 1: Standard library |
| 61 | +- Group 2: Internal cosmos modules |
| 62 | +- Group 3: External dependencies |
| 63 | + |
| 64 | +When only stdlib imports exist, use a single group. |
| 65 | + |
| 66 | +## Naming |
| 67 | + |
| 68 | +### Receiver Names |
| 69 | + |
| 70 | +Use **full-word receiver names** matching the type name in lowercase. This is a firm convention. |
| 71 | + |
| 72 | +```go |
| 73 | +// Correct |
| 74 | +func (problem Problem) With(key string, value any) Problem { ... } |
| 75 | +func (router *Router[H]) Group(pattern string, fn func(*Router[H])) { ... } |
| 76 | +func (accept Accept) Quality(media string) float64 { ... } |
| 77 | + |
| 78 | +// Wrong |
| 79 | +func (p Problem) With(key string, value any) Problem { ... } |
| 80 | +func (r *Router[H]) Group(pattern string, fn func(*Router[H])) { ... } |
| 81 | +``` |
| 82 | + |
| 83 | +Rationale: Short names create ambiguity in methods >10 lines and make grep/search |
| 84 | +less useful. `r` could be a Router, Request, Reader, or Redis client. Full words |
| 85 | +eliminate that class of confusion entirely. |
| 86 | + |
| 87 | +### General Naming |
| 88 | + |
| 89 | +- Descriptive names over abbreviations: `subrouter` not `sr`, `handler` not `h` (exception: named return values like `h H, ok bool` are fine). |
| 90 | +- PascalCase for exported, camelCase for unexported. |
| 91 | +- Constructor: `New` for single-type packages, `NewTypeName` when multiple types exist. |
| 92 | +- Symmetric method pairs: `With`/`Without`, `WithError`/`WithoutError`. |
| 93 | +- Past participle for methods returning a modified copy: `Defaulted`, `Grouped`. |
| 94 | + |
| 95 | +## Functions |
| 96 | + |
| 97 | +- **Small and focused.** Target <30 lines body, <5 cyclomatic complexity. |
| 98 | +- **Single purpose.** If you need a comment saying "now do the second thing," extract a function. |
| 99 | +- **Early returns over nesting.** Use guard clauses for preconditions, errors, and edge cases. |
| 100 | +- **Zero `else` after early returns.** The happy path flows downward without indentation. |
| 101 | + |
| 102 | +```go |
| 103 | +// Correct: early return, no else |
| 104 | +func (problem Problem) With(key string, value any) Problem { |
| 105 | + if problem.additional == nil { |
| 106 | + problem.additional = map[string]any{key: value} |
| 107 | + |
| 108 | + return problem |
| 109 | + } |
| 110 | + |
| 111 | + problem.additional = maps.Clone(problem.additional) |
| 112 | + problem.additional[key] = value |
| 113 | + |
| 114 | + return problem |
| 115 | +} |
| 116 | + |
| 117 | +// Wrong: unnecessary else |
| 118 | +func (problem Problem) With(key string, value any) Problem { |
| 119 | + if problem.additional == nil { |
| 120 | + problem.additional = map[string]any{key: value} |
| 121 | + } else { |
| 122 | + problem.additional = maps.Clone(problem.additional) |
| 123 | + problem.additional[key] = value |
| 124 | + } |
| 125 | + |
| 126 | + return problem |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +## Blank Lines |
| 131 | + |
| 132 | +Use blank lines to separate logical blocks within functions: |
| 133 | + |
| 134 | +- After variable declarations before logic. |
| 135 | +- Between guard clauses / early-return blocks. |
| 136 | +- Between distinct logical steps. |
| 137 | +- After `struct {` opening when fields have doc-comment blocks. |
| 138 | + |
| 139 | +Do not use blank lines between tightly coupled one-liners (e.g. sequential `delete()` calls). |
| 140 | + |
| 141 | +## Documentation |
| 142 | + |
| 143 | +### Doc Comments |
| 144 | + |
| 145 | +All exported symbols get doc comments. Start with the symbol name: |
| 146 | + |
| 147 | +```go |
| 148 | +// ParseAccept creates a new [Accept] based on a |
| 149 | +// [http.Request] by parsing its headers. |
| 150 | +func ParseAccept(request *http.Request) Accept { |
| 151 | +``` |
| 152 | +
|
| 153 | +Unexported functions with non-obvious logic also get doc comments: |
| 154 | +
|
| 155 | +```go |
| 156 | +// stackTrace creates a stack trace of all the errors found |
| 157 | +// that have been either Joined or Wrapped using [errors.Join] |
| 158 | +// or [fmt.Errorf] with `%w` directive. |
| 159 | +func stackTrace(err error) []error { |
| 160 | +``` |
| 161 | +
|
| 162 | +### Godoc Links |
| 163 | +
|
| 164 | +Use bracket syntax to cross-reference types and functions: |
| 165 | +
|
| 166 | +```go |
| 167 | +// With adds a new additional value to the given key. |
| 168 | +// Use [Problem.Without] to remove values. |
| 169 | +// See [NewProblem] for creating problems from errors. |
| 170 | +``` |
| 171 | +
|
| 172 | +### Struct Field Comments |
| 173 | +
|
| 174 | +For structs with >3 fields, document each field with a comment block above it. |
| 175 | +For structs with 1-3 fields, inline comments or a single doc comment on the type suffice. |
| 176 | +
|
| 177 | +### Sentence Completeness |
| 178 | +
|
| 179 | +Doc comments must be complete sentences. End with a period. Do not leave |
| 180 | +truncated sentences. |
| 181 | +
|
| 182 | +## Error Handling |
| 183 | +
|
| 184 | +- Early-return on error: `if err != nil { return err }`. |
| 185 | +- Use `errors.Is()` and `errors.As()` for error inspection, never type assertions. |
| 186 | +- When intentionally discarding an error, use explicit `_ =` and comment the reason: |
| 187 | +
|
| 188 | +```go |
| 189 | +// Error is intentionally discarded: headers are already written, |
| 190 | +// nothing useful can be done if encoding fails. |
| 191 | +_ = json.NewEncoder(w).Encode(problem) |
| 192 | +``` |
| 193 | +
|
| 194 | +- Never silently swallow errors without a documented reason. |
| 195 | +- Error strings are lowercase, no punctuation at the end. |
| 196 | +
|
| 197 | +## Design Patterns |
| 198 | +
|
| 199 | +### Immutability (Copy-on-Write) |
| 200 | +
|
| 201 | +For types holding mutable internal state (maps, slices), prefer value receivers |
| 202 | +and methods that return modified copies rather than mutating in place. |
| 203 | +
|
| 204 | +Clone mutable fields before modification to prevent aliasing: |
| 205 | +
|
| 206 | +```go |
| 207 | +func (problem Problem) With(key string, value any) Problem { |
| 208 | + problem.additional = maps.Clone(problem.additional) |
| 209 | + problem.additional[key] = value |
| 210 | + |
| 211 | + return problem |
| 212 | +} |
| 213 | +``` |
| 214 | +
|
| 215 | +This enables safe derivation from package-level variables: `ErrNotFound.With("id", 42)`. |
| 216 | +
|
| 217 | +Use `maps.Clone`, `slices.Clone` from the standard library. Never hand-roll copy loops. |
| 218 | +
|
| 219 | +When mutation is the natural API (like `Router.Use()`), document the contrast |
| 220 | +with the immutable alternative clearly. |
| 221 | +
|
| 222 | +### Encapsulation |
| 223 | +
|
| 224 | +Unexported struct fields, exported methods. Implementation details go in |
| 225 | +`internal/` packages when they serve a single parent package. |
| 226 | +
|
| 227 | +### Interface Satisfaction |
| 228 | +
|
| 229 | +Satisfy interfaces implicitly (duck typing). Do not add explicit compile-time checks |
| 230 | +like `var _ Interface = Type{}` unless the satisfaction is non-obvious or critical. |
| 231 | +
|
| 232 | +Common interfaces to implement: `error`, `http.Handler`, `json.Marshaler`/`json.Unmarshaler`. |
| 233 | +
|
| 234 | +### Generics |
| 235 | +
|
| 236 | +Use type constraints meaningfully. Prefer named constraints over inline unions: |
| 237 | +
|
| 238 | +```go |
| 239 | +type Middleware[H http.Handler] = func(H) H // type alias preserves assignability |
| 240 | +type Router[H http.Handler] struct { ... } // constrained to http.Handler |
| 241 | +``` |
| 242 | +
|
| 243 | +### Modern Standard Library |
| 244 | +
|
| 245 | +Prefer modern Go stdlib functions: |
| 246 | +
|
| 247 | +- `strings.CutPrefix`, `strings.CutSuffix` over manual `HasPrefix` + `TrimPrefix` combinations. |
| 248 | +- `strings.SplitSeq` (iterator-based) over `strings.Split` when not materializing the full slice. |
| 249 | +- `slices.SortFunc`, `slices.Clone` over hand-rolled sort/copy. |
| 250 | +- `maps.Clone`, `maps.Copy` over manual map iteration. |
| 251 | +- `path.Join` for URL path construction. |
| 252 | +
|
| 253 | +## Testing |
| 254 | +
|
| 255 | +See [references/testing-guide.md](references/testing-guide.md) for complete patterns and examples. |
| 256 | +
|
| 257 | +Summary: |
| 258 | +
|
| 259 | +- **Atomic test functions.** One concern per test function. No table-driven tests. |
| 260 | +- **Parallel by default.** Call `t.Parallel()` in every test unless shared state prevents it. |
| 261 | +- **Black-box testing.** Use `package foo_test` (external test package). |
| 262 | +- **Testify assertions.** Use `require` for fatal checks, `assert` for non-fatal. |
| 263 | +- **Descriptive names.** `TestProblemWithAddsAdditionalValue`, `TestRouterHasMatchesTrailingSlash`. |
| 264 | +- **httptest for HTTP.** `httptest.NewRequest` + `httptest.NewRecorder` or `Router.Record()`. |
| 265 | +
|
| 266 | +## When to Load References |
| 267 | +
|
| 268 | +- **Writing or reviewing Go source code:** See [references/code-patterns.md](references/code-patterns.md) for annotated examples of all major patterns (immutability, middleware, constructors, content negotiation, recursive resolution). |
| 269 | +- **Writing or reviewing tests:** See [references/testing-guide.md](references/testing-guide.md) for test structure, assertion patterns, HTTP testing, and anti-patterns. |
0 commit comments