Skip to main content

arcly_http/web/
validation.rs

1//! `Validated<T>` -- type-branded, deserialized-and-validated request payload.
2//!
3//! Unlike the `#[Body] dto: T` shorthand (which validates silently), `Validated<T>`
4//! makes the validation contract visible at the type level. Functions that only
5//! accept pre-validated inputs declare `Validated<T>` in their signature;
6//! the type system enforces the invariant across the call stack.
7//!
8//! ## Usage
9//!
10//! ```rust,ignore
11//! async fn create_user(ctx: RequestContext) -> Result<Json<User>, HttpException> {
12//!     let dto = Validated::<CreateUserDto>::from_body(&ctx)?;
13//!     service.create(dto.into_inner()).await
14//! }
15//! ```
16//!
17//! ## RFC 7807 Error Shape
18//!
19//! Deserialize failure  -> `400 Bad Request`  (`bad-request` problem type)
20//! Constraint violation -> `422 Unprocessable Entity` (`validation` problem type)
21//!                         with per-field `errors` array
22
23use std::ops::Deref;
24
25use serde::de::DeserializeOwned;
26use validator::Validate;
27
28use crate::web::context::RequestContext;
29use crate::web::error::{HttpException, Validation};
30use crate::web::extract::{extract_body_json, extract_query};
31
32// ─── The wrapper type ────────────────────────────────────────────────────────
33
34/// A payload `T` that has been deserialized **and** validated.
35///
36/// Construction is possible only through [`from_body`](Self::from_body) or
37/// [`from_query`](Self::from_query) (plus [`assume_valid`](Self::assume_valid)
38/// for test fixtures). All other callers receive a `T` provably free of
39/// constraint violations.
40#[derive(Debug, Clone)]
41pub struct Validated<T>(T);
42
43impl<T> Validated<T> {
44    /// Consume the wrapper, returning the validated inner value.
45    #[inline]
46    pub fn into_inner(self) -> T {
47        self.0
48    }
49
50    /// Borrow the validated inner value.
51    #[inline]
52    pub fn inner(&self) -> &T {
53        &self.0
54    }
55
56    /// Construct without validation -- use **only** in unit-test fixtures where
57    /// the value is already known good. Never call in handler code.
58    #[inline]
59    pub fn assume_valid(v: T) -> Self {
60        Self(v)
61    }
62}
63
64impl<T> Deref for Validated<T> {
65    type Target = T;
66    #[inline]
67    fn deref(&self) -> &T {
68        &self.0
69    }
70}
71
72// ─── Extraction from RequestContext ─────────────────────────────────────────
73
74impl<T: DeserializeOwned + Validate> Validated<T> {
75    /// Deserialize `T` from the JSON request body, then run `T::validate()`.
76    ///
77    /// Returns `Err(HttpException)` on either step, converting automatically
78    /// into the RFC 7807 problem shape:
79    ///
80    /// | Failure             | Status | `type` field     |
81    /// |---------------------|--------|------------------|
82    /// | Malformed JSON      | 400    | `bad-request`    |
83    /// | Constraint violated | 422    | `validation`     |
84    pub fn from_body(ctx: &RequestContext) -> Result<Self, HttpException> {
85        let v: T = extract_body_json(ctx)?;
86        v.validate()
87            .map_err(|e| HttpException::from(Validation::from(e)))?;
88        Ok(Self(v))
89    }
90
91    /// Deserialize `T` from the URL query string, then validate.
92    ///
93    /// Same error shape as [`from_body`](Self::from_body).
94    pub fn from_query(ctx: &RequestContext) -> Result<Self, HttpException> {
95        let v: T = extract_query(ctx)?;
96        v.validate()
97            .map_err(|e| HttpException::from(Validation::from(e)))?;
98        Ok(Self(v))
99    }
100}