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}