Skip to main content

siderite_core/
routing.rs

1//! Method routers and per-operation metadata.
2//!
3//! ```ignore
4//! App::new().route(
5//!     "/users",
6//!     post(create_user).status(201).summary("Create a user").tag("users")
7//!         .get(list_users).tag("users"),
8//! )
9//! ```
10//!
11//! Metadata methods (`summary`, `tag`, `status`, ...) apply to the **most
12//! recently added** method. Route macros (`#[post(...)]`) expand to exactly
13//! these calls.
14
15use crate::body::Body;
16use crate::extract::Request;
17use crate::handler::Handler;
18use crate::middleware::BoxService;
19use crate::response::{Response, SUCCESS};
20use http::{Method, StatusCode};
21use siderite_openapi::{HttpMethod, Operation, Schema, SchemaRegistry};
22use std::convert::Infallible;
23use std::future::Future;
24use std::pin::Pin;
25use std::sync::Arc;
26use tower::{Layer, Service, ServiceExt};
27
28type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
29
30/// Type-erased handler.
31pub(crate) type BoxedHandler = Arc<dyn Fn(Request) -> BoxFuture<Response> + Send + Sync>;
32
33/// Function documenting an operation.
34pub(crate) type DescribeFn = fn(&mut Operation, &mut SchemaRegistry);
35
36/// Static metadata attached to one operation.
37#[derive(Debug, Clone, Default)]
38pub struct OperationMeta {
39    /// One-line summary.
40    pub summary: Option<String>,
41    /// Longer description.
42    pub description: Option<String>,
43    /// Tags.
44    pub tags: Vec<String>,
45    /// Operation id.
46    pub operation_id: Option<String>,
47    /// Deprecated flag.
48    pub deprecated: bool,
49    /// Success status override (applies at runtime and in docs).
50    pub status: Option<StatusCode>,
51    /// Omit from the OpenAPI document.
52    pub hidden: bool,
53    pub(crate) response_model: Option<DescribeFn>,
54}
55
56impl OperationMeta {
57    /// Apply metadata on top of the signature-derived operation.
58    pub(crate) fn apply(&self, op: &mut Operation, registry: &mut SchemaRegistry) {
59        if let Some(model) = self.response_model {
60            model(op, registry);
61        }
62        if let Some(status) = self.status
63            && status.as_u16() != 200
64        {
65            op.remap_response(SUCCESS, status.as_str());
66        }
67        op.summary.clone_from(&self.summary);
68        op.description.clone_from(&self.description);
69        op.tags.clone_from(&self.tags);
70        op.operation_id.clone_from(&self.operation_id);
71        op.deprecated = self.deprecated;
72    }
73}
74
75/// One method on one path.
76#[derive(Clone)]
77pub(crate) struct Endpoint {
78    pub(crate) method: Method,
79    pub(crate) handler: BoxedHandler,
80    pub(crate) describe: DescribeFn,
81    pub(crate) meta: OperationMeta,
82    pub(crate) background_tasks: bool,
83}
84
85impl std::fmt::Debug for Endpoint {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("Endpoint")
88            .field("method", &self.method)
89            .field("meta", &self.meta)
90            .finish_non_exhaustive()
91    }
92}
93
94impl Endpoint {
95    fn new<H: Handler<T>, T: 'static>(method: Method, handler: H) -> Self {
96        let boxed: BoxedHandler = Arc::new(move |req| Box::pin(handler.clone().call(req)));
97        Self {
98            method,
99            handler: boxed,
100            describe: H::describe,
101            meta: OperationMeta::default(),
102            background_tasks: H::BACKGROUND_TASKS,
103        }
104    }
105
106    /// Build the operation document.
107    pub(crate) fn operation(&self, registry: &mut SchemaRegistry) -> Operation {
108        let mut op = Operation::default();
109        (self.describe)(&mut op, registry);
110        self.meta.apply(&mut op, registry);
111        op
112    }
113
114    /// OpenAPI method, if representable.
115    pub(crate) fn openapi_method(&self) -> Option<HttpMethod> {
116        Some(match self.method {
117            Method::GET => HttpMethod::Get,
118            Method::POST => HttpMethod::Post,
119            Method::PUT => HttpMethod::Put,
120            Method::PATCH => HttpMethod::Patch,
121            Method::DELETE => HttpMethod::Delete,
122            Method::HEAD => HttpMethod::Head,
123            Method::OPTIONS => HttpMethod::Options,
124            Method::TRACE => HttpMethod::Trace,
125            _ => return None,
126        })
127    }
128
129    /// Adapt to the internal router, applying the status override.
130    pub(crate) fn into_axum(self) -> Option<axum::routing::MethodRouter> {
131        use axum::routing::MethodFilter;
132        let filter = match self.method {
133            Method::GET => MethodFilter::GET,
134            Method::POST => MethodFilter::POST,
135            Method::PUT => MethodFilter::PUT,
136            Method::PATCH => MethodFilter::PATCH,
137            Method::DELETE => MethodFilter::DELETE,
138            Method::HEAD => MethodFilter::HEAD,
139            Method::OPTIONS => MethodFilter::OPTIONS,
140            Method::TRACE => MethodFilter::TRACE,
141            _ => return None,
142        };
143        let handler = self.handler;
144        let status = self.meta.status;
145        let background = self.background_tasks;
146        Some(axum::routing::on(
147            filter,
148            move |req: axum::extract::Request| {
149                let handler = Arc::clone(&handler);
150                async move {
151                    let mut req = req.map(Body::from_inner);
152                    let guard = background
153                        .then(|| crate::background::RequestGuard::install(req.extensions_mut()));
154                    let mut response = handler(req).await;
155                    if let Some(status) = status
156                        && response.status() == StatusCode::OK
157                    {
158                        *response.status_mut() = status;
159                    }
160                    if let Some(guard) = guard {
161                        guard.finish(response.status());
162                    }
163                    response.map(Body::into_inner)
164                }
165            },
166        ))
167    }
168}
169
170/// Maps HTTP methods to handlers for a single path.
171#[derive(Debug, Clone, Default)]
172pub struct MethodRouter {
173    pub(crate) endpoints: Vec<Endpoint>,
174}
175
176macro_rules! method_routes {
177    ($($name:ident => $method:ident),+ $(,)?) => {
178        $(
179            #[doc = concat!("Route `", stringify!($method), "` requests to `handler`.")]
180            pub fn $name<H: Handler<T>, T: 'static>(handler: H) -> MethodRouter {
181                MethodRouter::default().$name(handler)
182            }
183        )+
184
185        impl MethodRouter {
186            $(
187                #[doc = concat!("Additionally route `", stringify!($method), "` requests to `handler`.")]
188                #[must_use]
189                pub fn $name<H: Handler<T>, T: 'static>(self, handler: H) -> Self {
190                    self.on(Method::$method, handler)
191                }
192            )+
193        }
194    };
195}
196
197method_routes! {
198    get => GET,
199    post => POST,
200    put => PUT,
201    patch => PATCH,
202    delete => DELETE,
203    head => HEAD,
204    options => OPTIONS,
205}
206
207impl MethodRouter {
208    /// Route `method` to `handler`. Methods outside GET, POST, PUT, PATCH,
209    /// DELETE, HEAD, OPTIONS and TRACE are rejected when the app is built.
210    #[must_use]
211    pub fn on<H: Handler<T>, T: 'static>(mut self, method: Method, handler: H) -> Self {
212        self.endpoints.push(Endpoint::new(method, handler));
213        self
214    }
215
216    fn last(mut self, f: impl FnOnce(&mut OperationMeta)) -> Self {
217        if let Some(e) = self.endpoints.last_mut() {
218            f(&mut e.meta);
219        }
220        self
221    }
222
223    /// Wrap every handler added so far in `layer`.
224    ///
225    /// The layer runs after routing and only for this path, so it can be
226    /// used for per-route middleware such as `siderite_cache::RouteCache`.
227    /// Handlers added after this call are not wrapped.
228    #[must_use]
229    pub fn layer<L>(mut self, layer: L) -> Self
230    where
231        L: Layer<BoxService>,
232        L::Service: Service<Request, Response = Response, Error = Infallible>
233            + Clone
234            + Send
235            + Sync
236            + 'static,
237        <L::Service as Service<Request>>::Future: Send + 'static,
238    {
239        for endpoint in &mut self.endpoints {
240            let handler = Arc::clone(&endpoint.handler);
241            let inner = BoxService::new(tower::service_fn(move |req: Request| {
242                let fut = handler(req);
243                async move { Ok::<_, Infallible>(fut.await) }
244            }));
245            let service = layer.layer(inner);
246            endpoint.handler = Arc::new(move |req| {
247                let service = service.clone();
248                Box::pin(async move {
249                    match service.oneshot(req).await {
250                        Ok(response) => response,
251                        Err(never) => match never {},
252                    }
253                })
254            });
255        }
256        self
257    }
258
259    /// Set the summary.
260    #[must_use]
261    pub fn summary(self, summary: impl Into<String>) -> Self {
262        let summary = summary.into();
263        self.last(|m| m.summary = Some(summary))
264    }
265
266    /// Set the description.
267    #[must_use]
268    pub fn description(self, description: impl Into<String>) -> Self {
269        let description = description.into();
270        self.last(|m| m.description = Some(description))
271    }
272
273    /// Add a tag.
274    #[must_use]
275    pub fn tag(self, tag: impl Into<String>) -> Self {
276        let tag = tag.into();
277        self.last(|m| m.tags.push(tag))
278    }
279
280    /// Set the operation id.
281    #[must_use]
282    pub fn operation_id(self, id: impl Into<String>) -> Self {
283        let id = id.into();
284        self.last(|m| m.operation_id = Some(id))
285    }
286
287    /// Mark deprecated.
288    #[must_use]
289    pub fn deprecated(self) -> Self {
290        self.last(|m| m.deprecated = true)
291    }
292
293    /// Hide from the OpenAPI document.
294    #[must_use]
295    pub fn hidden(self) -> Self {
296        self.last(|m| m.hidden = true)
297    }
298
299    /// Success status: replaces a `200 OK` produced by the handler at
300    /// runtime, and documents the success response under this code.
301    #[must_use]
302    pub fn status(self, status: StatusCode) -> Self {
303        self.last(|m| m.status = Some(status))
304    }
305
306    /// Document the success response body as `T` (`application/json`),
307    /// independent of the handler's return type.
308    #[must_use]
309    pub fn response_model<T: Schema + 'static>(self) -> Self {
310        fn describe<T: Schema + 'static>(op: &mut Operation, registry: &mut SchemaRegistry) {
311            let schema = registry.subschema::<T>();
312            op.add_response(
313                SUCCESS,
314                "Successful Response",
315                Some(("application/json", schema)),
316            );
317        }
318        self.last(|m| m.response_model = Some(describe::<T>))
319    }
320}
321
322/// A path plus its method router; produced by route macros and `routes![]`.
323#[derive(Debug, Clone)]
324pub struct Route {
325    /// Path template.
326    pub path: &'static str,
327    /// Handlers.
328    pub router: MethodRouter,
329}
330
331impl Route {
332    /// Pair a path with a router.
333    pub fn new(path: &'static str, router: MethodRouter) -> Self {
334        Self { path, router }
335    }
336}