Skip to main content

ferro_rs/database/
route_binding.rs

1//! Route model binding support
2//!
3//! Provides automatic model resolution from route parameters.
4//!
5//! # Automatic Route Model Binding
6//!
7//! Route model binding is automatic for all SeaORM models whose Entity implements
8//! `ferro_rs::database::Model`. Simply use the Model type as a handler parameter:
9//!
10//! ```rust,ignore
11//! use ferro_rs::{handler, json_response, Response};
12//! use ferro_rs::models::user;
13//!
14//! // Just use the Model in your handler - binding is automatic!
15//! #[handler]
16//! pub async fn show(user: user::Model) -> Response {
17//!     json_response!({ "name": user.name })
18//! }
19//! ```
20//!
21//! The parameter name (`user`) is used as the route parameter key. So for a route
22//! defined as `/users/{user}`, the `user` parameter will be automatically resolved.
23//!
24//! If the model is not found, a 404 Not Found response is returned.
25//! If the parameter cannot be parsed, a 400 Bad Request response is returned.
26
27use crate::error::FrameworkError;
28use async_trait::async_trait;
29use sea_orm::{EntityTrait, ModelTrait as SeaModelTrait, PrimaryKeyTrait};
30
31/// Trait for models that can be automatically resolved from route parameters
32///
33/// Implement this trait on your SeaORM Model types to enable automatic
34/// route model binding in handlers. When a route parameter matches the
35/// `param_name()`, the model will be automatically fetched from the database.
36///
37/// If the model is not found, a 404 Not Found response is returned.
38///
39/// # Example
40///
41/// ```rust,ignore
42/// use ferro_rs::database::RouteBinding;
43/// use ferro_rs::FrameworkError;
44///
45/// #[async_trait]
46/// impl RouteBinding for user::Model {
47///     fn param_name() -> &'static str {
48///         "user"  // matches {user} in route like /users/{user}
49///     }
50///
51///     async fn from_route_param(value: &str) -> Result<Self, FrameworkError> {
52///         let id: i32 = value.parse()
53///             .map_err(|_| FrameworkError::param_parse(value, "i32"))?;
54///
55///         user::Entity::find_by_pk(id)
56///             .await?
57///             .ok_or_else(|| FrameworkError::model_not_found("User"))
58///     }
59/// }
60/// ```
61#[async_trait]
62pub trait RouteBinding: Sized + Send {
63    /// The route parameter name to bind from
64    ///
65    /// This should match the parameter placeholder in your route definition.
66    /// For example, if your route is `/users/{user}`, this should return `"user"`.
67    fn param_name() -> &'static str;
68
69    /// Fetch the model from the database using the route parameter value
70    ///
71    /// This method is called automatically by the `#[handler]` macro when
72    /// a parameter of this type is declared in the handler function.
73    ///
74    /// # Returns
75    ///
76    /// - `Ok(Self)` - The model was found
77    /// - `Err(FrameworkError::ModelNotFound)` - Model not found (returns 404)
78    /// - `Err(FrameworkError::ParamParse)` - Parameter could not be parsed (returns 400)
79    async fn from_route_param(value: &str) -> Result<Self, FrameworkError>;
80}
81
82/// Trait for automatic route model binding
83///
84/// This trait is automatically implemented for all SeaORM models whose Entity
85/// implements `ferro_rs::database::Model`. You don't need to implement this manually.
86///
87/// Unlike [`RouteBinding`], this trait doesn't require a `param_name()` method.
88/// The parameter name is derived from the handler function signature.
89///
90/// # Example
91///
92/// ```rust,ignore
93/// // Just use Model in handler - binding is automatic!
94/// #[handler]
95/// pub async fn show(user: user::Model) -> Response {
96///     json_response!({ "name": user.name })
97/// }
98/// ```
99#[async_trait]
100pub trait AutoRouteBinding: Sized + Send {
101    /// Fetch the model from the database using the route parameter value
102    ///
103    /// This method parses the parameter as the primary key type and fetches
104    /// the corresponding model from the database.
105    ///
106    /// # Returns
107    ///
108    /// - `Ok(Self)` - The model was found
109    /// - `Err(FrameworkError::ModelNotFound)` - Model not found (returns 404)
110    /// - `Err(FrameworkError::ParamParse)` - Parameter could not be parsed (returns 400)
111    async fn from_route_param(value: &str) -> Result<Self, FrameworkError>;
112}
113
114/// Blanket implementation of AutoRouteBinding for all SeaORM models
115///
116/// This automatically implements route model binding for any SeaORM Model type
117/// whose Entity implements `ferro_rs::database::Model`. Supports any primary key type
118/// that implements `FromStr` (i32, i64, String, UUID, etc.).
119#[async_trait]
120impl<M, E> AutoRouteBinding for M
121where
122    M: SeaModelTrait<Entity = E> + Send + Sync,
123    E: EntityTrait<Model = M> + crate::database::Model + Sync,
124    E::PrimaryKey: PrimaryKeyTrait,
125    <E::PrimaryKey as PrimaryKeyTrait>::ValueType: std::str::FromStr + Send,
126{
127    async fn from_route_param(value: &str) -> Result<Self, FrameworkError> {
128        let id: <E::PrimaryKey as PrimaryKeyTrait>::ValueType = value.parse().map_err(|_| {
129            FrameworkError::param_parse(
130                value,
131                std::any::type_name::<<E::PrimaryKey as PrimaryKeyTrait>::ValueType>(),
132            )
133        })?;
134
135        <E as crate::database::Model>::find_by_pk(id)
136            .await?
137            .ok_or_else(|| {
138                // Extract a cleaner model name from the full type name
139                let full_name = std::any::type_name::<M>();
140                let model_name = full_name.rsplit("::").nth(1).unwrap_or(full_name);
141                FrameworkError::model_not_found(model_name)
142            })
143    }
144}
145
146/// Convenience macro to implement RouteBinding for a SeaORM model
147///
148/// **DEPRECATED**: This macro is no longer needed. Route model binding is now
149/// automatic for any model whose Entity implements `ferro_rs::database::Model`.
150/// Simply use the Model type in your handler parameter.
151///
152/// This macro implements the `RouteBinding` trait for a model, enabling
153/// automatic route model binding with 404 handling.
154///
155/// # Arguments
156///
157/// - `$entity` - The SeaORM Entity type (e.g., `user::Entity`)
158/// - `$model` - The SeaORM Model type (e.g., `user::Model`)
159/// - `$param` - The route parameter name (e.g., `"user"`)
160///
161/// # Example
162///
163/// ```rust,ignore
164/// use ferro_rs::route_binding;
165///
166/// // In your model file (e.g., models/user.rs)
167/// route_binding!(Entity, Model, "user");
168///
169/// // Now you can use automatic binding in handlers:
170/// #[handler]
171/// pub async fn show(user: user::Model) -> Response {
172///     json_response!({ "id": user.id, "name": user.name })
173/// }
174/// ```
175///
176/// # Route Definition
177///
178/// The parameter name must match your route definition:
179///
180/// ```rust,ignore
181/// routes! {
182///     get!("/users/{user}", controllers::user::show),
183/// }
184/// ```
185#[macro_export]
186macro_rules! route_binding {
187    ($entity:ty, $model:ty, $param:literal) => {
188        #[async_trait::async_trait]
189        impl $crate::RouteBinding for $model {
190            fn param_name() -> &'static str {
191                $param
192            }
193
194            async fn from_route_param(value: &str) -> Result<Self, $crate::FrameworkError> {
195                let id: i32 = value
196                    .parse()
197                    .map_err(|_| $crate::FrameworkError::param_parse(value, "i32"))?;
198
199                <$entity as $crate::Model>::find_by_pk(id)
200                    .await?
201                    .ok_or_else(|| $crate::FrameworkError::model_not_found(stringify!($model)))
202            }
203        }
204    };
205}