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": "" }`), }, // --- media-items batch 3a: reader-adjacent CRUD --- "GET /api/media-items/:id/bookmarks": { Response: []database.MediaBookmarks{}, }, "POST /api/media-items/:id/bookmarks": { Request: handlers.CreateMediaBookmarkRequest{}, Response: database.MediaBookmarks{}, }, "PUT /api/media-items/:id/bookmarks/:bookmarkId": { Request: handlers.UpdateMediaBookmarkRequest{}, Response: database.MediaBookmarks{}, }, "DELETE /api/media-items/:id/bookmarks/:bookmarkId": {}, // Review: the upsert binds an inline anonymous struct {review}; pinned // by RawMessage. Empty review string = delete, returns {"review": null}. "POST /api/media-items/:id/review": { Request: json.RawMessage(`{"review": ""}`), Response: database.MediaReviews{}, }, "PUT /api/media-items/:id/review": { Request: json.RawMessage(`{"review": ""}`), Response: database.MediaReviews{}, }, "GET /api/media-items/:id/review": { Response: database.MediaReviews{}, }, "DELETE /api/media-items/:id/review": { Response: json.RawMessage(`{"message": ""}`), }, "GET /api/media-items/:id/notes/:noteId": { Response: database.MediaNotes{}, }, "GET /api/media-items/:id/highlights/:highlightId": { Response: database.MediaHighlights{}, }, "POST /api/media-items/:id/annotations/:annotationId/restore": { Request: nil, // kind via ?annotation_type= or optional body Response: json.RawMessage(`{"restored": false}`), }, "DELETE /api/media-items/:id/annotations/:annotationId": { Request: nil, // kind via required ?annotation_type= Response: json.RawMessage(`{"purged": false}`), }, }