feat(docs): drift gate — docs frontmatter validated against real routes
Every API doc that declares frontmatter (method/path/auth/area) is now checked by a unit test against three sources of truth: 1. Route registration: internal/router/*.go is parsed the way echo resolves it (group prefixes concatenate; scanner.go's parameterized groups seeded). Doc paths that no longer exist fail. (This is the check that would have caught the /api/api/admin/hash-conflicts split.) 2. Auth: frontmatter auth label must match the middleware chain (none|jwt|jwt+admin|device-token|optional-jwt). 3. Shapes: internal/docs/schema.go registry — example-request decoded with unknown fields DISALLOWED (typo guard), required-by-validate fields must appear (staleness guard), example-response top-level key set compared both directions against the marshaled handler type (or a RawMessage pin for hand-built map responses). Five collections docs are the pilots with real handler shapes; notable corrections they pin: update returns 201 (not 200), list excludes smart collections unless include_auto=true, rule priority is 1-10 higher-wins (docs claimed 1=highest), created_at is Go time.String() not RFC3339, hide_finished/show_up_next toggles documented. CollectionResponse hoisted out of GetCollections/GetBookCollections as CollectionSummary / BookCollectionSummary so the registry can pin real types. The gate fails on missing registry entries by design — that is the maintenance contract. Known pre-existing (not this commit): vet copy-lock warnings in media.go.
This commit is contained in:
1 parent
994e0f86c4
commit
cc893f758e
8 files changed
+824
-168
No files matched your search
@@ -0,0 +1,72 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"bookhoard/internal/handlers"
|
||||
)
|
||||
|
||||
// Schema pins the machine-checkable contract for one route. The drift gate
|
||||
// (drift_test.go) validates every frontmatter'd doc against the entry for its
|
||||
// "METHOD path" key.
|
||||
//
|
||||
// MAINTENANCE CONTRACT: every doc with frontmatter MUST have a registry
|
||||
// entry, and every new/changed route that ships a frontmatter'd doc updates
|
||||
// its entry in the same commit. The gate fails loudly on a missing entry —
|
||||
// that is the discipline, not an inconvenience.
|
||||
//
|
||||
// Request: the struct the handler Binds into (nil for bodyless routes).
|
||||
// - example-request is decoded with unknown fields DISALLOWED (typo guard)
|
||||
// - fields whose validate tag contains "required" must appear in the
|
||||
// example (staleness guard)
|
||||
//
|
||||
// Response: what the handler passes to c.JSON.
|
||||
// - struct / slice of struct: marshaled and its TOP-LEVEL key set compared
|
||||
// against example-response in both directions
|
||||
// - json.RawMessage: hand-built map responses pin their key set as a
|
||||
// sample; promote to a struct when the endpoint is next touched
|
||||
// - zero value (Schema{}): no JSON body expected (e.g. 204) — the doc must
|
||||
// not carry an example-response block
|
||||
type Schema struct {
|
||||
Request any
|
||||
Response any
|
||||
}
|
||||
|
||||
// Schemas is keyed by "METHOD path" in Echo syntax. Sample values inside
|
||||
// RawMessage pins use empty strings / zero values — only the KEY SET is
|
||||
// checked, never the values.
|
||||
var Schemas = map[string]Schema{
|
||||
// --- collections (pilot area) ---
|
||||
"POST /api/collections": {
|
||||
Request: handlers.CreateCollectionRequest{},
|
||||
Response: json.RawMessage(`{
|
||||
"id": "", "user_id": "", "name": "", "description": "",
|
||||
"color": "", "icon": "", "collection_type": "",
|
||||
"auto_assign_rules": null, "view_settings": {},
|
||||
"book_count": 0, "created_at": ""
|
||||
}`),
|
||||
},
|
||||
"GET /api/collections": {
|
||||
Response: struct {
|
||||
Collections []handlers.CollectionSummary `json:"collections"`
|
||||
Total int `json:"total"`
|
||||
}{},
|
||||
},
|
||||
"GET /api/collections/:id": {
|
||||
Response: json.RawMessage(`{
|
||||
"id": "", "user_id": "", "name": "", "description": "",
|
||||
"color": "", "icon": "", "auto_assign_rules": null,
|
||||
"view_settings": {}, "books": [], "book_count": 0,
|
||||
"is_smart": false, "up_next": [], "created_at": ""
|
||||
}`),
|
||||
},
|
||||
"PUT /api/collections/:id": {
|
||||
Request: handlers.UpdateCollectionRequest{},
|
||||
Response: json.RawMessage(`{
|
||||
"id": "", "user_id": "", "name": "", "description": "",
|
||||
"color": "", "icon": "", "auto_assign_rules": null,
|
||||
"view_settings": {}, "created_at": ""
|
||||
}`),
|
||||
},
|
||||
"DELETE /api/collections/:id": {},
|
||||
}
|
||||
Reference in new issue
Block a user