//! Machine-readable `OpenAPI` 3.1 specification for the tidalDB HTTP API. //! //! Two [`OpenApi`](utoipa::OpenApi) documents are derived from the //! `#[utoipa::path(...)]` attributes on the handlers and the //! `#[derive(ToSchema)]` on the DTOs: //! //! * [`StandaloneApiDoc`] — the data + health surface served by //! [`crate::router::build_router`]. //! * [`ClusterApiDoc`] — the same surface PLUS the cluster-management and //! sharded (scatter-gather) routes served by //! [`crate::cluster::build_cluster_router`]. //! //! Both are served UNAUTHENTICATED at `GET /openapi.json` (sibling to the //! `/health/*` probes): the document describes the API contract only — it //! carries no entity data, signals, or secrets — so gating it behind the same //! bearer token a client needs the spec to learn how to send would be //! self-defeating. The `bearerAuth` security scheme is declared via the //! [`SecurityAddon`] modifier so generated clients know the data routes require //! a token; the spec endpoint itself is exempt. use axum::Json; use utoipa::{ Modify, OpenApi, openapi::security::{Http, HttpAuthScheme, SecurityScheme}, }; /// `GET /openapi.json` handler for the **standalone** surface. /// /// Mounted unauthenticated alongside the `/health/*` probes by /// [`crate::router::build_router`]. Returns the derived `OpenAPI` 3.1 document /// as JSON (`utoipa::openapi::OpenApi` is `Serialize`). pub(crate) async fn serve_standalone() -> Json { Json(StandaloneApiDoc::openapi()) } /// `GET /openapi.json` handler for the **cluster** surface — the superset /// document that also describes the `/cluster/*` and `/sharded/*` routes. /// Mounted by [`crate::cluster::build_cluster_router`]. pub(crate) async fn serve_cluster() -> Json { Json(ClusterApiDoc::openapi()) } /// Injects the `bearerAuth` HTTP security scheme into the components. /// /// The per-handler `security(("bearerAuth" = []))` attributes reference this /// scheme by name; without registering it here the generated document would /// name an undefined scheme. Health and `/openapi.json` carry no `security` /// requirement, so they remain open. struct SecurityAddon; impl Modify for SecurityAddon { fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { // `components` is always `Some` once any schema is registered, but guard // rather than unwrap so an empty-component build degrades gracefully // (the crate forbids `unwrap`/`expect` in non-test code). let components = openapi.components.get_or_insert_with(Default::default); components.add_security_scheme( "bearerAuth", SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), ); } } /// `OpenAPI` document for the standalone HTTP surface. #[derive(OpenApi)] #[openapi( info( title = "tidalDB HTTP API", version = env!("CARGO_PKG_VERSION"), description = "Embeddable, single-node-first database for the personalized \ content ranking problem. Write items, embeddings, and signals; \ retrieve ranked feeds and run hybrid search. Data routes require \ a Bearer token when TIDAL_API_KEY is configured; health probes \ and this spec do not.", ), paths( crate::router::health, crate::router::create_item, crate::router::write_embedding, crate::router::write_signal, crate::router::feed, crate::router::search, ), components(schemas( crate::dto::ItemRequest, crate::dto::EmbeddingRequest, crate::dto::SignalRequest, crate::dto::FeedResponse, crate::dto::FeedItem, crate::dto::SignalValue, crate::dto::SearchResponse, crate::dto::SearchItem, )), modifiers(&SecurityAddon), tags( (name = "health", description = "Liveness / readiness probes (unauthenticated)."), (name = "data", description = "Item, embedding, signal writes and ranked reads."), ), )] pub struct StandaloneApiDoc; /// `OpenAPI` document for the cluster HTTP surface. /// /// Superset of [`StandaloneApiDoc`]: the same data + health routes plus the /// cluster-management (`/cluster/*`) and sharded scatter-gather /// (`/sharded/*`) routes. The cluster handlers register the SAME `/items`, /// `/embeddings`, `/signals`, `/feed`, `/search` paths as the standalone ones; /// because this is a distinct document the shared paths are described once here /// from the cluster handlers' attributes (region-aware response text), so there /// is no path-key collision. #[derive(OpenApi)] #[openapi( info( title = "tidalDB HTTP API", version = env!("CARGO_PKG_VERSION"), description = "Cluster-mode tidalDB HTTP surface: the standalone data + health \ routes plus multi-region cluster management and sharded \ scatter-gather routes. EXPERIMENTAL — all regions run in one \ process (no host/process isolation).", ), paths( crate::cluster::cluster_status, crate::cluster::cluster_promote, crate::cluster::cluster_partition, crate::cluster::cluster_heal, crate::cluster::create_item, crate::cluster::write_embedding, crate::cluster::write_signal, crate::cluster::feed, crate::cluster::search, crate::cluster::sharded_create_item, crate::cluster::sharded_write_embedding, crate::cluster::sharded_write_signal, crate::cluster::sharded_feed, crate::cluster::sharded_search, ), components(schemas( crate::dto::ItemRequest, crate::dto::EmbeddingRequest, crate::dto::SignalRequest, crate::dto::FeedResponse, crate::dto::FeedItem, crate::dto::SignalValue, crate::dto::SearchResponse, crate::dto::SearchItem, crate::cluster::ClusterStatusResponse, crate::cluster::RegionStatus, crate::cluster::RegionRequest, crate::cluster::ScatterGatherInfo, crate::cluster::ShardedFeedResponse, crate::cluster::ShardedSearchResponse, )), modifiers(&SecurityAddon), tags( (name = "health", description = "Liveness / readiness probes (unauthenticated)."), (name = "data", description = "Item, embedding, signal writes and ranked reads."), (name = "cluster", description = "Multi-region cluster management."), (name = "sharded", description = "Scatter-gather routes fanning across all shards."), ), )] pub struct ClusterApiDoc; #[cfg(test)] mod tests { use super::*; /// The standalone document must enumerate the data + health paths and carry /// the bearerAuth scheme so generated clients know how to authenticate. #[test] fn standalone_doc_lists_core_paths() { let doc = StandaloneApiDoc::openapi(); let paths = &doc.paths.paths; for p in [ "/health", "/items", "/embeddings", "/signals", "/feed", "/search", ] { assert!(paths.contains_key(p), "standalone doc missing path {p}"); } // The cluster-only routes must NOT leak into the standalone document. assert!( !paths.contains_key("/cluster/status"), "standalone doc must not advertise cluster routes" ); let schemes = &doc .components .as_ref() .expect("components present after schema registration") .security_schemes; assert!( schemes.contains_key("bearerAuth"), "SecurityAddon must register the bearerAuth scheme" ); } /// The cluster document is a superset: data + health + cluster + sharded. #[test] fn cluster_doc_lists_cluster_and_sharded_paths() { let doc = ClusterApiDoc::openapi(); let paths = &doc.paths.paths; for p in [ "/feed", "/cluster/status", "/cluster/promote", "/cluster/partition", "/cluster/heal", "/sharded/items", "/sharded/feed", "/sharded/search", ] { assert!(paths.contains_key(p), "cluster doc missing path {p}"); } } /// The served version string is the crate version, exposed for the route's /// contract test and the verification report. #[test] fn doc_version_is_crate_version() { let doc = StandaloneApiDoc::openapi(); assert_eq!(doc.info.version, env!("CARGO_PKG_VERSION")); } }