1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
//! Procedural macros for [`doxa`](../doxa/index.html).
//!
//! # Derive macros
//!
//! - [`macro@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.
//! - [`macro@SseEvent`] — implements
//! [`SseEventMeta`](../doxa/trait.SseEventMeta.html) for a tagged enum
//! so [`SseStream`](../doxa/struct.SseStream.html) names each SSE frame
//! after the variant carrying it. Override names with `#[sse(name = "…")]`.
//!
//! # HTTP method attribute macros
//!
//! [`macro@get`], [`macro@post`], [`macro@put`], [`macro@patch`],
//! [`macro@delete`] delegate to [`utoipa::path`] with automatic inference
//! from the handler signature. Use [`macro@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 parameters** — `Query<T>` extractors (including wrapped)
//! contribute query parameters via trait dispatch.
//! - **Header parameters** — `Header<H>` extractors contribute header
//! parameters. The `headers(H1, H2)` attribute documents headers
//! without extracting them; both forms deduplicate.
//! - **Success response** — `Json<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.
//! - **Tags** — `tag = "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
//!
//! [`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`.
//!
//! ```no_run
//! 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()))
//! }
//!
//! # async fn run() {
//! 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());
//! # let _ = app;
//! # }
//! ```
//!
//! ## 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`](../doxa/trait.DocumentedHeader.html)
//! trait, which exposes the wire name as a runtime fn so the same
//! marker can be reused on the layer side via
//! [`HeaderParam::typed`](../doxa/struct.HeaderParam.html#method.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.
use TokenStream;
/// Derive [`axum::response::IntoResponse`] and [`utoipa::IntoResponses`]
/// for an error enum from a single per-variant declaration.
///
/// Each variant is annotated with `#[api_error(status = N, code =
/// "string")]` where:
///
/// - `status` — the HTTP status code as a `u16` literal
/// - `code` — an application-level error code string written into the `code`
/// field of the
/// [`doxa::ApiErrorBody`](../doxa/struct.ApiErrorBody.html)
/// response body emitted by the generated `IntoResponse` impl
///
/// Multiple variants may share the same status code. The derive groups
/// them at expand time so the OpenAPI spec emits one `Response` per
/// status with each variant contributing a named example.
///
/// # Example
///
/// ```no_run
/// use doxa::{ApiError, ToSchema};
/// use serde::Serialize;
///
/// #[derive(Debug, thiserror::Error, Serialize, ToSchema, ApiError)]
/// pub enum MyError {
/// #[error("validation failed: {0}")]
/// #[api(status = 400, code = "validation_error")]
/// Validation(String),
///
/// #[error("query failed: {0}")]
/// #[api(status = 400, code = "query_error")]
/// Query(String),
///
/// #[error("not found: {0}")]
/// #[api(status = 404, code = "not_found")]
/// NotFound(String),
///
/// #[error("internal error")]
/// #[api(status = 500, code = "internal")]
/// Internal,
/// }
/// ```
///
/// The generated `IntoResponse` impl maps each variant to its declared
/// status and emits an `ApiErrorBody` envelope with the variant's
/// `code` and the variant's `Display` output as the `message`. The
/// `IntoResponses` impl groups `Validation` and `Query` under one
/// `400` response with two examples.
/// Derive [`SseEventMeta`](../doxa/trait.SseEventMeta.html) for an
/// enum whose variants represent the events of a Server-Sent Event
/// stream.
///
/// Pair with upstream `serde::Serialize` and `utoipa::ToSchema` derives
/// plus `#[serde(tag = "event", content = "data", rename_all =
/// "snake_case")]` so the wire format and the OpenAPI schema stay
/// aligned. Each variant's event name defaults to its snake-case form;
/// override with `#[sse(name = "…")]`.
///
/// ```no_run
/// use doxa::SseEvent;
///
/// #[derive(serde::Serialize, utoipa::ToSchema, SseEvent)]
/// #[serde(tag = "event", content = "data", rename_all = "snake_case")]
/// enum MigrationEvent {
/// Started { pipeline: String },
/// Progress { done: u64, total: u64 },
/// #[sse(name = "finished")]
/// Completed,
/// Heartbeat,
/// }
/// ```
///
/// The derive does not implement `Serialize` or `ToSchema` itself —
/// that keeps serde's renaming rules authoritative and avoids
/// duplicating them in this crate.
/// Shortcut for `#[utoipa::path(get, path = "...")]`.
///
/// Auto-fills `operation_id` from the function name when omitted. The
/// path string lives in exactly one place.
///
/// Supports `tag = "..."` for a single tag or `tags("A", "B")` for
/// multiple tags. Tags control how operations are grouped in
/// documentation UIs (Scalar, Swagger UI, Redoc) and code generators.
///
/// Additional `key = value` pairs are forwarded to `utoipa::path`
/// verbatim, so any feature accepted by the upstream macro (request
/// body, responses, security, params) works without modification.
///
/// # Tags
///
/// ```no_run
/// use doxa::get;
///
/// // Single tag (forwarded to utoipa as-is):
/// #[get("/api/v1/models", tag = "Models")]
/// async fn list_models() -> &'static str { "[]" }
///
/// // Multiple tags (extracted and emitted as `tags = [...]`):
/// #[get("/api/v2/models", tags("Models", "Public API"))]
/// async fn list_models_public() -> &'static str { "[]" }
/// ```
/// `#[post("/path", ...)]` shortcut for [`utoipa::path`]. See
/// [`macro@get`] for the inference rules.
/// `#[put("/path", ...)]` shortcut for [`utoipa::path`]. See
/// [`macro@get`] for the inference rules.
/// `#[patch("/path", ...)]` shortcut for [`utoipa::path`]. See
/// [`macro@get`] for the inference rules.
/// `#[delete("/path", ...)]` shortcut for [`utoipa::path`]. See
/// [`macro@get`] for the inference rules.
/// Declare a `Capable` marker type backed by a `Capability` constant.
///
/// Generates the struct, a hidden `Capability` constant, and the
/// `Capable` impl so the marker can be used with
/// `doxa_auth::Require<M>` immediately. Requires `doxa-policy` in the
/// consumer's dependency tree.
///
/// # Attribute arguments
///
/// - `name = "scope.name"` — the stable client-facing capability identifier.
/// - `description = "Human-readable description"` — displayed in UI badges.
/// - `checks(action = "...", entity_type = "...", entity_id = "...")` — one or
/// more check blocks. All must pass for the capability to be granted.
///
/// # Example
///
/// ```no_run
/// use doxa::capability;
///
/// #[capability(
/// name = "widgets.read",
/// description = "Read widget definitions",
/// checks(action = "read", entity_type = "Widget", entity_id = "collection"),
/// )]
/// pub struct WidgetsRead;
/// ```
/// Generic operation attribute for cases where the HTTP method must be
/// specified explicitly (multi-method routes, non-standard verbs).
///
/// `#[operation(get, "/path", ...)]` is equivalent to
/// `#[get("/path", ...)]`. Prefer the method-specific shortcuts for
/// clarity.