Skip to main content

doxa_docs/
lib.rs

1//! # doxa
2//!
3//! Ergonomic OpenAPI documentation for axum services. Built on top of
4//! [`utoipa`] and [`utoipa_axum`], this crate provides:
5//!
6//! - An [`ApiDocBuilder`] for assembling an OpenAPI document from a
7//!   [`utoipa::openapi::OpenApi`] value, finalizing it into an in-memory
8//!   [`ApiDoc`] whose serialized JSON is shared via a reference-counted
9//!   [`bytes::Bytes`] buffer.
10//! - A [`mount_docs`] helper that mounts `GET /openapi.json` plus an
11//!   interactive documentation UI on an existing [`axum::Router`], all from
12//!   memory — the spec is never written to disk.
13//! - An RFC 7807 [`ProblemDetails`] response body usable as the default error
14//!   schema across a project.
15//!
16//! All UI integrations are feature-gated independently. The default
17//! feature set enables [`docs-scalar`](crate#features) which mounts
18//! the Scalar API reference UI from a CDN-loaded HTML template,
19//! rendered out of the box with the three-pane `modern` layout, dark
20//! mode on, the schemas index hidden, the codegen sidebar suppressed,
21//! and Scalar's paid product upsells (Agent / MCP) disabled. Every
22//! one of those choices is overridable via [`ScalarConfig`] passed
23//! through [`MountOpts::scalar`]. Scalar is preferred because it is
24//! actively maintained, parses OpenAPI 3.2 natively, renders the
25//! `x-badges` vendor extension, and surfaces required OAuth2 scopes
26//! inline under each operation — covering per-operation permission
27//! requirements produced by extractor-side [`DocOperationSecurity`]
28//! impls.
29//!
30//! # Tour
31//!
32//! The full surface of the crate, end to end. Every macro, derive,
33//! extractor, and builder method that ships in the default feature set
34//! appears in the snippet below and the whole thing compiles under
35//! `cargo test --doc`.
36//!
37//! ```no_run
38//! use axum::Json;
39//! use doxa::{
40//!     routes, ApiDocBuilder, ApiErrorBody, ApiResult, DocumentedHeader, Header,
41//!     MountDocsExt, MountOpts, OpenApiRouter, ScalarConfig, ScalarLayout, ScalarTheme,
42//!     SseEvent, SseEventMeta, SseSpecVersion, SseStream, ToSchema,
43//! };
44//! use doxa::{get, post, ApiError};
45//! use futures_core::Stream;
46//! use serde::{Deserialize, Serialize};
47//! use std::convert::Infallible;
48//!
49//! // ----- Typed error envelope ----------------------------------------------
50//! //
51//! // `#[derive(ApiError)]` wires both `IntoResponse` and `IntoResponses`
52//! // from per-variant `#[api(status, code)]` attributes. Multiple
53//! // variants may share a status — they are grouped into one OpenAPI
54//! // response with separate examples.
55//! #[derive(Debug, thiserror::Error, Serialize, ToSchema, ApiError)]
56//! enum WidgetError {
57//!     #[error("validation failed: {0}")]
58//!     #[api(status = 400, code = "validation_error")]
59//!     Validation(String),
60//!
61//!     #[error("conflict: {0}")]
62//!     #[api(status = 400, code = "conflict")]
63//!     Conflict(String),
64//!
65//!     #[error("not found")]
66//!     #[api(status = 404, code = "not_found")]
67//!     NotFound,
68//!
69//!     #[error("internal error")]
70//!     #[api(status = 500, code = "internal")]
71//!     Internal,
72//! }
73//!
74//! // ----- Typed request / response bodies -----------------------------------
75//! #[derive(Debug, Serialize, ToSchema)]
76//! struct Widget { id: u32, name: String }
77//!
78//! #[derive(Debug, Deserialize, ToSchema)]
79//! struct CreateWidget { name: String }
80//!
81//! // ----- Typed header extractor --------------------------------------------
82//! //
83//! // Implementing `DocumentedHeader` on a marker type lets the same
84//! // marker drive both extraction (via `Header<XApiKey>`) and OpenAPI
85//! // documentation. The macro recognizes `Header<H>` in the handler
86//! // signature and emits the corresponding params block automatically.
87//! struct XApiKey;
88//! impl DocumentedHeader for XApiKey {
89//!     fn name() -> &'static str { "X-Api-Key" }
90//!     fn description() -> &'static str { "Tenant API key" }
91//! }
92//!
93//! // ----- SSE event stream --------------------------------------------------
94//! //
95//! // `#[derive(SseEvent)]` provides the per-variant event name; pair
96//! // it with `serde::Serialize` and `ToSchema` so the wire format and
97//! // OpenAPI schema stay aligned. `SseStream<E, S>` is the response
98//! // wrapper — handlers never construct axum's `Sse` directly.
99//! #[derive(Serialize, ToSchema, SseEvent)]
100//! #[serde(tag = "event", content = "data", rename_all = "snake_case")]
101//! enum BuildEvent {
102//!     Started { id: u64 },
103//!     Progress { done: u64, total: u64 },
104//!     #[sse(name = "finished")]
105//!     Completed,
106//! }
107//!
108//! // ----- Handlers ----------------------------------------------------------
109//! /// Create a widget. The path uses the `#[post]` shortcut, takes a
110//! /// typed JSON body and a typed header, and returns an
111//! /// `ApiResult<Json<T>, E>` so successes and the full error
112//! /// vocabulary both flow into the OpenAPI document.
113//! #[post("/widgets", tag = "Widgets")]
114//! async fn create_widget(
115//!     Header(_key, ..): Header<XApiKey>,
116//!     Json(req): Json<CreateWidget>,
117//! ) -> ApiResult<(axum::http::StatusCode, Json<Widget>), WidgetError> {
118//!     if req.name.is_empty() {
119//!         return Err(WidgetError::Validation("name is required".into()));
120//!     }
121//!     Ok((
122//!         axum::http::StatusCode::CREATED,
123//!         Json(Widget { id: 42, name: req.name }),
124//!     ))
125//! }
126//!
127//! /// Stream build progress as Server-Sent Events. The macro
128//! /// recognizes `SseStream<E, _>` and emits a `text/event-stream`
129//! /// response with one `oneOf` branch per `SseEvent` variant.
130//! #[get("/builds/{id}/events", tag = "Builds")]
131//! async fn stream_build(
132//! ) -> SseStream<BuildEvent, impl Stream<Item = Result<BuildEvent, Infallible>>> {
133//!     let events = futures::stream::iter(vec![
134//!         Ok(BuildEvent::Started { id: 1 }),
135//!         Ok(BuildEvent::Progress { done: 1, total: 10 }),
136//!         Ok(BuildEvent::Completed),
137//!     ]);
138//!     SseStream::new(events)
139//! }
140//!
141//! // ----- Compose, finalize, mount -----------------------------------------
142//! # async fn run() {
143//! let (router, openapi) = OpenApiRouter::<()>::new()
144//!     .routes(routes!(create_widget))
145//!     .routes(routes!(stream_build))
146//!     .split_for_parts();
147//!
148//! let api_doc = ApiDocBuilder::new()
149//!     .title("Widgets API")
150//!     .version("1.0.0")
151//!     .description("Tour service")
152//!     .bearer_security("bearerAuth")
153//!     .tag_group("Core", ["Widgets"])
154//!     .tag_group("Streaming", ["Builds"])
155//!     // Use OpenAPI 3.2 `itemSchema` for SSE responses (the default).
156//!     .sse_openapi_version(SseSpecVersion::V3_2)
157//!     .merge(openapi)
158//!     .build();
159//!
160//! // Customize the Scalar UI: classic single-column layout with a
161//! // light theme, dark mode off. `MountOpts::default()` keeps the
162//! // historical three-pane modern dark-mode appearance.
163//! let app = router.mount_docs(
164//!     api_doc,
165//!     MountOpts::default()
166//!         .scalar(
167//!             ScalarConfig::default()
168//!                 .layout(ScalarLayout::Classic)
169//!                 .theme(ScalarTheme::Solarized)
170//!                 .dark_mode(false),
171//!         ),
172//! );
173//! # // The body envelope `ApiErrorBody` is the shape every error
174//! # // response carries; reference it here so the import is exercised.
175//! # let _: ApiErrorBody<()> = ApiErrorBody::new(500, "internal", "boom", ());
176//! # let _ = app;
177//! # }
178//! ```
179//!
180//! The crate's public surface contains **no project-specific types** —
181//! everything is generic over `utoipa`'s native types so it can be lifted
182//! into any axum project.
183
184#![deny(missing_docs)]
185#![deny(rustdoc::broken_intra_doc_links)]
186
187mod builder;
188mod contribution;
189mod doc_params;
190mod doc_responses;
191mod doc_traits;
192mod extractor;
193mod handler_ops;
194mod headers;
195mod inner_schema;
196mod mount;
197mod private_dispatch;
198mod problem;
199mod router_ext;
200mod routes_macro;
201mod sse;
202mod ui;
203
204pub use builder::{ApiDoc, ApiDocBuilder, BuildError, SseSpecVersion};
205pub use contribution::{
206    apply_badge_to_operation, apply_contribution, record_required_permission, BadgeContribution,
207    DocumentedLayer, LayerContribution, ResponseContribution, SecurityContribution,
208};
209pub use doc_params::DocHeaderEntry;
210pub use doc_responses::DocResponseBody;
211pub use doc_traits::{
212    DocHeaderParams, DocOperationSecurity, DocPathParams, DocPathScalar, DocQueryParams,
213    DocRequestBody, PathScalar,
214};
215pub use extractor::Header;
216pub use handler_ops::{operation_for_method_mut, ApidocHandlerOps};
217pub use headers::{DocumentedHeader, HeaderParam};
218pub use inner_schema::{ApidocHandlerSchemas, InnerToSchema};
219pub use mount::{mount_docs, MountDocsExt, MountOpts};
220pub use problem::{ApiErrorBody, ProblemDetails};
221pub use router_ext::OpenApiRouterExt;
222pub use sse::{SseEventMeta, SseStream};
223
224#[cfg(feature = "docs-scalar")]
225pub use ui::{DeveloperTools, DocumentDownload, ScalarConfig, ScalarLayout, ScalarTheme};
226
227// Re-export the companion proc-macro crate so consumers only need
228// `doxa` on their dependency list to write an event-stream
229// handler or derive `ApiError`. Gated on the `macros` feature
230// (enabled by default) so the proc-macro compile cost can be opted
231// out of when the derives aren't needed.
232#[cfg(feature = "macros")]
233pub use doxa_macros::{capability, delete, get, operation, patch, post, put, ApiError, SseEvent};
234
235// Re-export the underlying utoipa types so consumers depend on a single
236// crate. Each re-export is explicit (no glob) so the public surface is
237// auditable from one place.
238pub use utoipa::openapi::OpenApi;
239pub use utoipa::{IntoParams, IntoResponses, ToSchema};
240pub use utoipa_axum::router::OpenApiRouter;
241
242// Our `routes!` macro wraps `utoipa_axum::routes!` and extends the
243// collected schemas with those referenced by handler-argument types
244// via [`ApidocHandlerSchemas`]. See [`routes_macro`] for the
245// implementation.
246
247/// Convenience alias for handler return types whose error half implements
248/// [`IntoResponses`]. Equivalent to [`Result<T, E>`] but signals intent.
249pub type ApiResult<T, E> = Result<T, E>;
250
251/// Re-exports used exclusively by the `doxa-macros` proc-macro
252/// crate. Not part of the public API — paths inside this module may
253/// change between minor versions. The macros reference items here so
254/// consumer crates do not need to depend on `tracing` directly just to
255/// use `#[derive(ApiError)]`.
256#[doc(hidden)]
257pub mod __private {
258    pub use tracing;
259
260    /// Audit outcome attached to response extensions by
261    /// `#[derive(ApiError)]`'s generated `IntoResponse` impl.
262    ///
263    /// Lives here so the macro can reference it without depending on
264    /// `doxa-audit`. The `AuditLayer` reads this from response
265    /// extensions and maps it to `doxa_audit::Outcome`.
266    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
267    pub enum ResponseAuditOutcome {
268        Allowed,
269        Denied,
270        Error,
271    }
272
273    /// Trait for error types that declare their audit outcome per variant.
274    ///
275    /// `#[derive(ApiError)]` generates this impl automatically. Each
276    /// variant's outcome is declared via `#[api(outcome = "denied")]` —
277    /// when omitted, the variant defaults to [`ResponseAuditOutcome::Error`].
278    pub trait HasAuditOutcome {
279        fn audit_outcome(&self) -> ResponseAuditOutcome;
280    }
281
282    // Autoref-specialized dispatch scaffolding referenced by the
283    // method macro's generated per-handler `IntoParams` impls. Not
284    // part of the public API.
285    pub use crate::private_dispatch::{
286        BareSchemaContribution, BareSchemaImplementedAdhoc, BareSchemaMissingAdhoc,
287        GenericArgSchemaContribution, GenericArgSchemaImplementedAdhoc,
288        GenericArgSchemaMissingAdhoc, HeaderParamContribution, HeaderParamsImplementedAdhoc,
289        HeaderParamsMissingAdhoc, InnerSchemaContribution, InnerSchemaImplementedAdhoc,
290        InnerSchemaMissingAdhoc, OpSecurityContribution, OpSecurityImplementedAdhoc,
291        OpSecurityMissingAdhoc, PathParamContribution, PathParamsImplementedAdhoc,
292        PathParamsMissingAdhoc, PathScalarContribution, PathScalarImplementedAdhoc,
293        PathScalarMissingAdhoc, QueryParamContribution, QueryParamsImplementedAdhoc,
294        QueryParamsMissingAdhoc, ResponseBodyContribution, ResponseBodyImplementedAdhoc,
295        ResponseBodyMissingAdhoc,
296    };
297
298    // Re-export paste so our `routes!` macro can concatenate idents
299    // (`__path_<fn>`) when the caller invokes it.
300    pub use paste;
301
302    // Re-export the upstream macro under a shadowed name so
303    // `doxa::routes!` can call into it without requiring the
304    // caller crate to declare `utoipa-axum` as a direct dependency.
305    pub use utoipa_axum::routes as utoipa_axum_routes;
306}