Skip to main content

autumn_web/
validation.rs

1//! Validation support via the `validator` crate.
2//!
3//! See the [forms, validation and normalization guide](https://github.com/autumn-foundation/autumn/blob/trunk/docs/guide/forms.md)
4//! for when to reach for [`Valid`] versus [`crate::form::ChangesetForm`].
5//!
6//! Provides [`Validated<T>`] — a newtype that proves validation has run —
7//! and [`Valid<T>`] — an extractor that auto-validates request bodies.
8//!
9//! # Usage
10//!
11//! ```rust,ignore
12//! use autumn_web::prelude::*;
13//! use validator::Validate;
14//!
15//! #[derive(Deserialize, Validate)]
16//! struct NewPost {
17//!     #[validate(length(min = 1, max = 200))]
18//!     title: String,
19//! }
20//!
21//! #[post("/posts")]
22//! async fn create(Valid(Json(post)): Valid<Json<NewPost>>) -> &'static str {
23//!     // `post` is guaranteed valid
24//!     "created"
25//! }
26//! ```
27
28use std::collections::HashMap;
29
30use axum::extract::{FromRequest, Request};
31use axum::response::{IntoResponse, Response};
32
33// ── Validated<T> newtype ────────────────────────────────────────
34
35/// Proof that `T` has passed validation.
36///
37/// Cannot be constructed outside this crate — the only way to obtain one
38/// is via [`ValidateExt::validate`] or the [`Valid`] extractor.
39///
40/// Dereferences transparently to `T` for reading, but intentionally does
41/// **not** implement `DerefMut` to prevent mutation into an invalid state.
42pub struct Validated<T>(T);
43
44impl<T> Validated<T> {
45    /// Create a new `Validated<T>`. Restricted to this crate.
46    pub(crate) const fn new(value: T) -> Self {
47        Self(value)
48    }
49
50    /// Unwrap the validated value.
51    #[must_use]
52    pub fn into_inner(self) -> T {
53        self.0
54    }
55}
56
57impl<T> std::ops::Deref for Validated<T> {
58    type Target = T;
59    fn deref(&self) -> &T {
60        &self.0
61    }
62}
63
64impl<T> AsRef<T> for Validated<T> {
65    fn as_ref(&self) -> &T {
66        &self.0
67    }
68}
69
70// ── ValidateExt trait ───────────────────────────────────────────
71
72/// Extension trait that adds `.validate()` to any type implementing
73/// [`validator::Validate`].
74///
75/// Returns `AutumnResult<Validated<Self>>` so the `?` operator works
76/// in handlers.
77///
78/// # Hazard: don't combine with `#[validate(nested)]`
79///
80/// This blanket `impl<T: validator::Validate> ValidateExt for T` applies to
81/// *every* `Validate` type, including one used as a `#[validate(nested)]`
82/// field inside another `#[derive(validator::Validate)]` struct. `nested`'s
83/// generated code calls that field with bare method syntax —
84/// `(&self.field).validate()` — and if this trait is ALSO in scope in the
85/// **struct's own defining module** at that point (it is, the moment that
86/// module does `use autumn_web::prelude::*;`), rustc reports `E0034: multiple
87/// applicable items in scope`, pointing at the derive expansion rather than
88/// at anything you wrote. This applies equally to `#[autumn_web::model]`
89/// structs (issue #1751) — the macro forwards `#[validate(...)]` verbatim and
90/// cannot detect the collision at expansion time, since a derive/attribute
91/// macro only sees the item it's attached to, not the rest of its enclosing
92/// module's `use` statements, so it does not (and cannot reliably) refuse
93/// `nested` on your behalf. The collision is scoped to that one module,
94/// though: importing this trait in some *other* module that merely uses the
95/// nested-validating type does not trigger it. So keep a nested-validating
96/// struct's own module free of this trait (and of `autumn_web::prelude`), or
97/// express the rule with `#[validate(custom(function = "..."))]` instead.
98pub trait ValidateExt: validator::Validate + Sized {
99    /// Validate this value and wrap it in [`Validated`].
100    ///
101    /// # Errors
102    ///
103    /// Returns [`crate::AutumnError`] with status 422 and field-level
104    /// error details if validation fails.
105    fn validate(self) -> crate::AutumnResult<Validated<Self>> {
106        if let Err(errors) = validator::Validate::validate(&self) {
107            return Err(validation_errors_to_autumn_error(&errors));
108        }
109        Ok(Validated::new(self))
110    }
111}
112
113impl<T: validator::Validate> ValidateExt for T {}
114
115// ── Valid<T> extractor ──────────────────────────────────────────
116
117/// Extractor that deserializes and validates in one step.
118///
119/// Wraps any inner extractor (`Json`, `Form`, `Query`). If
120/// deserialization succeeds but validation fails, returns 422 with
121/// structured error details.
122///
123/// # Examples
124///
125/// ```rust,ignore
126/// use autumn_web::prelude::*;
127/// use autumn_web::Valid;
128///
129/// #[post("/posts")]
130/// async fn create(Valid(Json(new)): Valid<Json<NewPost>>) -> &'static str {
131///     // `new` is guaranteed valid
132///     "created"
133/// }
134/// ```
135pub struct Valid<T>(pub T);
136
137impl<S, T, Inner> FromRequest<S> for Valid<Inner>
138where
139    S: Send + Sync,
140    Inner: FromRequest<S> + AsValidatable<Inner = T>,
141    Inner::Rejection: IntoResponse,
142    T: validator::Validate,
143{
144    type Rejection = Response;
145
146    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
147        let inner = Inner::from_request(req, state)
148            .await
149            .map_err(IntoResponse::into_response)?;
150
151        let value = inner.as_validatable();
152        if let Err(errors) = validator::Validate::validate(value) {
153            return Err(
154                crate::AutumnError::validation(validation_errors_to_map(&errors)).into_response(),
155            );
156        }
157
158        Ok(Self(inner))
159    }
160}
161
162/// Helper trait for extracting the validatable inner type from extractors
163/// like `Json<T>`, `Form<T>`, `Query<T>`.
164pub trait AsValidatable {
165    /// The inner type to validate.
166    type Inner;
167    /// Returns a reference to the inner type to validate.
168    fn as_validatable(&self) -> &Self::Inner;
169}
170
171impl<T> AsValidatable for axum::Json<T> {
172    type Inner = T;
173    fn as_validatable(&self) -> &T {
174        &self.0
175    }
176}
177
178impl<T> AsValidatable for crate::extract::Json<T> {
179    type Inner = T;
180    fn as_validatable(&self) -> &T {
181        &self.0
182    }
183}
184
185impl<T> AsValidatable for axum::extract::Form<T> {
186    type Inner = T;
187    fn as_validatable(&self) -> &T {
188        &self.0
189    }
190}
191
192impl<T> AsValidatable for crate::extract::Form<T> {
193    type Inner = T;
194    fn as_validatable(&self) -> &T {
195        &self.0
196    }
197}
198
199impl<T> AsValidatable for axum::extract::Query<T> {
200    type Inner = T;
201    fn as_validatable(&self) -> &T {
202        &self.0
203    }
204}
205
206impl<T> AsValidatable for crate::extract::Query<T> {
207    type Inner = T;
208    fn as_validatable(&self) -> &T {
209        &self.0
210    }
211}
212
213/// Convert `validator::ValidationErrors` into a field → messages map.
214pub(crate) fn validation_errors_to_map(
215    errors: &validator::ValidationErrors,
216) -> HashMap<String, Vec<String>> {
217    errors
218        .field_errors()
219        .into_iter()
220        .map(|(field, errs)| {
221            let messages = errs
222                .iter()
223                .map(|e| {
224                    e.message.as_ref().map_or_else(
225                        || format!("validation failed: {}", e.code),
226                        ToString::to_string,
227                    )
228                })
229                .collect();
230            (field.to_string(), messages)
231        })
232        .collect()
233}
234
235/// Convert validation errors into an `AutumnError` with 422 status
236/// and structured field-level details.
237///
238/// Not implemented via `From` because `AutumnError` already has a blanket
239/// `From<E: Error>` impl that would conflict.
240fn validation_errors_to_autumn_error(errors: &validator::ValidationErrors) -> crate::AutumnError {
241    crate::AutumnError::validation(validation_errors_to_map(errors))
242}
243
244// ── Conditional validation (autoref specialization) ─────────────
245//
246// The `#[repository(api = ...)]` macro needs to validate a decoded write
247// payload *only when its type implements `validator::Validate`* — the
248// generated `NewModel` derives `Validate` solely when the model declares
249// `#[validate(...)]` rules, and a hand-written `NewModel` may not implement
250// it at all. Rust has no stable negative/​specialization reasoning, so we use
251// autoref-based specialization (Kalbertodt/dtolnay): a value that implements
252// `Validate` resolves to the real validating impl (fewer autorefs), while
253// everything else falls through to a no-op. This keeps existing repositories
254// compiling with zero migration burden.
255
256/// Wrapper carrying a reference to a candidate write payload for the autoref
257/// specialization used by the generated API write handlers.
258///
259/// Not part of the stable API — used only by macro-generated code.
260#[doc(hidden)]
261pub struct MaybeValidate<'a, T>(pub &'a T);
262
263/// Specialized branch: runs `validator::Validate` and maps failures to a
264/// `422` [`crate::AutumnError`] with the per-field `errors` map.
265#[doc(hidden)]
266pub trait MaybeValidateViaValidator {
267    /// Validate the wrapped value; `Ok(())` when it passes.
268    ///
269    /// # Errors
270    /// Returns a `422` validation error when the wrapped value fails a rule.
271    fn autumn_maybe_validate(&self) -> crate::AutumnResult<()>;
272}
273
274impl<T: validator::Validate> MaybeValidateViaValidator for MaybeValidate<'_, T> {
275    fn autumn_maybe_validate(&self) -> crate::AutumnResult<()> {
276        match validator::Validate::validate(self.0) {
277            Ok(()) => Ok(()),
278            Err(errors) => Err(validation_errors_to_autumn_error(&errors)),
279        }
280    }
281}
282
283/// Fallback branch (behind one autoref): any type that does *not* implement
284/// `Validate` is accepted without validation.
285#[doc(hidden)]
286pub trait MaybeValidateFallback {
287    /// No-op validation for types without `#[validate]` rules.
288    ///
289    /// # Errors
290    /// Never returns an error.
291    fn autumn_maybe_validate(&self) -> crate::AutumnResult<()>;
292}
293
294impl<T> MaybeValidateFallback for &MaybeValidate<'_, T> {
295    fn autumn_maybe_validate(&self) -> crate::AutumnResult<()> {
296        Ok(())
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn validated_deref() {
306        let v = Validated::new(42);
307        assert_eq!(*v, 42);
308    }
309
310    #[test]
311    fn validated_into_inner() {
312        let v = Validated::new("hello".to_string());
313        let s = v.into_inner();
314        assert_eq!(s, "hello");
315    }
316
317    #[test]
318    fn validated_as_ref() {
319        let v = Validated::new(vec![1, 2, 3]);
320        let r: &Vec<i32> = v.as_ref();
321        assert_eq!(r.len(), 3);
322    }
323
324    #[test]
325    fn validation_errors_to_map_basic() {
326        #[derive(validator::Validate)]
327        struct TestForm {
328            #[validate(length(min = 5))]
329            name: String,
330        }
331
332        let form = TestForm {
333            name: "ab".to_string(),
334        };
335        let errors = validator::Validate::validate(&form).unwrap_err();
336        let map = validation_errors_to_map(&errors);
337
338        assert!(map.contains_key("name"));
339        assert_eq!(map["name"].len(), 1);
340        assert_eq!(map["name"][0], "validation failed: length");
341    }
342
343    #[test]
344    fn update_patch_field_validates_via_derive() {
345        // #1719: a generated `UpdateModel` derives `validator::Validate` and
346        // carries `#[validate(...)]` on `Patch<T>` fields. A `Set` value runs
347        // the rule (and surfaces a per-field error keyed by the field name),
348        // while an absent (`Unchanged`/`Clear`) field is skipped.
349        use crate::hooks::Patch;
350
351        #[derive(validator::Validate)]
352        struct UpdatePost {
353            #[validate(length(min = 1))]
354            title: Patch<String>,
355        }
356
357        // `Set("")` violates `length(min = 1)` → 422-shaped field error map.
358        let bad = UpdatePost {
359            title: Patch::Set(String::new()),
360        };
361        let errors = validator::Validate::validate(&bad).unwrap_err();
362        let map = validation_errors_to_map(&errors);
363        assert!(map.contains_key("title"));
364        assert_eq!(map["title"][0], "validation failed: length");
365
366        // Absent field → rule skipped → passes.
367        let unchanged = UpdatePost {
368            title: Patch::Unchanged,
369        };
370        assert!(validator::Validate::validate(&unchanged).is_ok());
371
372        // `Set` with a satisfying value → passes.
373        let good = UpdatePost {
374            title: Patch::Set("hello".into()),
375        };
376        assert!(validator::Validate::validate(&good).is_ok());
377    }
378
379    #[test]
380    fn validate_ext_ok() {
381        #[derive(validator::Validate)]
382        struct GoodInput {
383            #[validate(length(min = 1))]
384            value: String,
385        }
386
387        let input = GoodInput {
388            value: "hello".into(),
389        };
390        let validated = input.validate();
391        assert!(validated.is_ok());
392        assert_eq!(validated.unwrap().value, "hello");
393    }
394
395    #[test]
396    fn validate_ext_err() {
397        #[derive(validator::Validate)]
398        struct BadInput {
399            #[validate(length(min = 5))]
400            value: String,
401        }
402
403        let input = BadInput { value: "hi".into() };
404        let result = input.validate();
405        assert!(result.is_err());
406    }
407
408    #[test]
409    fn validation_errors_convert_to_autumn_error() {
410        #[derive(validator::Validate)]
411        struct Form {
412            #[validate(email)]
413            email: String,
414        }
415
416        let form = Form {
417            email: "not-an-email".into(),
418        };
419        let errors = validator::Validate::validate(&form).unwrap_err();
420        let autumn_err = validation_errors_to_autumn_error(&errors);
421        assert_eq!(
422            autumn_err.status(),
423            axum::http::StatusCode::UNPROCESSABLE_ENTITY
424        );
425    }
426
427    #[test]
428    fn validate_ext_failure_exposes_field_details() {
429        // #2587: a non-HTTP caller reads the field map off the error.
430        #[derive(validator::Validate)]
431        struct Form {
432            #[validate(email(message = "Must be a valid email address"))]
433            email: String,
434        }
435
436        let Err(err) = Form {
437            email: "not-an-email".into(),
438        }
439        .validate() else {
440            panic!("invalid email is rejected");
441        };
442
443        let details = err.details().expect("validation error carries details");
444        assert_eq!(
445            details["email"],
446            ["Must be a valid email address".to_owned()]
447        );
448        assert_eq!(err.code(), "autumn.validation_failed");
449        assert_eq!(
450            err.to_string(),
451            "Validation failed: email: Must be a valid email address"
452        );
453    }
454
455    #[test]
456    fn validation_errors_to_map_fallback_message() {
457        let mut errors = validator::ValidationErrors::new();
458        // Create an error with no custom message
459        let error = validator::ValidationError::new("custom_code");
460        errors.add("my_field", error);
461
462        let map = validation_errors_to_map(&errors);
463
464        assert!(map.contains_key("my_field"));
465        assert_eq!(map["my_field"][0], "validation failed: custom_code");
466    }
467
468    #[tokio::test]
469    async fn valid_extractor_ok() {
470        use axum::body::Body;
471
472        #[derive(serde::Deserialize, validator::Validate)]
473        struct TestInput {
474            #[validate(length(min = 1))]
475            name: String,
476        }
477
478        let req = Request::builder()
479            .method("POST")
480            .header("content-type", "application/json")
481            .body(Body::from(r#"{"name": "Alice"}"#))
482            .unwrap();
483
484        let state = ();
485        let result = Valid::<axum::Json<TestInput>>::from_request(req, &state).await;
486
487        assert!(result.is_ok());
488        assert_eq!(result.unwrap().0.0.name, "Alice");
489    }
490
491    #[test]
492    // Mirrors the `(&MaybeValidate(&x)).autumn_maybe_validate()` form the
493    // repository macro emits: both traits in scope, explicit leading borrow.
494    #[allow(clippy::needless_borrow, unused_imports)]
495    fn maybe_validate_runs_for_validate_types() {
496        // Autoref specialization: a type implementing `Validate` resolves to
497        // the real validating branch and surfaces a 422 on failure.
498        use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
499
500        #[derive(validator::Validate)]
501        struct HasRules {
502            #[validate(length(min = 5))]
503            name: String,
504        }
505
506        let bad = HasRules {
507            name: "ab".to_string(),
508        };
509        let err = (&MaybeValidate(&bad))
510            .autumn_maybe_validate()
511            .expect_err("short name must fail validation");
512        assert_eq!(err.status(), axum::http::StatusCode::UNPROCESSABLE_ENTITY);
513
514        let good = HasRules {
515            name: "alice".to_string(),
516        };
517        assert!(
518            (&MaybeValidate(&good)).autumn_maybe_validate().is_ok(),
519            "valid input must pass"
520        );
521    }
522
523    #[test]
524    #[allow(clippy::needless_borrow, unused_imports)]
525    fn maybe_validate_survives_an_extra_borrow() {
526        // #2586: the repository insert path holds its payload as `&New*`. An
527        // extra borrow makes `T` a reference type, which reaches the validating
528        // arm only through `validator`'s blanket `impl<T: Validate> Validate
529        // for &T`. Both forms must validate — if that blanket impl ever goes,
530        // the wrapped-reference form silently starts accepting everything, and
531        // this is what catches it.
532        use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
533
534        #[derive(validator::Validate)]
535        struct HasRules {
536            #[validate(length(min = 5))]
537            name: String,
538        }
539
540        let owned = HasRules {
541            name: "ab".to_string(),
542        };
543        let payload: &HasRules = &owned;
544        assert!(
545            (&MaybeValidate(payload)).autumn_maybe_validate().is_err(),
546            "the emitted form must reach the validating branch"
547        );
548        assert!(
549            (&MaybeValidate(&payload)).autumn_maybe_validate().is_err(),
550            "a wrapped `&&New*` must not fall through to the no-op arm"
551        );
552    }
553
554    #[test]
555    #[allow(clippy::needless_borrow, unused_imports)]
556    fn maybe_validate_is_noop_for_non_validate_types() {
557        // Autoref specialization: a type that does NOT implement `Validate`
558        // falls through to the no-op branch and compiles + always succeeds.
559        use super::{MaybeValidate, MaybeValidateFallback as _, MaybeValidateViaValidator as _};
560
561        struct NoRules {
562            _name: String,
563        }
564
565        let value = NoRules {
566            _name: "anything".to_string(),
567        };
568        assert!(
569            (&MaybeValidate(&value)).autumn_maybe_validate().is_ok(),
570            "types without validation rules must be accepted unchanged"
571        );
572    }
573
574    #[tokio::test]
575    async fn valid_extractor_err() {
576        use axum::body::Body;
577
578        #[derive(serde::Deserialize, validator::Validate)]
579        struct TestInput {
580            #[validate(length(min = 5))]
581            name: String,
582        }
583
584        let req = Request::builder()
585            .method("POST")
586            .header("content-type", "application/json")
587            .body(Body::from(r#"{"name": "Bob"}"#)) // Too short
588            .unwrap();
589
590        let state = ();
591        let result = Valid::<axum::Json<TestInput>>::from_request(req, &state).await;
592
593        match result {
594            Ok(_) => panic!("Expected validation error"),
595            Err(response) => {
596                assert_eq!(
597                    response.status(),
598                    axum::http::StatusCode::UNPROCESSABLE_ENTITY
599                );
600            }
601        }
602    }
603}