fix(api): resolve all OpenAPI schema reference errors

- Add #[schema(value_type = Option<String>)] on chrono::DateTime fields
- Register SeriesPage in openapi.rs components
- Fix module-prefixed ref (index_jobs::IndexJobResponse -> IndexJobResponse)
- Strengthen test: assert all $ref targets exist in components/schemas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 22:27:52 +01:00
parent f1b3aec94a
commit 4c75e08056
3 changed files with 23 additions and 12 deletions

View File

@@ -18,6 +18,7 @@ pub struct LibraryResponse {
pub book_count: i64, pub book_count: i64,
pub monitor_enabled: bool, pub monitor_enabled: bool,
pub scan_mode: String, pub scan_mode: String,
#[schema(value_type = Option<String>)]
pub next_scan_at: Option<chrono::DateTime<chrono::Utc>>, pub next_scan_at: Option<chrono::DateTime<chrono::Utc>>,
pub watcher_enabled: bool, pub watcher_enabled: bool,
} }

View File

@@ -42,6 +42,7 @@ use utoipa::OpenApi;
crate::books::BooksPage, crate::books::BooksPage,
crate::books::BookDetails, crate::books::BookDetails,
crate::books::SeriesItem, crate::books::SeriesItem,
crate::books::SeriesPage,
crate::pages::PageQuery, crate::pages::PageQuery,
crate::search::SearchQuery, crate::search::SearchQuery,
crate::search::SearchResponse, crate::search::SearchResponse,
@@ -118,15 +119,24 @@ mod tests {
.to_pretty_json() .to_pretty_json()
.expect("Failed to serialize OpenAPI"); .expect("Failed to serialize OpenAPI");
// Check that there are no references to non-existent schemas // Check that all $ref targets exist in components/schemas
assert!( let doc: serde_json::Value =
!json.contains("\"/components/schemas/Uuid\""), serde_json::from_str(&json).expect("OpenAPI JSON should be valid");
"Uuid schema should not be referenced" let empty = serde_json::Map::new();
); let schemas = doc["components"]["schemas"]
assert!( .as_object()
!json.contains("\"/components/schemas/DateTime\""), .unwrap_or(&empty);
"DateTime schema should not be referenced" let prefix = "#/components/schemas/";
); let mut broken: Vec<String> = Vec::new();
for part in json.split(prefix).skip(1) {
if let Some(name) = part.split('"').next() {
if !schemas.contains_key(name) {
broken.push(name.to_string());
}
}
}
broken.dedup();
assert!(broken.is_empty(), "Unresolved schema refs: {:?}", broken);
// Save to file for inspection // Save to file for inspection
std::fs::write("/tmp/openapi.json", &json).expect("Failed to write file"); std::fs::write("/tmp/openapi.json", &json).expect("Failed to write file");

View File

@@ -6,7 +6,7 @@ use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use utoipa::ToSchema; use utoipa::ToSchema;
use crate::{error::ApiError, index_jobs, state::AppState}; use crate::{error::ApiError, index_jobs::{self, IndexJobResponse}, state::AppState};
#[derive(Deserialize, ToSchema)] #[derive(Deserialize, ToSchema)]
pub struct ThumbnailsRebuildRequest { pub struct ThumbnailsRebuildRequest {
@@ -21,7 +21,7 @@ pub struct ThumbnailsRebuildRequest {
tag = "indexing", tag = "indexing",
request_body = Option<ThumbnailsRebuildRequest>, request_body = Option<ThumbnailsRebuildRequest>,
responses( responses(
(status = 200, body = index_jobs::IndexJobResponse), (status = 200, body = IndexJobResponse),
(status = 401, description = "Unauthorized"), (status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - Admin scope required"), (status = 403, description = "Forbidden - Admin scope required"),
), ),
@@ -55,7 +55,7 @@ pub async fn start_thumbnails_rebuild(
tag = "indexing", tag = "indexing",
request_body = Option<ThumbnailsRebuildRequest>, request_body = Option<ThumbnailsRebuildRequest>,
responses( responses(
(status = 200, body = index_jobs::IndexJobResponse), (status = 200, body = IndexJobResponse),
(status = 401, description = "Unauthorized"), (status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - Admin scope required"), (status = 403, description = "Forbidden - Admin scope required"),
), ),