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
83/// Wrapper to convert a Handler into a tower Service
84pub struct HandlerService<H, T> {
85    handler: H,
86    _marker: PhantomData<fn() -> T>,
87}
88
89impl<H, T> HandlerService<H, T> {
90    pub fn new(handler: H) -> Self {
91        Self {
92            handler,
93            _marker: PhantomData,
94        }
95    }
96}
97
98impl<H: Clone, T> Clone for HandlerService<H, T> {
99    fn clone(&self) -> Self {
100        Self {
101            handler: self.handler.clone(),
102            _marker: PhantomData,
103        }
104    }
105}
106
107// Implement Handler for async functions with 0-6 extractors
108
109// 0 args
110impl<F, Fut, Res> Handler<()> for F
111where
112    F: FnOnce() -> Fut + Clone + Send + Sync + 'static,
113    Fut: Future<Output = Res> + Send + 'static,
114    Res: IntoResponse + ResponseModifier,
115{
116    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
117
118    fn call(self, _req: Request) -> Self::Future {
119        Box::pin(async move { self().await.into_response() })
120    }
121
122    fn update_operation(op: &mut Operation) {
123        Res::update_response(op);
124    }
125}
126
127// 1 arg
128impl<F, Fut, Res, T1> Handler<(T1,)> for F
129where
130    F: FnOnce(T1) -> Fut + Clone + Send + Sync + 'static,
131    Fut: Future<Output = Res> + Send + 'static,
132    Res: IntoResponse + ResponseModifier,
133    T1: FromRequest + OperationModifier + Send + 'static,
134{
135    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
136
137    fn call(self, mut req: Request) -> Self::Future {
138        Box::pin(async move {
139            let t1 = match T1::from_request(&mut req).await {
140                Ok(v) => v,
141                Err(e) => return e.into_response(),
142            };
143            self(t1).await.into_response()
144        })
145    }
146
147    fn update_operation(op: &mut Operation) {
148        T1::update_operation(op);
149        Res::update_response(op);
150    }
151}
152
153// 2 args
154impl<F, Fut, Res, T1, T2> Handler<(T1, T2)> for F
155where
156    F: FnOnce(T1, T2) -> Fut + Clone + Send + Sync + 'static,
157    Fut: Future<Output = Res> + Send + 'static,
158    Res: IntoResponse + ResponseModifier,
159    T1: FromRequest + OperationModifier + Send + 'static,
160    T2: FromRequest + OperationModifier + Send + 'static,
161{
162    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
163
164    fn call(self, mut req: Request) -> Self::Future {
165        Box::pin(async move {
166            let t1 = match T1::from_request(&mut req).await {
167                Ok(v) => v,
168                Err(e) => return e.into_response(),
169            };
170            let t2 = match T2::from_request(&mut req).await {
171                Ok(v) => v,
172                Err(e) => return e.into_response(),
173            };
174            self(t1, t2).await.into_response()
175        })
176    }
177
178    fn update_operation(op: &mut Operation) {
179        T1::update_operation(op);
180        T2::update_operation(op);
181        Res::update_response(op);
182    }
183}
184
185// 3 args
186impl<F, Fut, Res, T1, T2, T3> Handler<(T1, T2, T3)> for F
187where
188    F: FnOnce(T1, T2, T3) -> Fut + Clone + Send + Sync + 'static,
189    Fut: Future<Output = Res> + Send + 'static,
190    Res: IntoResponse + ResponseModifier,
191    T1: FromRequest + OperationModifier + Send + 'static,
192    T2: FromRequest + OperationModifier + Send + 'static,
193    T3: FromRequest + OperationModifier + Send + 'static,
194{
195    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
196
197    fn call(self, mut req: Request) -> Self::Future {
198        Box::pin(async move {
199            let t1 = match T1::from_request(&mut req).await {
200                Ok(v) => v,
201                Err(e) => return e.into_response(),
202            };
203            let t2 = match T2::from_request(&mut req).await {
204                Ok(v) => v,
205                Err(e) => return e.into_response(),
206            };
207            let t3 = match T3::from_request(&mut req).await {
208                Ok(v) => v,
209                Err(e) => return e.into_response(),
210            };
211            self(t1, t2, t3).await.into_response()
212        })
213    }
214
215    fn update_operation(op: &mut Operation) {
216        T1::update_operation(op);
217        T2::update_operation(op);
218        T3::update_operation(op);
219        Res::update_response(op);
220    }
221}
222
223// 4 args
224impl<F, Fut, Res, T1, T2, T3, T4> Handler<(T1, T2, T3, T4)> for F
225where
226    F: FnOnce(T1, T2, T3, T4) -> Fut + Clone + Send + Sync + 'static,
227    Fut: Future<Output = Res> + Send + 'static,
228    Res: IntoResponse + ResponseModifier,
229    T1: FromRequest + OperationModifier + Send + 'static,
230    T2: FromRequest + OperationModifier + Send + 'static,
231    T3: FromRequest + OperationModifier + Send + 'static,
232    T4: FromRequest + OperationModifier + Send + 'static,
233{
234    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
235
236    fn call(self, mut req: Request) -> Self::Future {
237        Box::pin(async move {
238            let t1 = match T1::from_request(&mut req).await {
239                Ok(v) => v,
240                Err(e) => return e.into_response(),
241            };
242            let t2 = match T2::from_request(&mut req).await {
243                Ok(v) => v,
244                Err(e) => return e.into_response(),
245            };
246            let t3 = match T3::from_request(&mut req).await {
247                Ok(v) => v,
248                Err(e) => return e.into_response(),
249            };
250            let t4 = match T4::from_request(&mut req).await {
251                Ok(v) => v,
252                Err(e) => return e.into_response(),
253            };
254            self(t1, t2, t3, t4).await.into_response()
255        })
256    }
257
258    fn update_operation(op: &mut Operation) {
259        T1::update_operation(op);
260        T2::update_operation(op);
261        T3::update_operation(op);
262        T4::update_operation(op);
263        Res::update_response(op);
264    }
265}
266
267// 5 args
268impl<F, Fut, Res, T1, T2, T3, T4, T5> Handler<(T1, T2, T3, T4, T5)> for F
269where
270    F: FnOnce(T1, T2, T3, T4, T5) -> Fut + Clone + Send + Sync + 'static,
271    Fut: Future<Output = Res> + Send + 'static,
272    Res: IntoResponse + ResponseModifier,
273    T1: FromRequest + OperationModifier + Send + 'static,
274    T2: FromRequest + OperationModifier + Send + 'static,
275    T3: FromRequest + OperationModifier + Send + 'static,
276    T4: FromRequest + OperationModifier + Send + 'static,
277    T5: FromRequest + OperationModifier + Send + 'static,
278{
279    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
280
281    fn call(self, mut req: Request) -> Self::Future {
282        Box::pin(async move {
283            let t1 = match T1::from_request(&mut req).await {
284                Ok(v) => v,
285                Err(e) => return e.into_response(),
286            };
287            let t2 = match T2::from_request(&mut req).await {
288                Ok(v) => v,
289                Err(e) => return e.into_response(),
290            };
291            let t3 = match T3::from_request(&mut req).await {
292                Ok(v) => v,
293                Err(e) => return e.into_response(),
294            };
295            let t4 = match T4::from_request(&mut req).await {
296                Ok(v) => v,
297                Err(e) => return e.into_response(),
298            };
299            let t5 = match T5::from_request(&mut req).await {
300                Ok(v) => v,
301                Err(e) => return e.into_response(),
302            };
303            self(t1, t2, t3, t4, t5).await.into_response()
304        })
305    }
306
307    fn update_operation(op: &mut Operation) {
308        T1::update_operation(op);
309        T2::update_operation(op);
310        T3::update_operation(op);
311        T4::update_operation(op);
312        T5::update_operation(op);
313        Res::update_response(op);
314    }
315}
316
317// Type-erased handler for storage in router
318pub(crate) type BoxedHandler =
319    std::sync::Arc<dyn Fn(Request) -> Pin<Box<dyn Future<Output = Response> + Send>> + Send + Sync>;
320
321/// Create a boxed handler from any Handler
322pub(crate) fn into_boxed_handler<H, T>(handler: H) -> BoxedHandler
323where
324    H: Handler<T>,
325    T: 'static,
326{
327    std::sync::Arc::new(move |req| {
328        let handler = handler.clone();
329        Box::pin(async move { handler.call(req).await })
330    })
331}
332
333/// Trait for handlers with route metadata (generated by `#[rustapi::get]`, etc.)
334///
335/// This trait provides the path and method information for a handler,
336/// allowing `.mount(handler)` to automatically register the route.
337pub trait RouteHandler<T>: Handler<T> {
338    /// The path pattern for this route (e.g., "/users/{id}")
339    const PATH: &'static str;
340    /// The HTTP method for this route (e.g., "GET")
341    const METHOD: &'static str;
342}
343
344/// Represents a route definition that can be registered with .mount()
345pub struct Route {
346    pub(crate) path: &'static str,
347    pub(crate) method: &'static str,
348    pub(crate) handler: BoxedHandler,
349    pub(crate) operation: Operation,
350    /// Custom parameter schemas for OpenAPI (param_name -> schema_type)
351    /// Supported types: "uuid", "integer", "string", "boolean", "number"
352    pub(crate) param_schemas: std::collections::BTreeMap<String, String>,
353    /// Custom error responses for OpenAPI (status_code -> description)
354    pub(crate) error_responses: Vec<(u16, String)>,
355}
356
357impl Route {
358    /// Create a new route from a handler with path and method
359    pub fn new<H, T>(path: &'static str, method: &'static str, handler: H) -> Self
360    where
361        H: Handler<T>,
362        T: 'static,
363    {
364        let mut operation = Operation::new();
365        H::update_operation(&mut operation);
366
367        Self {
368            path,
369            method,
370            handler: into_boxed_handler(handler),
371            operation,
372            param_schemas: std::collections::BTreeMap::new(),
373            error_responses: Vec::new(),
374        }
375    }
376    /// Set the operation summary
377    pub fn summary(mut self, summary: impl Into<String>) -> Self {
378        self.operation = self.operation.summary(summary);
379        self
380    }
381
382    /// Set the operation description
383    pub fn description(mut self, description: impl Into<String>) -> Self {
384        self.operation = self.operation.description(description);
385        self
386    }
387
388    /// Add a tag to the operation
389    pub fn tag(mut self, tag: impl Into<String>) -> Self {
390        self.operation.tags.push(tag.into());
391        self
392    }
393
394    /// Get the route path
395    pub fn path(&self) -> &str {
396        self.path
397    }
398
399    /// Get the route method
400    pub fn method(&self) -> &str {
401        self.method
402    }
403
404    /// Set a custom OpenAPI schema type for a path parameter
405    ///
406    /// This is useful for overriding the auto-inferred type, e.g., when
407    /// a parameter named `id` is actually a UUID instead of an integer.
408    ///
409    /// # Supported schema types
410    /// - `"uuid"` - String with UUID format
411    /// - `"integer"` or `"int"` - Integer with int64 format
412    /// - `"string"` - Plain string
413    /// - `"boolean"` or `"bool"` - Boolean
414    /// - `"number"` - Number (float)
415    ///
416    /// # Example
417    ///
418    /// ```rust,ignore
419    /// #[rustapi::get("/users/{id}")]
420    /// async fn get_user(Path(id): Path<Uuid>) -> Json<User> {
421    ///     // ...
422    /// }
423    ///
424    /// // In route registration:
425    /// get_route("/users/{id}", get_user).param("id", "uuid")
426    /// ```
427    pub fn param(mut self, name: impl Into<String>, schema_type: impl Into<String>) -> Self {
428        self.param_schemas.insert(name.into(), schema_type.into());
429        self
430    }
431
432    /// Get the custom parameter schemas
433    pub fn param_schemas(&self) -> &std::collections::BTreeMap<String, String> {
434        &self.param_schemas
435    }
436
437    /// Add a typed error response to the OpenAPI specification for this route
438    ///
439    /// This adds an error response entry to the operation's responses map,
440    /// referencing the standard `ErrorSchema` component.
441    ///
442    /// # Arguments
443    ///
444    /// * `status` - HTTP status code (e.g., 404, 403, 409)
445    /// * `description` - Human-readable description of when this error occurs
446    ///
447    /// # Example
448    ///
449    /// ```rust,ignore
450    /// get_route("/users/{id}", get_user)
451    ///     .error_response(404, "User not found")
452    ///     .error_response(403, "Forbidden - insufficient permissions")
453    /// ```
454    pub fn error_response(mut self, status: u16, description: impl Into<String>) -> Self {
455        let desc = description.into();
456        self.error_responses.push((status, desc.clone()));
457
458        // Also add directly to the operation's responses
459        let mut content = std::collections::BTreeMap::new();
460        content.insert(
461            "application/json".to_string(),
462            rustapi_openapi::MediaType {
463                schema: Some(rustapi_openapi::SchemaRef::Ref {
464                    reference: "#/components/schemas/ErrorSchema".to_string(),
465                }),
466                example: None,
467            },
468        );
469
470        self.operation.responses.insert(
471            status.to_string(),
472            rustapi_openapi::ResponseSpec {
473                description: desc,
474                content,
475                headers: std::collections::BTreeMap::new(),
476            },
477        );
478
479        self
480    }
481
482    /// Get the custom error responses
483    pub fn error_responses(&self) -> &[(u16, String)] {
484        &self.error_responses
485    }
486}
487
488/// Helper macro to create a Route from a handler with RouteHandler trait
489#[macro_export]
490macro_rules! route {
491    ($handler:ident) => {{
492        $crate::Route::new($handler::PATH, $handler::METHOD, $handler)
493    }};
494}
495
496/// Create a GET route
497pub fn get_route<H, T>(path: &'static str, handler: H) -> Route
498where
499    H: Handler<T>,
500    T: 'static,
501{
502    Route::new(path, "GET", handler)
503}
504
505/// Create a POST route
506pub fn post_route<H, T>(path: &'static str, handler: H) -> Route
507where
508    H: Handler<T>,
509    T: 'static,
510{
511    Route::new(path, "POST", handler)
512}
513
514/// Create a PUT route
515pub fn put_route<H, T>(path: &'static str, handler: H) -> Route
516where
517    H: Handler<T>,
518    T: 'static,
519{
520    Route::new(path, "PUT", handler)
521}
522
523/// Create a PATCH route
524pub fn patch_route<H, T>(path: &'static str, handler: H) -> Route
525where
526    H: Handler<T>,
527    T: 'static,
528{
529    Route::new(path, "PATCH", handler)
530}
531
532/// Create a DELETE route
533pub fn delete_route<H, T>(path: &'static str, handler: H) -> Route
534where
535    H: Handler<T>,
536    T: 'static,
537{
538    Route::new(path, "DELETE", handler)
539}