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.
190pub(crate) fn validation_errors_to_map(
191    errors: &validator::ValidationErrors,
192) -> HashMap<String, Vec<String>> {
193    errors
194        .field_errors()
195        .into_iter()
196        .map(|(field, errs)| {
197            let messages = errs
198                .iter()
199                .map(|e| {
200                    e.message.as_ref().map_or_else(
201                        || format!("validation failed: {}", e.code),
202                        ToString::to_string,
203                    )
204                })
205                .collect();
206            (field.to_string(), messages)
207        })
208        .collect()
209}
210
211/// Convert validation errors into an `AutumnError` with 422 status
212/// and structured field-level details.
213///
214/// Not implemented via `From` because `AutumnError` already has a blanket
215/// `From<E: Error>` impl that would conflict.
216fn validation_errors_to_autumn_error(errors: &validator::ValidationErrors) -> crate::AutumnError {
217    crate::AutumnError::validation(validation_errors_to_map(errors))
218}
219
220// ── Conditional validation (autoref specialization) ─────────────
221//
222// The `#[repository(api = ...)]` macro needs to validate a decoded write
223// payload *only when its type implements `validator::Validate`* — the
224// generated `NewModel` derives `Validate` solely when the model declares
225// `#[validate(...)]` rules, and a hand-written `NewModel` may not implement
226// it at all. Rust has no stable negative/​specialization reasoning, so we use
227// autoref-based specialization (Kalbertodt/dtolnay): a value that implements
228// `Validate` resolves to the real validating impl (fewer autorefs), while
229// everything else falls through to a no-op. This keeps existing repositories
230// compiling with zero migration burden.
231
232/// Wrapper carrying a reference to a candidate write payload for the autoref
233/// specialization used by the generated API write handlers.
234///
235/// Not part of the stable API — used only by macro-generated code.
236#[doc(hidden)]
237pub struct MaybeValidate<'a, T>(pub &'a T);
238
239/// Specialized branch: runs `validator::Validate` and maps failures to a
240/// `422` [`crate::AutumnError`] with the per-field `errors` map.
241#[doc(hidden)]
242pub trait MaybeValidateViaValidator {
243    /// Validate the wrapped value; `Ok(())` when it passes.
244    ///
245    /// # Errors
246    /// Returns a `422` validation error when the wrapped value fails a rule.
247    fn autumn_maybe_validate(&self) -> crate::AutumnResult<()>;
248}
249
250impl<T: validator::Validate> MaybeValidateViaValidator for MaybeValidate<'_, T> {
251    fn autumn_maybe_validate(&self) -> crate::AutumnResult<()> {
252        match validator::Validate::validate(self.0) {
253            Ok(()) => Ok(()),
254            Err(errors) => Err(validation_errors_to_autumn_error(&errors)),
255        }
256    }
257}
258
259/// Fallback branch (behind one autoref): any type that does *not* implement
260/// `Validate` is accepted without validation.
261#[doc(hidden)]
262pub trait MaybeValidateFallback {
263    /// No-op validation for types without `#[validate]` rules.
264    ///
265    /// # Errors
266    /// Never returns an error.
267    fn autumn_maybe_validate(&self) -> crate::AutumnResult<()>;
268}
269
270impl<T> MaybeValidateFallback for &MaybeValidate<'_, T> {
271    fn autumn_maybe_validate(&self) -> crate::AutumnResult<()> {
272        Ok(())
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn validated_deref() {
282        let v = Validated::new(42);
283        assert_eq!(*v, 42);
284    }
285
286    #[test]
287    fn validated_into_inner() {
288        let v = Validated::new("hello".to_string());
289        let s = v.into_inner();
290        assert_eq!(s, "hello");
291    }
292
293    #[test]
294    fn validated_as_ref() {
295        let v = Validated::new(vec![1, 2, 3]);
296        let r: &Vec<i32> = v.as_ref();
297        assert_eq!(r.len(), 3);
298    }
299
300    #[test]
301    fn validation_errors_to_map_basic() {
302        #[derive(validator::Validate)]
303        struct TestForm {
304            #[validate(length(min = 5))]
305            name: String,
306        }
307
308        let form = TestForm {
309            name: "ab".to_string(),
310        };
311        let errors = validator::Validate::validate(&form).unwrap_err();
312        let map = validation_errors_to_map(&errors);
313
314        assert!(map.contains_key("name"));
315        assert_eq!(map["name"].len(), 1);
316        assert_eq!(map["name"][0], "validation failed: length");
317    }
318
319    #[test]
320    fn update_patch_field_validates_via_derive() {
321        // #1719: a generated `UpdateModel` derives `validator::Validate` and
322        // carries `#[validate(...)]` on `Patch<T>` fields. A `Set` value runs
323        // the rule (and surfaces a per-field error keyed by the field name),
324        // while an absent (`Unchanged`/`Clear`) field is skipped.
325        use crate::hooks::Patch;
326
327        #[derive(validator::Validate)]
328        struct UpdatePost {
329            #[validate(length(min = 1))]
330            title: Patch<String>,
331        }
332
333        // `Set("")` violates `length(min = 1)` → 422-shaped field error map.
334        let bad = UpdatePost {
335            title: Patch::Set(String::new()),
336        };
337        let errors = validator::Validate::validate(&bad).unwrap_err();
338        let map = validation_errors_to_map(&errors);
339        assert!(map.contains_key("title"));
340        assert_eq!(map["title"][0], "validation failed: length");
341
342        // Absent field → rule skipped → passes.
343        let unchanged = UpdatePost {
344            title: Patch::Unchanged,
345        };
346        assert!(validator::Validate::validate(&unchanged).is_ok());
347
348        // `Set` with a satisfying value → passes.
349        let good = UpdatePost {
350            title: Patch::Set("hello".into()),
351        };
352        assert!(validator::Validate::validate(&good).is_ok());
353    }
354
355    #[test]
356    fn validate_ext_ok() {
357        #[derive(validator::Validate)]
358        struct GoodInput {
359            #[validate(length(min = 1))]
360            value: String,
361        }
362
363        let input = GoodInput {
364            value: "hello".into(),
365        };
366        let validated = input.validate();
367        assert!(validated.is_ok());
368        assert_eq!(validated.unwrap().value, "hello");
369    }
370
371    #[test]
372    fn validate_ext_err() {
373        #[derive(validator::Validate)]
374        struct BadInput {
375            #[validate(length(min = 5))]
376            value: String,
377        }
378
379        let input = BadInput { value: "hi".into() };
380        let result = input.validate();
381        assert!(result.is_err());
382    }
383
384    #[test]
385    fn validation_errors_convert_to_autumn_error() {
386        #[derive(validator::Validate)]
387        struct Form {
388            #[validate(email)]
389            email: String,
390        }
391
392        let form = Form {
393            email: "not-an-email".into(),
394        };
395        let errors = validator::Validate::validate(&form).unwrap_err();
396        let autumn_err = validation_errors_to_autumn_error(&errors);
397        assert_eq!(
398            autumn_err.status(),
399            axum::http::StatusCode::UNPROCESSABLE_ENTITY
400        );
401    }
402
403    #[test]
404    fn validation_errors_to_map_fallback_message() {
405        let mut errors = validator::ValidationErrors::new();
406        // Create an error with no custom message
407        let error = validator::ValidationError::new("custom_code");
408        errors.add("my_field", error);
409
410        let map = validation_errors_to_map(&errors);
411
412        assert!(map.contains_key("my_field"));
413        assert_eq!(map["my_field"][0], "validation failed: custom_code");
414    }
415
416    #[tokio::test]
417    async fn valid_extractor_ok() {
418        use axum::body::Body;
419
420        #[derive(serde::Deserialize, validator::Validate)]
421        struct TestInput {
422            #[validate(length(min = 1))]
423            name: String,
424        }
425
426        let req = Request::builder()
427            .method("POST")
428            .header("content-type", "application/json")
429            .body(Body::from(r#"{"name": "Alice"}"#))
430            .unwrap();
431
432        let state = ();
433        let result = Valid::<axum::Json<TestInput>>::from_request(req, &state).await;
434
435        assert!(result.is_ok());
436        assert_eq!(result.unwrap().0.0.name, "Alice");
437    }
438
439    #[test]
440    // Mirrors the `(&MaybeValidate(&x)).autumn_maybe_validate()` form the
441    // repository macro emits: both traits in scope, explicit leading borrow.
442    #[allow(clippy::needless_borrow, unused_imports)]
443    fn maybe_validate_runs_for_validate_types() {
444        // Autoref specialization: a type implementing `Validate` resolves to
445        // the real validating branch and surfaces a 422 on failure.
446        use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
447
448        #[derive(validator::Validate)]
449        struct HasRules {
450            #[validate(length(min = 5))]
451            name: String,
452        }
453
454        let bad = HasRules {
455            name: "ab".to_string(),
456        };
457        let err = (&MaybeValidate(&bad))
458            .autumn_maybe_validate()
459            .expect_err("short name must fail validation");
460        assert_eq!(err.status(), axum::http::StatusCode::UNPROCESSABLE_ENTITY);
461
462        let good = HasRules {
463            name: "alice".to_string(),
464        };
465        assert!(
466            (&MaybeValidate(&good)).autumn_maybe_validate().is_ok(),
467            "valid input must pass"
468        );
469    }
470
471    #[test]
472    #[allow(clippy::needless_borrow, unused_imports)]
473    fn maybe_validate_is_noop_for_non_validate_types() {
474        // Autoref specialization: a type that does NOT implement `Validate`
475        // falls through to the no-op branch and compiles + always succeeds.
476        use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
477
478        struct NoRules {
479            _name: String,
480        }
481
482        let value = NoRules {
483            _name: "anything".to_string(),
484        };
485        assert!(
486            (&MaybeValidate(&value)).autumn_maybe_validate().is_ok(),
487            "types without validation rules must be accepted unchanged"
488        );
489    }
490
491    #[tokio::test]
492    async fn valid_extractor_err() {
493        use axum::body::Body;
494
495        #[derive(serde::Deserialize, validator::Validate)]
496        struct TestInput {
497            #[validate(length(min = 5))]
498            name: String,
499        }
500
501        let req = Request::builder()
502            .method("POST")
503            .header("content-type", "application/json")
504            .body(Body::from(r#"{"name": "Bob"}"#)) // Too short
505            .unwrap();
506
507        let state = ();
508        let result = Valid::<axum::Json<TestInput>>::from_request(req, &state).await;
509
510        match result {
511            Ok(_) => panic!("Expected validation error"),
512            Err(response) => {
513                assert_eq!(
514                    response.status(),
515                    axum::http::StatusCode::UNPROCESSABLE_ENTITY
516                );
517            }
518        }
519    }
520}