Skip to main content

rustapi_core/
handler.rs

1//! Handler trait and utilities
2//!
3//! This module provides the [`Handler`] trait and related types for defining
4//! HTTP request handlers in RustAPI.
5//!
6//! # Handler Functions
7//!
8//! Any async function that takes extractors as parameters and returns a type
9//! implementing [`IntoResponse`] can be used as a handler:
10//!
11//! ```rust,ignore
12//! use rustapi_core::{Json, Path, IntoResponse};
13//! use serde::{Deserialize, Serialize};
14//!
15//! // No parameters
16//! async fn hello() -> &'static str {
17//!     "Hello, World!"
18//! }
19//!
20//! // With extractors
21//! async fn get_user(Path(id): Path<i64>) -> Json<User> {
22//!     Json(User { id, name: "Alice".to_string() })
23//! }
24//!
25//! // Multiple extractors (up to 5 supported)
26//! async fn create_user(
27//!     State(db): State<DbPool>,
28//!     Json(body): Json<CreateUser>,
29//! ) -> Result<Created<User>, ApiError> {
30//!     // ...
31//! }
32//! ```
33//!
34//! # Route Helpers
35//!
36//! The module provides helper functions for creating routes with metadata:
37//!
38//! ```rust,ignore
39//! use rustapi_core::handler::{get_route, post_route};
40//!
41//! let get = get_route("/users", list_users);
42//! let post = post_route("/users", create_user);
43//! ```
44//!
45//! # Macro-Based Routing
46//!
47//! For more ergonomic routing, use the `#[rustapi::get]`, `#[rustapi::post]`, etc.
48//! macros from `rustapi-macros`:
49//!
50//! ```rust,ignore
51//! #[rustapi::get("/users/{id}")]
52//! async fn get_user(Path(id): Path<i64>) -> Json<User> {
53//!     // ...
54//! }
55//! ```
56
57use crate::extract::FromRequest;
58use crate::request::Request;
59use crate::response::{IntoResponse, Response};
60use rustapi_openapi::{Operation, OperationModifier, ResponseModifier};
61use std::future::Future;
62use std::marker::PhantomData;
63use std::pin::Pin;
64
65mod sealed {
66    pub trait Sealed {}
67
68    impl<T> Sealed for T where T: Clone + Send + Sync + Sized + 'static {}
69}
70
71/// Trait representing an async handler function
72pub trait Handler<T>: sealed::Sealed + Clone + Send + Sync + Sized + 'static {
73    /// The response type
74    type Future: Future<Output = Response> + Send + 'static;
75
76    /// Call the handler with the request
77    fn call(self, req: Request) -> Self::Future;
78
79    /// Update the OpenAPI operation
80    fn update_operation(op: &mut Operation);
81
82    /// Register any OpenAPI components referenced by this handler.
83    fn register_components(spec: &mut rustapi_openapi::OpenApiSpec);
84}
85
86/// Wrapper to convert a Handler into a tower Service
87pub struct HandlerService<H, T> {
88    handler: H,
89    _marker: PhantomData<fn() -> T>,
90}
91
92impl<H, T> HandlerService<H, T> {
93    pub fn new(handler: H) -> Self {
94        Self {
95            handler,
96            _marker: PhantomData,
97        }
98    }
99}
100
101impl<H: Clone, T> Clone for HandlerService<H, T> {
102    fn clone(&self) -> Self {
103        Self {
104            handler: self.handler.clone(),
105            _marker: PhantomData,
106        }
107    }
108}
109
110// Implement Handler for async functions with 0-6 extractors
111
112// 0 args
113impl<F, Fut, Res> Handler<()> for F
114where
115    F: FnOnce() -> Fut + Clone + Send + Sync + 'static,
116    Fut: Future<Output = Res> + Send + 'static,
117    Res: IntoResponse + ResponseModifier,
118{
119    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
120
121    fn call(self, _req: Request) -> Self::Future {
122        Box::pin(async move { self().await.into_response() })
123    }
124
125    fn update_operation(op: &mut Operation) {
126        Res::update_response(op);
127    }
128
129    fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
130        Res::register_components(spec);
131    }
132}
133
134// 1 arg
135impl<F, Fut, Res, T1> Handler<(T1,)> for F
136where
137    F: FnOnce(T1) -> Fut + Clone + Send + Sync + 'static,
138    Fut: Future<Output = Res> + Send + 'static,
139    Res: IntoResponse + ResponseModifier,
140    T1: FromRequest + OperationModifier + Send + 'static,
141{
142    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
143
144    fn call(self, mut req: Request) -> Self::Future {
145        Box::pin(async move {
146            let t1 = match T1::from_request(&mut req).await {
147                Ok(v) => v,
148                Err(e) => return e.into_response(),
149            };
150            self(t1).await.into_response()
151        })
152    }
153
154    fn update_operation(op: &mut Operation) {
155        T1::update_operation(op);
156        Res::update_response(op);
157    }
158
159    fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
160        T1::register_components(spec);
161        Res::register_components(spec);
162    }
163}
164
165// 2 args
166impl<F, Fut, Res, T1, T2> Handler<(T1, T2)> for F
167where
168    F: FnOnce(T1, T2) -> Fut + Clone + Send + Sync + 'static,
169    Fut: Future<Output = Res> + Send + 'static,
170    Res: IntoResponse + ResponseModifier,
171    T1: FromRequest + OperationModifier + Send + 'static,
172    T2: FromRequest + OperationModifier + Send + 'static,
173{
174    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
175
176    fn call(self, mut req: Request) -> Self::Future {
177        Box::pin(async move {
178            let t1 = match T1::from_request(&mut req).await {
179                Ok(v) => v,
180                Err(e) => return e.into_response(),
181            };
182            let t2 = match T2::from_request(&mut req).await {
183                Ok(v) => v,
184                Err(e) => return e.into_response(),
185            };
186            self(t1, t2).await.into_response()
187        })
188    }
189
190    fn update_operation(op: &mut Operation) {
191        T1::update_operation(op);
192        T2::update_operation(op);
193        Res::update_response(op);
194    }
195
196    fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
197        T1::register_components(spec);
198        T2::register_components(spec);
199        Res::register_components(spec);
200    }
201}
202
203// 3 args
204impl<F, Fut, Res, T1, T2, T3> Handler<(T1, T2, T3)> for F
205where
206    F: FnOnce(T1, T2, T3) -> Fut + Clone + Send + Sync + 'static,
207    Fut: Future<Output = Res> + Send + 'static,
208    Res: IntoResponse + ResponseModifier,
209    T1: FromRequest + OperationModifier + Send + 'static,
210    T2: FromRequest + OperationModifier + Send + 'static,
211    T3: FromRequest + OperationModifier + Send + 'static,
212{
213    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
214
215    fn call(self, mut req: Request) -> Self::Future {
216        Box::pin(async move {
217            let t1 = match T1::from_request(&mut req).await {
218                Ok(v) => v,
219                Err(e) => return e.into_response(),
220            };
221            let t2 = match T2::from_request(&mut req).await {
222                Ok(v) => v,
223                Err(e) => return e.into_response(),
224            };
225            let t3 = match T3::from_request(&mut req).await {
226                Ok(v) => v,
227                Err(e) => return e.into_response(),
228            };
229            self(t1, t2, t3).await.into_response()
230        })
231    }
232
233    fn update_operation(op: &mut Operation) {
234        T1::update_operation(op);
235        T2::update_operation(op);
236        T3::update_operation(op);
237        Res::update_response(op);
238    }
239
240    fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
241        T1::register_components(spec);
242        T2::register_components(spec);
243        T3::register_components(spec);
244        Res::register_components(spec);
245    }
246}
247
248// 4 args
249impl<F, Fut, Res, T1, T2, T3, T4> Handler<(T1, T2, T3, T4)> for F
250where
251    F: FnOnce(T1, T2, T3, T4) -> Fut + Clone + Send + Sync + 'static,
252    Fut: Future<Output = Res> + Send + 'static,
253    Res: IntoResponse + ResponseModifier,
254    T1: FromRequest + OperationModifier + Send + 'static,
255    T2: FromRequest + OperationModifier + Send + 'static,
256    T3: FromRequest + OperationModifier + Send + 'static,
257    T4: FromRequest + OperationModifier + Send + 'static,
258{
259    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
260
261    fn call(self, mut req: Request) -> Self::Future {
262        Box::pin(async move {
263            let t1 = match T1::from_request(&mut req).await {
264                Ok(v) => v,
265                Err(e) => return e.into_response(),
266            };
267            let t2 = match T2::from_request(&mut req).await {
268                Ok(v) => v,
269                Err(e) => return e.into_response(),
270            };
271            let t3 = match T3::from_request(&mut req).await {
272                Ok(v) => v,
273                Err(e) => return e.into_response(),
274            };
275            let t4 = match T4::from_request(&mut req).await {
276                Ok(v) => v,
277                Err(e) => return e.into_response(),
278            };
279            self(t1, t2, t3, t4).await.into_response()
280        })
281    }
282
283    fn update_operation(op: &mut Operation) {
284        T1::update_operation(op);
285        T2::update_operation(op);
286        T3::update_operation(op);
287        T4::update_operation(op);
288        Res::update_response(op);
289    }
290
291    fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
292        T1::register_components(spec);
293        T2::register_components(spec);
294        T3::register_components(spec);
295        T4::register_components(spec);
296        Res::register_components(spec);
297    }
298}
299
300// 5 args
301impl<F, Fut, Res, T1, T2, T3, T4, T5> Handler<(T1, T2, T3, T4, T5)> for F
302where
303    F: FnOnce(T1, T2, T3, T4, T5) -> Fut + Clone + Send + Sync + 'static,
304    Fut: Future<Output = Res> + Send + 'static,
305    Res: IntoResponse + ResponseModifier,
306    T1: FromRequest + OperationModifier + Send + 'static,
307    T2: FromRequest + OperationModifier + Send + 'static,
308    T3: FromRequest + OperationModifier + Send + 'static,
309    T4: FromRequest + OperationModifier + Send + 'static,
310    T5: FromRequest + OperationModifier + Send + 'static,
311{
312    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
313
314    fn call(self, mut req: Request) -> Self::Future {
315        Box::pin(async move {
316            let t1 = match T1::from_request(&mut req).await {
317                Ok(v) => v,
318                Err(e) => return e.into_response(),
319            };
320            let t2 = match T2::from_request(&mut req).await {
321                Ok(v) => v,
322                Err(e) => return e.into_response(),
323            };
324            let t3 = match T3::from_request(&mut req).await {
325                Ok(v) => v,
326                Err(e) => return e.into_response(),
327            };
328            let t4 = match T4::from_request(&mut req).await {
329                Ok(v) => v,
330                Err(e) => return e.into_response(),
331            };
332            let t5 = match T5::from_request(&mut req).await {
333                Ok(v) => v,
334                Err(e) => return e.into_response(),
335            };
336            self(t1, t2, t3, t4, t5).await.into_response()
337        })
338    }
339
340    fn update_operation(op: &mut Operation) {
341        T1::update_operation(op);
342        T2::update_operation(op);
343        T3::update_operation(op);
344        T4::update_operation(op);
345        T5::update_operation(op);
346        Res::update_response(op);
347    }
348
349    fn register_components(spec: &mut rustapi_openapi::OpenApiSpec) {
350        T1::register_components(spec);
351        T2::register_components(spec);
352        T3::register_components(spec);
353        T4::register_components(spec);
354        T5::register_components(spec);
355        Res::register_components(spec);
356    }
357}
358
359// Type-erased handler for storage in router
360pub(crate) type BoxedHandler =
361    std::sync::Arc<dyn Fn(Request) -> Pin<Box<dyn Future<Output = Response> + Send>> + Send + Sync>;
362
363/// Create a boxed handler from any Handler
364pub(crate) fn into_boxed_handler<H, T>(handler: H) -> BoxedHandler
365where
366    H: Handler<T>,
367    T: 'static,
368{
369    std::sync::Arc::new(move |req| {
370        let handler = handler.clone();
371        Box::pin(async move { handler.call(req).await })
372    })
373}
374
375/// Trait for handlers with route metadata (generated by `#[rustapi::get]`, etc.)
376///
377/// This trait provides the path and method information for a handler,
378/// allowing `.mount(handler)` to automatically register the route.
379pub trait RouteHandler<T>: Handler<T> {
380    /// The path pattern for this route (e.g., "/users/{id}")
381    const PATH: &'static str;
382    /// The HTTP method for this route (e.g., "GET")
383    const METHOD: &'static str;
384}
385
386/// Represents a route definition that can be registered with .mount()
387pub struct Route {
388    pub(crate) path: &'static str,
389    pub(crate) method: &'static str,
390    pub(crate) handler: BoxedHandler,
391    pub(crate) operation: Operation,
392    pub(crate) component_registrar: fn(&mut rustapi_openapi::OpenApiSpec),
393    /// Custom parameter schemas for OpenAPI (param_name -> schema_type)
394    /// Supported types: "uuid", "integer", "string", "boolean", "number"
395    pub(crate) param_schemas: std::collections::BTreeMap<String, String>,
396    /// Custom error responses for OpenAPI (status_code -> description)
397    pub(crate) error_responses: Vec<(u16, String)>,
398}
399
400impl Route {
401    /// Create a new route from a handler with path and method
402    pub fn new<H, T>(path: &'static str, method: &'static str, handler: H) -> Self
403    where
404        H: Handler<T>,
405        T: 'static,
406    {
407        let mut operation = Operation::new();
408        H::update_operation(&mut operation);
409
410        Self {
411            path,
412            method,
413            handler: into_boxed_handler(handler),
414            operation,
415            component_registrar: <H as Handler<T>>::register_components,
416            param_schemas: std::collections::BTreeMap::new(),
417            error_responses: Vec::new(),
418        }
419    }
420    /// Set the operation summary
421    pub fn summary(mut self, summary: impl Into<String>) -> Self {
422        self.operation = self.operation.summary(summary);
423        self
424    }
425
426    /// Set the operation description
427    pub fn description(mut self, description: impl Into<String>) -> Self {
428        self.operation = self.operation.description(description);
429        self
430    }
431
432    /// Add a tag to the operation
433    pub fn tag(mut self, tag: impl Into<String>) -> Self {
434        self.operation.tags.push(tag.into());
435        self
436    }
437
438    /// Attach MCP metadata to this route (becomes `x-mcp` in OpenAPI).
439    /// This enables rich scoping like `skip`, `readonly`, `write`, `require = "confirm"`.
440    pub fn mcp(mut self, meta: rustapi_openapi::McpOperation) -> Self {
441        self.operation = self.operation.mcp(meta);
442        self
443    }
444
445    /// Get the route path
446    pub fn path(&self) -> &str {
447        self.path
448    }
449
450    /// Get the route method
451    pub fn method(&self) -> &str {
452        self.method
453    }
454
455    /// Set a custom OpenAPI schema type for a path parameter
456    ///
457    /// This is useful for overriding the auto-inferred type, e.g., when
458    /// a parameter named `id` is actually a UUID instead of an integer.
459    ///
460    /// # Supported schema types
461    /// - `"uuid"` - String with UUID format
462    /// - `"integer"` or `"int"` - Integer with int64 format
463    /// - `"string"` - Plain string
464    /// - `"boolean"` or `"bool"` - Boolean
465    /// - `"number"` - Number (float)
466    ///
467    /// # Example
468    ///
469    /// ```rust,ignore
470    /// #[rustapi::get("/users/{id}")]
471    /// async fn get_user(Path(id): Path<Uuid>) -> Json<User> {
472    ///     // ...
473    /// }
474    ///
475    /// // In route registration:
476    /// get_route("/users/{id}", get_user).param("id", "uuid")
477    /// ```
478    pub fn param(mut self, name: impl Into<String>, schema_type: impl Into<String>) -> Self {
479        self.param_schemas.insert(name.into(), schema_type.into());
480        self
481    }
482
483    /// Get the custom parameter schemas
484    pub fn param_schemas(&self) -> &std::collections::BTreeMap<String, String> {
485        &self.param_schemas
486    }
487
488    /// Add a typed error response to the OpenAPI specification for this route
489    ///
490    /// This adds an error response entry to the operation's responses map,
491    /// referencing the standard `ErrorSchema` component.
492    ///
493    /// # Arguments
494    ///
495    /// * `status` - HTTP status code (e.g., 404, 403, 409)
496    /// * `description` - Human-readable description of when this error occurs
497    ///
498    /// # Example
499    ///
500    /// ```rust,ignore
501    /// get_route("/users/{id}", get_user)
502    ///     .error_response(404, "User not found")
503    ///     .error_response(403, "Forbidden - insufficient permissions")
504    /// ```
505    pub fn error_response(mut self, status: u16, description: impl Into<String>) -> Self {
506        let desc = description.into();
507        self.error_responses.push((status, desc.clone()));
508
509        // Also add directly to the operation's responses
510        let mut content = std::collections::BTreeMap::new();
511        content.insert(
512            "application/json".to_string(),
513            rustapi_openapi::MediaType {
514                schema: Some(rustapi_openapi::SchemaRef::Ref {
515                    reference: "#/components/schemas/ErrorSchema".to_string(),
516                }),
517                example: None,
518            },
519        );
520
521        self.operation.responses.insert(
522            status.to_string(),
523            rustapi_openapi::ResponseSpec {
524                description: desc,
525                content,
526                headers: std::collections::BTreeMap::new(),
527            },
528        );
529
530        self
531    }
532
533    /// Get the custom error responses
534    pub fn error_responses(&self) -> &[(u16, String)] {
535        &self.error_responses
536    }
537}
538
539/// Helper macro to create a Route from a handler with RouteHandler trait
540#[macro_export]
541macro_rules! route {
542    ($handler:ident) => {{
543        $crate::Route::new($handler::PATH, $handler::METHOD, $handler)
544    }};
545}
546
547/// Create a GET route
548pub fn get_route<H, T>(path: &'static str, handler: H) -> Route
549where
550    H: Handler<T>,
551    T: 'static,
552{
553    Route::new(path, "GET", handler)
554}
555
556/// Create a POST route
557pub fn post_route<H, T>(path: &'static str, handler: H) -> Route
558where
559    H: Handler<T>,
560    T: 'static,
561{
562    Route::new(path, "POST", handler)
563}
564
565/// Create a PUT route
566pub fn put_route<H, T>(path: &'static str, handler: H) -> Route
567where
568    H: Handler<T>,
569    T: 'static,
570{
571    Route::new(path, "PUT", handler)
572}
573
574/// Create a PATCH route
575pub fn patch_route<H, T>(path: &'static str, handler: H) -> Route
576where
577    H: Handler<T>,
578    T: 'static,
579{
580    Route::new(path, "PATCH", handler)
581}
582
583/// Create a DELETE route
584pub fn delete_route<H, T>(path: &'static str, handler: H) -> Route
585where
586    H: Handler<T>,
587    T: 'static,
588{
589    Route::new(path, "DELETE", handler)
590}