doxa-macros 0.1.2

Procedural macros for doxa: #[derive(ApiError)], #[derive(SseEvent)], and HTTP method shortcuts that delegate to utoipa::path.
Documentation

doxa-macros

Procedural macros for the doxa ecosystem. Derive macros, HTTP method attributes, and a capability declaration macro — all designed to eliminate boilerplate when building OpenAPI-documented axum services.

Most users should depend on doxa with the macros feature (enabled by default) rather than pulling this crate directly.

Derive macros

#[derive(ApiError)]

Turns an error enum into three trait implementations from a single #[api(...)] annotation per variant:

  • axum::response::IntoResponse — maps each variant to its HTTP status code and emits a typed ApiErrorBody<Self> JSON envelope with message, status, code, and error fields.
  • utoipa::IntoResponses — produces an OpenAPI response map. Variants sharing a status code are grouped into one response with per-variant named examples and a oneOf schema for the error field.
  • HasAuditOutcome — maps each variant to an audit outcome (allowed, denied, or error) for automatic audit trail integration via AuditLayer.

The generated IntoResponse also emits structured tracing: error! for 5xx, warn! for 4xx, debug! for everything else.

#[derive(Debug, thiserror::Error, Serialize, ToSchema, ApiError)]
enum WidgetError {
    #[error("not found")]
    #[api(status = 404, code = "not_found", outcome = "allowed")]
    NotFound,

    #[error("validation failed: {0}")]
    #[api(status = 400, code = "validation_error")]
    Validation(String),

    #[error("conflict: {0}")]
    #[api(status = 400, code = "conflict")]  // grouped with Validation under one 400 response
    Conflict(String),

    #[error("internal error")]
    #[api(status = 500, code = "internal")]   // outcome defaults to "error" when omitted
    Internal,
}

#[api(...)] attributes

Key Required Description
status yes HTTP status code (u16)
code no Application error code string; defaults to snake_case of variant name
outcome no Audit outcome: "allowed", "denied", or "error" (default)

#[derive(SseEvent)]

Implements SseEventMeta for internally-tagged enums so Server-Sent Event frames carry the variant name as the SSE event type. Pair with #[serde(tag = "event", content = "data", rename_all = "snake_case")] to keep wire format and OpenAPI schema aligned.

#[derive(Serialize, ToSchema, SseEvent)]
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
enum Progress {
    Started { job_id: u64 },
    Tick { percent: u8 },
    #[sse(name = "finished")]  // override the default snake_case name
    Completed { job_id: u64 },
}

HTTP method attribute macros

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

#[get("/widgets/{id}", tag = "Widgets")]
async fn get_widget(Path(id): Path<u32>) -> Result<Json<Widget>, WidgetError> {
    // ...
}

#[post("/widgets", tags("Widgets", "Public"))]
async fn create_widget(
    Json(req): Json<CreateWidget>,
) -> Result<(StatusCode, Json<Widget>), WidgetError> {
    // ...
}

What the macros infer

The method macros read the handler signature and automatically populate utoipa::path attributes:

Inference How it works
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 are matched to Path<T> extractors (scalar, tuple, struct)
Query parameters Query<T> extractors contribute query parameters via trait dispatch
Header parameters Header<H> extractors contribute header parameters; headers(H1, H2) documents headers without extracting
Success response Json<T> → 200; (StatusCode, Json<T>) → 201; SseStream<E, _>text/event-stream
Error responses E from Result<_, E> folded into responses(...) as IntoResponses
Tags tag = "Name" for one, tags("A", "B") for multiple

Explicit overrides always win. Supplying request_body = ..., params(...), or responses(...) by hand suppresses inference for that field. Any additional key = value pairs are forwarded to utoipa::path verbatim.

Header documentation

Two equivalent ways to declare a header on a handler — both use the DocumentedHeader trait and deduplicate:

// Via extractor — extracts the value AND documents it
#[get("/widgets")]
async fn list(Header(key, ..): Header<XApiKey>) -> Json<Vec<Widget>> { /* ... */ }

// Via attribute — documents without extracting
#[get("/health", headers(XApiKey))]
async fn health() -> &'static str { "ok" }

Capability attribute macro

#[capability] declares a Capable marker type backed by a Capability constant, for use with doxa_auth::Require<M>. Requires doxa-policy in the consumer's dependency tree.

use doxa_macros::capability;

#[capability(
    name = "widgets.read",
    description = "Read widget definitions",
    checks(action = "read", entity_type = "Widget", entity_id = "collection"),
)]
pub struct WidgetsRead;

// Use in a handler — enforces at runtime AND stamps OpenAPI security metadata
#[get("/widgets")]
async fn list_widgets(_: Require<WidgetsRead>) -> Json<Vec<Widget>> {
    Json(load().await)
}

Multiple checks(...) blocks are supported — all must pass for the capability to be granted.

#[capability] attributes

Key Required Description
name yes Stable client-facing capability identifier (e.g. "widgets.read")
description yes Human-readable description, displayed in UI badges
checks(...) yes (1+) One or more check blocks with action, entity_type, entity_id

License

Apache 2.0