Skip to main content

ferro_rs/http/
form_request.rs

1//! FormRequest trait for validated request data
2//!
3//! Provides Laravel-like FormRequest pattern with automatic body parsing,
4//! validation, and authorization.
5
6use super::body::{parse_form, parse_json};
7use super::extract::FromRequest;
8use super::Request;
9use crate::error::{FrameworkError, ValidationErrors};
10use async_trait::async_trait;
11use serde::de::DeserializeOwned;
12use validator::Validate;
13
14/// Trait for validated form/JSON request data
15///
16/// Implement this trait on request structs to enable automatic:
17/// - Body parsing (JSON or form-urlencoded based on Content-Type)
18/// - Validation using the `validator` crate
19/// - Authorization checks
20///
21/// # Example
22///
23/// ```rust,ignore
24/// use ferro_rs::FormRequest;
25/// use serde::Deserialize;
26/// use validator::Validate;
27///
28/// #[derive(FormRequest)]  // Auto-derives Deserialize, Validate, and FormRequest impl
29/// pub struct CreateUserRequest {
30///     #[validate(email)]
31///     pub email: String,
32///
33///     #[validate(length(min = 8))]
34///     pub password: String,
35/// }
36///
37/// // In controller:
38/// #[handler]
39/// pub async fn store(form: CreateUserRequest) -> Response {
40///     // `form` is already validated - returns 422 if invalid
41///     json_response!({ "email": form.email })
42/// }
43/// ```
44///
45/// # Authorization
46///
47/// Override `authorize()` to add authorization logic:
48///
49/// ```rust,ignore
50/// impl FormRequest for CreateUserRequest {
51///     fn authorize(_req: &Request) -> bool {
52///         // Check if user is authenticated
53///         true
54///     }
55/// }
56/// ```
57#[async_trait]
58pub trait FormRequest: Sized + DeserializeOwned + Validate + Send {
59    /// Check if the request is authorized
60    ///
61    /// Override this method to add authorization logic.
62    /// Returns `true` by default (all requests authorized).
63    ///
64    /// Returning `false` will result in a 403 Forbidden response.
65    fn authorize(_req: &Request) -> bool {
66        true
67    }
68
69    /// Extract and validate data from the request
70    ///
71    /// This method:
72    /// 1. Checks authorization
73    /// 2. Parses the request body (JSON or form based on Content-Type)
74    /// 3. Validates the parsed data
75    ///
76    /// Returns `Err(FrameworkError)` on authorization failure, parse error,
77    /// or validation failure.
78    async fn extract(req: Request) -> Result<Self, FrameworkError> {
79        // Check authorization first
80        if !Self::authorize(&req) {
81            return Err(FrameworkError::Unauthorized);
82        }
83
84        // Get content type before consuming body
85        let content_type = req.content_type().map(|s| s.to_string());
86
87        // Collect and parse body
88        let (_, bytes) = req.body_bytes().await?;
89
90        let data: Self = match content_type.as_deref() {
91            Some(ct) if ct.starts_with("application/x-www-form-urlencoded") => parse_form(&bytes)?,
92            _ => parse_json(&bytes)?,
93        };
94
95        // Validate the parsed data
96        if let Err(errors) = data.validate() {
97            return Err(FrameworkError::Validation(
98                ValidationErrors::from_validator(errors),
99            ));
100        }
101
102        Ok(data)
103    }
104}
105
106/// Blanket implementation of FromRequest for all FormRequest types
107#[async_trait]
108impl<T: FormRequest> FromRequest for T {
109    async fn from_request(req: Request) -> Result<Self, FrameworkError> {
110        T::extract(req).await
111    }
112}