Skip to main content

Crate doxa_macros

Crate doxa_macros 

Source
Expand description

Procedural macros for doxa.

§Derive macros

  • ApiError — wires an error enum into both axum::response::IntoResponse and utoipa::IntoResponses from a single per-variant #[api(...)] declaration. Multiple variants sharing a status code are grouped into one OpenAPI response with distinct examples. An optional outcome attribute integrates with the audit trail.
  • SseEvent — implements SseEventMeta for a tagged enum so SseStream names each SSE frame after the variant carrying it. Override names with #[sse(name = "…")].

§HTTP method attribute macros

get, post, put, patch, delete delegate to utoipa::path with automatic inference from the handler signature. Use operation for custom or multi-method routes.

§What the method macros infer

  • operation_id — defaults to the function name.
  • request_body — detected from the first Json<T> parameter, including through transparent wrappers like Valid<Json<T>>.
  • Path parameters{name} segments in the route template are matched to Path<T> extractors (scalar, tuple, and struct forms).
  • Query parametersQuery<T> extractors (including wrapped) contribute query parameters via trait dispatch.
  • Header parametersHeader<H> extractors contribute header parameters. The headers(H1, H2) attribute documents headers without extracting them; both forms deduplicate.
  • Success responseJson<T> → 200; (StatusCode, Json<T>) → 201; SseStream<E, _>text/event-stream with per-variant event names.
  • Error responses — the E from Result<_, E> is folded into responses(...) as an IntoResponses reference.
  • Tagstag = "Name" for a single tag, tags("A", "B") for multiple. Tags control grouping in documentation UIs.

Explicit overrides always win: if you supply request_body = ..., params(...), or responses(...) by hand, inference for that field is suppressed.

§Capability attribute macro

capability declares a Capable marker type backed by a Capability constant for use with doxa_auth::Require<M>.

§Usage

Consumers should depend on doxa (with the default macros feature) and import these macros via doxa::{get, post, ApiError, SseEvent, …} rather than depending on this crate directly.

§Tour

Every macro the crate exports, exercised end-to-end. Compiles under cargo test --doc.

use axum::Json;
use doxa::{
    routes, ApiDocBuilder, ApiResult, DocumentedHeader, Header,
    MountDocsExt, MountOpts, OpenApiRouter, SseEventMeta, SseStream, ToSchema,
};
use doxa::{get, post, ApiError, SseEvent};
use futures_core::Stream;
use serde::{Deserialize, Serialize};
use std::convert::Infallible;

// -- ApiError: multi-variant-per-status grouping --------------------------
#[derive(Debug, thiserror::Error, Serialize, ToSchema, ApiError)]
enum WidgetError {
    #[error("validation failed: {0}")]
    #[api(status = 400, code = "validation_error")]
    Validation(String),

    // Second variant at the same status — the OpenAPI spec emits one
    // 400 response with two named examples.
    #[error("conflict: {0}")]
    #[api(status = 400, code = "conflict")]
    Conflict(String),

    #[error("not found")]
    #[api(status = 404, code = "not_found")]
    NotFound,
}

// -- SseEvent: variant-tagged event stream --------------------------------
#[derive(Serialize, ToSchema, SseEvent)]
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
enum BuildEvent {
    Started { id: u64 },
    Progress { done: u64, total: u64 },
    // Override the default snake-case event name.
    #[sse(name = "finished")]
    Completed,
}

// -- DocumentedHeader: typed header on the handler signature --------------
struct XApiKey;
impl DocumentedHeader for XApiKey {
    fn name() -> &'static str { "X-Api-Key" }
    fn description() -> &'static str { "Tenant API key" }
}

// -- Method shortcuts: tags, request body, headers, Result return --------
#[derive(Debug, Serialize, ToSchema)]
struct Widget { id: u32, name: String }

#[derive(Debug, Deserialize, ToSchema)]
struct CreateWidget { name: String }

/// Single tag — forwarded to utoipa as `tag = "Widgets"`.
#[get("/widgets", tag = "Widgets")]
async fn list_widgets(
    Header(_key, ..): Header<XApiKey>,
) -> ApiResult<Json<Vec<Widget>>, WidgetError> {
    Ok(Json(vec![]))
}

/// Multiple tags — emitted as `tags = ["Widgets", "Public"]`.
/// Inferred request body (`Json<CreateWidget>`), inferred 201
/// success from `(StatusCode, Json<T>)`, error responses folded
/// in from the `Err` half of the return.
#[post("/widgets", tags("Widgets", "Public"))]
async fn create_widget(
    Json(req): Json<CreateWidget>,
) -> ApiResult<(axum::http::StatusCode, Json<Widget>), WidgetError> {
    Ok((
        axum::http::StatusCode::CREATED,
        Json(Widget { id: 1, name: req.name }),
    ))
}

/// Document a header without extracting its value — the marker is
/// listed under `headers(...)` and dedupes against any concurrent
/// `Header<H>` extractor on the same handler.
#[get("/health", headers(XApiKey))]
async fn health() -> &'static str { "ok" }

/// SseStream<E, _> return is recognized by the macro and emitted as
/// a `text/event-stream` response with one `oneOf` branch per
/// `SseEvent` variant.
#[get("/builds/{id}/events", tag = "Builds")]
async fn stream_build(
) -> SseStream<BuildEvent, impl Stream<Item = Result<BuildEvent, Infallible>>> {
    SseStream::new(futures::stream::iter(Vec::new()))
}

let (router, openapi) = OpenApiRouter::<()>::new()
    .routes(routes!(list_widgets, create_widget, health))
    .routes(routes!(stream_build))
    .split_for_parts();

let api_doc = ApiDocBuilder::new()
    .title("Tour")
    .version("1.0.0")
    .merge(openapi)
    .build();

let app = router.mount_docs(api_doc, MountOpts::default());

§Header form equivalence

The shortcut macros recognize two ways to declare a header on a handler — the Header<H> extractor in the signature and the headers(H, …) attribute. Both rely on the DocumentedHeader trait, which exposes the wire name as a runtime fn so the same marker can be reused on the layer side via HeaderParam::typed. Both forms are interchangeable and dedupe against each other if the same marker appears in both, so listing a header in headers(...) while also extracting it never produces two spec entries.

See the doxa crate-level docs for the broader design.

Attribute Macros§

capability
Declare a Capable marker type backed by a Capability constant.
delete
#[delete("/path", ...)] shortcut for utoipa::path. See get for the inference rules.
get
Shortcut for #[utoipa::path(get, path = "...")].
operation
Generic operation attribute for cases where the HTTP method must be specified explicitly (multi-method routes, non-standard verbs).
patch
#[patch("/path", ...)] shortcut for utoipa::path. See get for the inference rules.
post
#[post("/path", ...)] shortcut for utoipa::path. See get for the inference rules.
put
#[put("/path", ...)] shortcut for utoipa::path. See get for the inference rules.

Derive Macros§

ApiError
Derive axum::response::IntoResponse and utoipa::IntoResponses for an error enum from a single per-variant declaration.
SseEvent
Derive SseEventMeta for an enum whose variants represent the events of a Server-Sent Event stream.