Skip to main content

autumn_web/
validation.rs

1//! Validation support via the `validator` crate.
2//!
3//! Provides [`Validated<T>`] — a newtype that proves validation has run —
4//! and [`Valid<T>`] — an extractor that auto-validates request bodies.
5//!
6//! # Usage
7//!
8//! ```rust,ignore
9//! use autumn_web::prelude::*;
10//! use validator::Validate;
11//!
12//! #[derive(Deserialize, Validate)]
13//! struct NewPost {
14//!     #[validate(length(min = 1, max = 200))]
15//!     title: String,
16//! }
17//!
18//! #[post("/posts")]
19//! async fn create(Valid(Json(post)): Valid<Json<NewPost>>) -> &'static str {
20//!     // `post` is guaranteed valid
21//!     "created"
22//! }
23//! ```
24
25use std::collections::HashMap;
26
27use axum::extract::{FromRequest, Request};
28use axum::response::{IntoResponse, Response};
29
30// ── Validated<T> newtype ────────────────────────────────────────
31
32/// Proof that `T` has passed validation.
33///
34/// Cannot be constructed outside this crate — the only way to obtain one
35/// is via [`ValidateExt::validate`] or the [`Valid`] extractor.
36///
37/// Dereferences transparently to `T` for reading, but intentionally does
38/// **not** implement `DerefMut` to prevent mutation into an invalid state.
39pub struct Validated<T>(T);
40
41impl<T> Validated<T> {
42    /// Create a new `Validated<T>`. Restricted to this crate.
43    pub(crate) const fn new(value: T) -> Self {
44        Self(value)
45    }
46
47    /// Unwrap the validated value.
48    #[must_use]
49    pub fn into_inner(self) -> T {
50        self.0
51    }
52}
53
54impl<T> std::ops::Deref for Validated<T> {
55    type Target = T;
56    fn deref(&self) -> &T {
57        &self.0
58    }
59}
60
61impl<T> AsRef<T> for Validated<T> {
62    fn as_ref(&self) -> &T {
63        &self.0
64    }
65}
66
67// ── ValidateExt trait ───────────────────────────────────────────
68
69/// Extension trait that adds `.validate()` to any type implementing
70/// [`validator::Validate`].
71///
72/// Returns `AutumnResult<Validated<Self>>` so the `?` operator works
73/// in handlers.
74pub trait ValidateExt: validator::Validate + Sized {
75    /// Validate this value and wrap it in [`Validated`].
76    ///
77    /// # Errors
78    ///
79    /// Returns [`crate::AutumnError`] with status 422 and field-level
80    /// error details if validation fails.
81    fn validate(self) -> crate::AutumnResult<Validated<Self>> {
82        if let Err(errors) = validator::Validate::validate(&self) {
83            return Err(validation_errors_to_autumn_error(&errors));
84        }
85        Ok(Validated::new(self))
86    }
87}
88
89impl<T: validator::Validate> ValidateExt for T {}
90
91// ── Valid<T> extractor ──────────────────────────────────────────
92
93/// Extractor that deserializes and validates in one step.
94///
95/// Wraps any inner extractor (`Json`, `Form`, `Query`). If
96/// deserialization succeeds but validation fails, returns 422 with
97/// structured error details.
98///
99/// # Examples
100///
101/// ```rust,ignore
102/// use autumn_web::prelude::*;
103/// use autumn_web::Valid;
104///
105/// #[post("/posts")]
106/// async fn create(Valid(Json(new)): Valid<Json<NewPost>>) -> &'static str {
107///     // `new` is guaranteed valid
108///     "created"
109/// }
110/// ```
111pub struct Valid<T>(pub T);
112
113impl<S, T, Inner> FromRequest<S> for Valid<Inner>
114where
115    S: Send + Sync,
116    Inner: FromRequest<S> + AsValidatable<Inner = T>,
117    Inner::Rejection: IntoResponse,
118    T: validator::Validate,
119{
120    type Rejection = Response;
121
122    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
123        let inner = Inner::from_request(req, state)
124            .await
125            .map_err(IntoResponse::into_response)?;
126
127        let value = inner.as_validatable();
128        if let Err(errors) = validator::Validate::validate(value) {
129            return Err(
130                crate::AutumnError::validation(validation_errors_to_map(&errors)).into_response(),
131            );
132        }
133
134        Ok(Self(inner))
135    }
136}
137
138/// Helper trait for extracting the validatable inner type from extractors
139/// like `Json<T>`, `Form<T>`, `Query<T>`.
140pub trait AsValidatable {
141    /// The inner type to validate.
142    type Inner;
143    /// Returns a reference to the inner type to validate.
144    fn as_validatable(&self) -> &Self::Inner;
145}
146
147impl<T> AsValidatable for axum::Json<T> {
148    type Inner = T;
149    fn as_validatable(&self) -> &T {
150        &self.0
151    }
152}
153
154impl<T> AsValidatable for crate::extract::Json<T> {
155    type Inner = T;
156    fn as_validatable(&self) -> &T {
157        &self.0
158    }
159}
160
161impl<T> AsValidatable for axum::extract::Form<T> {
162    type Inner = T;
163    fn as_validatable(&self) -> &T {
164        &self.0
165    }
166}
167
168impl<T> AsValidatable for crate::extract::Form<T> {
169    type Inner = T;
170    fn as_validatable(&self) -> &T {
171        &self.0
172    }
173}
174
175impl<T> AsValidatable for axum::extract::Query<T> {
176    type Inner = T;
177    fn as_validatable(&self) -> &T {
178        &self.0
179    }
180}
181
182impl<T> AsValidatable for crate::extract::Query<T> {
183    type Inner = T;
184    fn as_validatable(&self) -> &T {
185        &self.0
186    }
187}
188
189/// Convert `validator::ValidationErrors` into a field → messages map.
190fn validation_errors_to_map(errors: &validator::ValidationErrors) -> HashMap<String, Vec<String>> {
191    errors
192        .field_errors()
193        .into_iter()
194        .map(|(field, errs)| {
195            let messages = errs
196                .iter()
197                .map(|e| {
198                    e.message.as_ref().map_or_else(
199                        || format!("validation failed: {}", e.code),
200                        ToString::to_string,
201                    )
202                })
203                .collect();
204            (field.to_string(), messages)
205        })
206        .collect()
207}
208
209/// Convert validation errors into an `AutumnError` with 422 status
210/// and structured field-level details.
211///
212/// Not implemented via `From` because `AutumnError` already has a blanket
213/// `From<E: Error>` impl that would conflict.
214fn validation_errors_to_autumn_error(errors: &validator::ValidationErrors) -> crate::AutumnError {
215    crate::AutumnError::validation(validation_errors_to_map(errors))
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn validated_deref() {
224        let v = Validated::new(42);
225        assert_eq!(*v, 42);
226    }
227
228    #[test]
229    fn validated_into_inner() {
230        let v = Validated::new("hello".to_string());
231        let s = v.into_inner();
232        assert_eq!(s, "hello");
233    }
234
235    #[test]
236    fn validated_as_ref() {
237        let v = Validated::new(vec![1, 2, 3]);
238        let r: &Vec<i32> = v.as_ref();
239        assert_eq!(r.len(), 3);
240    }
241
242    #[test]
243    fn validation_errors_to_map_basic() {
244        #[derive(validator::Validate)]
245        struct TestForm {
246            #[validate(length(min = 5))]
247            name: String,
248        }
249
250        let form = TestForm {
251            name: "ab".to_string(),
252        };
253        let errors = validator::Validate::validate(&form).unwrap_err();
254        let map = validation_errors_to_map(&errors);
255
256        assert!(map.contains_key("name"));
257        assert_eq!(map["name"].len(), 1);
258        assert_eq!(map["name"][0], "validation failed: length");
259    }
260
261    #[test]
262    fn validate_ext_ok() {
263        #[derive(validator::Validate)]
264        struct GoodInput {
265            #[validate(length(min = 1))]
266            value: String,
267        }
268
269        let input = GoodInput {
270            value: "hello".into(),
271        };
272        let validated = input.validate();
273        assert!(validated.is_ok());
274        assert_eq!(validated.unwrap().value, "hello");
275    }
276
277    #[test]
278    fn validate_ext_err() {
279        #[derive(validator::Validate)]
280        struct BadInput {
281            #[validate(length(min = 5))]
282            value: String,
283        }
284
285        let input = BadInput { value: "hi".into() };
286        let result = input.validate();
287        assert!(result.is_err());
288    }
289
290    #[test]
291    fn validation_errors_convert_to_autumn_error() {
292        #[derive(validator::Validate)]
293        struct Form {
294            #[validate(email)]
295            email: String,
296        }
297
298        let form = Form {
299            email: "not-an-email".into(),
300        };
301        let errors = validator::Validate::validate(&form).unwrap_err();
302        let autumn_err = validation_errors_to_autumn_error(&errors);
303        assert_eq!(
304            autumn_err.status(),
305            axum::http::StatusCode::UNPROCESSABLE_ENTITY
306        );
307    }
308
309    #[test]
310    fn validation_errors_to_map_fallback_message() {
311        let mut errors = validator::ValidationErrors::new();
312        // Create an error with no custom message
313        let error = validator::ValidationError::new("custom_code");
314        errors.add("my_field", error);
315
316        let map = validation_errors_to_map(&errors);
317
318        assert!(map.contains_key("my_field"));
319        assert_eq!(map["my_field"][0], "validation failed: custom_code");
320    }
321}