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}