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}