Files
bookhoard/internal/docs/schema.go
T
John O'Keefe 85cdaf7196 docs(libraries): Work Order 3 batch 2 — 10 missing endpoints + array-response gate support
New frontmatter'd docs with handler-compatible shapes:
- get_library_types.md (public; []LibraryTypes)
- list_libraries.md (admin; {data: ListLibrariesRow})
- get_library_folders.md (admin; []LibraryFolders)
- list/get stats/resolve/delete processing issues (admin; family-lens 403s
  documented)
- scan_library.md (202 + 409 coalescing with active_job_id)
- get/update scan-settings (ScanSettingsResponse)

Gate improvements forced by this batch:
- Array responses: registered slice -> doc example must be a JSON array; key
  set compared against the element type
- RawMessage pins may now be arrays
- update scan-settings uses a RawMessage pin: handler always sets Message but
  the struct's omitempty would reject correct docs via zero-value marshal

Registry: sqlc models pinned directly (LibraryTypes, LibraryFolders,
ListLibrariesRow — json tags verified in the generated code).
2026-10-24 14:57:45 -04:00

167 lines
5.5 KiB
Go

package docs
import (
"encoding/json"
"bookhoard/internal/database"
"bookhoard/internal/handlers"
"bookhoard/internal/services"
)
// 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": {},
// --- devices (batch 1) ---
"GET /api/devices/:id": {
Response: handlers.DeviceInfo{},
},
"PUT /api/devices/:id/regenerate-token": {
Response: json.RawMessage(`{
"message": "", "auth_token": "",
"device": {
"id": "", "device_name": "", "device_type": "",
"last_sync": null, "last_seen": null,
"sync_enabled": false, "auto_sync": false,
"sync_frequency_minutes": 0, "created_at": "",
"device_metadata": null, "auth_token": ""
},
"sync_urls": {}
}`),
},
"GET /api/devices/:id/collections": {
Response: struct {
DeviceID string `json:"device_id"`
Mappings []handlers.DeviceMappingSummary `json:"mappings"`
Total int `json:"total"`
}{},
},
"PUT /api/devices/:id/collections/:collectionId": {
Request: handlers.UpdateDeviceMappingRequest{},
Response: json.RawMessage(`{
"id": "", "collection_id": "", "device_id": "",
"device_shelf_name": "", "sync_direction": "", "created_at": ""
}`),
},
"GET /api/devices/:id/sidecar/download": {
Response: json.RawMessage(`{
"version": "", "bookhoard": {}, "books": {},
"collections": [], "opds_enabled": false,
"sidecar_enabled": false, "last_updated": ""
}`),
},
"POST /api/devices/:deviceId/sync/link-book": {
Request: services.LinkBookRequest{},
Response: json.RawMessage(`{
"status": "",
"device_file_alias": {
"id": "", "media_item_id": "", "device_id": "",
"file_path": "", "file_sha256": "",
"confidence_score": 0.0
}
}`),
},
// --- libraries (batch 2) ---
"GET /api/libraries/types": {
Response: []database.LibraryTypes{},
},
"GET /api/libraries": {
Response: struct {
Data []database.ListLibrariesRow `json:"data"`
}{},
},
"GET /api/libraries/:id/folders": {
Response: []database.LibraryFolders{},
},
"GET /api/libraries/:id/issues/list": {
Response: []handlers.ProcessingIssueResponse{},
},
"GET /api/libraries/:id/issues/stats": {
Response: handlers.ProcessingIssueStats{},
},
"POST /api/libraries/:id/issues/:issueId/:mediaItemId/resolve": {
Response: json.RawMessage(`{"success": false, "message": ""}`),
},
"DELETE /api/libraries/:id/issues/:issueId": {
Response: json.RawMessage(`{"success": false, "message": ""}`),
},
"POST /api/libraries/:id/scan": {
Request: nil, // library_id injected from the path; no body used
Response: json.RawMessage(`{"message": "", "job_id": "", "status": ""}`),
},
"GET /api/libraries/scan-settings": {
Response: handlers.ScanSettingsResponse{},
},
"PUT /api/libraries/scan-settings": {
Request: handlers.UpdateScanSettingsRequest{},
// RawMessage pin (not the struct): the update handler ALWAYS sets
// Message, but the struct's omitempty would drop it from a
// zero-value marshal and the gate would reject correct docs.
Response: json.RawMessage(`{
"scan_poll_interval_seconds": 0,
"auto_scan_enabled": false,
"message": ""
}`),
},
}