Skip to main content

autumn_web/
route.rs

1//! Route descriptor types used by macro-generated code.
2//!
3//! Each route macro ([`get`](crate::get), [`post`](crate::post), etc.)
4//! generates a companion function that returns a [`Route`]. The
5//! [`routes!`](crate::routes) macro collects these into a `Vec<Route>`
6//! for the [`AppBuilder`](crate::app::AppBuilder).
7//!
8//! Users do not construct `Route` values directly -- they use the
9//! proc macros and the `routes![]` collection macro.
10
11use std::time::Duration;
12
13use axum::routing::MethodRouter;
14use http::Method;
15
16use crate::openapi::ApiDoc;
17use crate::state::AppState;
18
19/// Metadata attached to routes emitted by the `#[repository(api = ...)]` macro.
20///
21/// Lets the app builder validate, at startup, that every auto-mounted CRUD
22/// endpoint is paired with a registered
23/// [`Policy`](crate::authorization::Policy).
24#[derive(Debug, Clone, Copy)]
25pub struct RepositoryApiMeta {
26    /// Stringified resource type name (e.g., `"Post"`). Used for
27    /// log messages and to look up the registered policy via
28    /// [`std::any::TypeId`] indirectly through the generated check
29    /// function in [`Self::policy_check`].
30    pub resource_type_name: &'static str,
31
32    /// Path prefix mounted by this repository (e.g., `"/api/posts"`).
33    pub api_path: &'static str,
34
35    /// `true` when the macro form used `policy = SomePolicy`, so the
36    /// auto-generated handlers enforce a record-level check before
37    /// running. `false` when the macro form is just
38    /// `#[repository(api = "...")]` — that form is rejected in
39    /// `prod` profile builds unless
40    /// `[security] allow_unauthorized_repository_api = true`.
41    pub has_policy: bool,
42
43    /// Type-erased registry probe emitted by the macro when
44    /// `policy = ...` is set. Returns `true` if a [`Policy`](crate::authorization::Policy) is
45    /// registered on the runtime
46    /// [`PolicyRegistry`](crate::authorization::PolicyRegistry) for
47    /// the resource type. Lets the app builder fail fast at
48    /// startup when a developer wires `policy = X` on the
49    /// `#[repository]` macro but forgets to call
50    /// `.policy::<R, _>(X)` on the builder — without this check,
51    /// every protected request would 500 with "no policy
52    /// registered" instead of failing fast at boot. `None` when
53    /// the macro form omits `policy = ...`.
54    pub policy_check: Option<fn(&crate::authorization::PolicyRegistry) -> bool>,
55
56    /// Type-erased registry probe emitted by the macro when
57    /// `scope = ...` is set. Returns `true` if a [`Scope`](crate::authorization::Scope) is
58    /// registered for the resource type. Companion to
59    /// [`Self::policy_check`] for the scope-list code path: the
60    /// generated `GET /<api>` handler resolves the scope from the
61    /// registry on every request, so a missing
62    /// `.scope::<R, _>(...)` registration would 500 every list
63    /// call. The startup guard fails fast instead. `None` when
64    /// the macro form omits `scope = ...`.
65    pub scope_check: Option<fn(&crate::authorization::PolicyRegistry) -> bool>,
66}
67
68/// Declares how the app-level idempotency layer should replay cached responses
69/// for this route.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum RouteIdempotency {
72    /// Unknown/manual routes have no guaranteed generated replay consumer.
73    /// Autumn stores the first successful mutation but fails closed on cache
74    /// hits instead of directly replaying a stale success around any
75    /// route-local authorization, tenant, audit, or similar layers.
76    #[default]
77    Direct,
78    /// Autumn-generated routes install a replay consumer inside the route
79    /// stack or generated guard body, allowing route-local middleware and
80    /// guards to run before the cached response is returned.
81    ///
82    /// Manual layered routes can use this too, but they must place
83    /// [`crate::idempotency::IdempotencyReplayLayer`] after those checks and
84    /// before the mutating handler.
85    ReplayThroughInner,
86}
87
88/// Per-route override for the global inbound request timeout
89/// (`[server.timeouts] request_timeout_ms`).
90///
91/// Emitted by the route macros from the `timeout_ms = ...` / `timeout = "off"`
92/// attributes and consulted by the timeout middleware (keyed by the matched
93/// route template). The default, [`RouteTimeout::Inherit`], applies the global
94/// deadline.
95///
96/// WebSocket routes also default to [`RouteTimeout::Inherit`]: the deadline
97/// bounds a hung pre-upgrade handshake (async auth/setup) but never reaches the
98/// established socket, whose future runs on a separate task via `on_upgrade`
99/// and is unbounded by design. SSE and other streaming responses need no
100/// override either — they are exempt *by construction*, because the deadline
101/// only bounds production of the response head, never the body stream. The
102/// [`RouteTimeout::Disabled`] variant is reached solely via `timeout = "off"`,
103/// for routes that intentionally block *before* producing the head (long-poll).
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum RouteTimeout {
106    /// Use the global `request_timeout_ms` deadline (or none if disabled).
107    #[default]
108    Inherit,
109    /// Override the global deadline with a route-specific wall-clock budget,
110    /// for known-slow endpoints (report exports, large uploads).
111    Override(Duration),
112    /// Exempt this route from the global deadline entirely. Emitted by
113    /// `timeout = "off"` for routes that intentionally block before producing
114    /// the response head, such as long-poll handlers. (SSE/streaming bodies are
115    /// already exempt by construction, and WebSocket handshakes inherit the
116    /// deadline — neither uses this variant by default.)
117    Disabled,
118}
119
120/// A single route binding an HTTP method + path to an Axum handler.
121///
122/// Created by the `__autumn_route_info_{name}()` companion functions
123/// that route macros ([`get`](crate::get), [`post`](crate::post), etc.)
124/// generate. Users don't construct this directly -- they use the
125/// attribute macros and the [`routes!`](crate::routes) macro.
126///
127/// # Examples
128///
129/// ```rust,no_run
130/// use autumn_web::prelude::*;
131///
132/// #[get("/hello")]
133/// async fn hello() -> &'static str { "hi" }
134///
135/// // `routes!` expands to a Vec<Route>:
136/// let route_vec: Vec<autumn_web::Route> = routes![hello];
137/// assert_eq!(route_vec.len(), 1);
138/// ```
139pub struct Route {
140    /// HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.).
141    pub method: Method,
142
143    /// URL path pattern (e.g., `"/users/{id}"`).
144    pub path: &'static str,
145
146    /// Axum [`MethodRouter`] that handles requests matching this route.
147    pub handler: MethodRouter<AppState>,
148
149    /// Handler function name, used for startup logging
150    /// (e.g., `"hello"`, `"create_item"`).
151    pub name: &'static str,
152
153    /// `OpenAPI` metadata inferred from the handler's signature and any
154    /// [`#[api_doc(...)]`](crate::api_doc) overrides. Consumed by
155    /// `AppBuilder::openapi` when
156    /// generating `/v3/api-docs`.
157    pub api_doc: ApiDoc,
158
159    /// API version of the route (e.g. "v1")
160    pub api_version: Option<&'static str>,
161
162    /// Whether this route opts out of sunset 410 response
163    pub sunset_opt_out: bool,
164
165    /// Repository auto-API metadata, populated by the
166    /// `#[repository(api = ...)]` macro. `None` for hand-written
167    /// route handlers.
168    pub repository: Option<RepositoryApiMeta>,
169
170    /// Idempotency replay behavior for this route.
171    pub idempotency: RouteIdempotency,
172
173    /// Per-route override for the global inbound request timeout.
174    pub timeout: RouteTimeout,
175}
176
177impl Route {
178    /// Opt this route in as an MCP tool, equivalent to tagging the handler
179    /// with `#[api_doc(mcp)]`.
180    ///
181    /// This is the registration-time escape hatch for code that can't (or
182    /// shouldn't) carry the attribute — most notably plugins, which can offer
183    /// a fluent `expose_mcp()` switch and let the *host* decide at install
184    /// time whether the plugin's routes become tools:
185    ///
186    /// ```rust,no_run
187    /// use autumn_web::Route;
188    /// use autumn_web::app::AppBuilder;
189    /// use autumn_web::plugin::Plugin;
190    /// use autumn_web::prelude::*;
191    ///
192    /// # #[get("/harvest/runs")]
193    /// # async fn list_runs() -> Json<Vec<String>> { Json(vec![]) }
194    /// pub struct HarvestPlugin {
195    ///     expose_mcp: bool,
196    /// }
197    ///
198    /// impl Plugin for HarvestPlugin {
199    ///     fn build(self, app: AppBuilder) -> AppBuilder {
200    ///         let mut rs = routes![list_runs];
201    ///         if self.expose_mcp {
202    ///             rs = rs.into_iter().map(Route::mcp).collect();
203    ///         }
204    ///         app.routes(rs)
205    ///     }
206    /// }
207    /// ```
208    ///
209    /// Like the attribute form, an explicit opt-in exposes any verb (not just
210    /// reads) and the usual eligibility rules still apply: the handler must
211    /// return `Json<T>`, declare an empty-body status (204/205), or use
212    /// [`mcp_stream()`](Self::mcp_stream) for an `Sse` handler — a schema-less
213    /// route opted in with plain `mcp()` derives no tool. The flag is inert
214    /// unless the host enables the `mcp` feature and calls `mount_mcp`.
215    #[must_use]
216    pub const fn mcp(mut self) -> Self {
217        self.api_doc.mcp_tool = true;
218        self
219    }
220
221    /// Exclude this route from MCP exposure, equivalent to
222    /// `#[api_doc(mcp = false)]`.
223    ///
224    /// Exclusion always wins — even over the whole-API
225    /// `expose_all_as_mcp()` hatch and a prior [`mcp()`](Self::mcp) call.
226    #[must_use]
227    pub const fn mcp_exclude(mut self) -> Self {
228        self.api_doc.mcp_exclude = true;
229        self
230    }
231
232    /// Opt this route in as a *streaming* MCP tool, equivalent to
233    /// `#[api_doc(mcp, stream)]` on an [`Sse`](crate::sse::Sse) handler.
234    ///
235    /// Implies [`mcp()`](Self::mcp) — `stream` alone is never exposed — and
236    /// exempts the route from the JSON-response eligibility gate, since an
237    /// SSE handler has no JSON response schema by nature.
238    #[must_use]
239    pub const fn mcp_stream(mut self) -> Self {
240        self.api_doc.mcp_tool = true;
241        self.api_doc.mcp_stream = true;
242        self
243    }
244}