Skip to main content

vld_salvo/
lib.rs

1//! # vld-salvo — Salvo integration for `vld`
2//!
3//! Validation extractors for [Salvo](https://salvo.rs).
4//! All extractors implement [`Extractible`] and can be used directly as
5//! `#[handler]` function parameters — just like Salvo's built-in `JsonBody`
6//! or `PathParam`.
7//!
8//! | Extractor | Source |
9//! |-----------|--------|
10//! | [`VldJson<T>`] | JSON request body |
11//! | [`VldQuery<T>`] | URL query parameters |
12//! | [`VldForm<T>`] | URL-encoded form body |
13//! | [`VldPath<T>`] | Path parameters |
14//! | [`VldHeaders<T>`] | HTTP headers |
15//! | [`VldCookie<T>`] | Cookie values |
16//!
17//! All extractors return **422 Unprocessable Entity** on validation failure.
18//!
19//! # Quick Example
20//!
21//! ```rust,ignore
22//! use salvo::prelude::*;
23//! use vld_salvo::prelude::*;
24//!
25//! vld::schema! {
26//!     #[derive(Debug, Clone, serde::Serialize)]
27//!     pub struct CreateUser {
28//!         pub name: String  => vld::string().min(2),
29//!         pub email: String => vld::string().email(),
30//!     }
31//! }
32//!
33//! // VldJson<T> is used as a handler parameter — no manual extraction needed!
34//! #[handler]
35//! async fn create(body: VldJson<CreateUser>, res: &mut Response) {
36//!     res.render(Json(serde_json::json!({"name": body.name})));
37//! }
38//! ```
39
40use std::sync::OnceLock;
41
42use salvo::extract::metadata::{Metadata, Source, SourceFrom, SourceParser};
43use salvo::extract::Extractible;
44use salvo::http::StatusCode;
45use salvo::prelude::*;
46use vld::schema::VldParse;
47use vld_http_common::coerce_value;
48
49// ---------------------------------------------------------------------------
50// Error type
51// ---------------------------------------------------------------------------
52
53/// Error type for vld validation failures in Salvo handlers.
54///
55/// Implements [`Writer`] so it can be returned from `#[handler]` functions
56/// via `Result<T, VldSalvoError>`.
57///
58/// On write, renders a `422 Unprocessable Entity` JSON response using
59/// [`vld_http_common::format_vld_error`].
60#[derive(Debug)]
61pub struct VldSalvoError {
62    /// The underlying validation error.
63    pub error: vld::error::VldError,
64}
65
66impl VldSalvoError {
67    /// Create a new `VldSalvoError` from a [`VldError`](vld::error::VldError).
68    pub fn new(error: vld::error::VldError) -> Self {
69        Self { error }
70    }
71}
72
73impl std::fmt::Display for VldSalvoError {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "Validation failed: {}", self.error)
76    }
77}
78
79impl std::error::Error for VldSalvoError {}
80
81impl From<vld::error::VldError> for VldSalvoError {
82    fn from(error: vld::error::VldError) -> Self {
83        Self { error }
84    }
85}
86
87#[async_trait]
88impl Writer for VldSalvoError {
89    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
90        let body = vld_http_common::format_vld_error(&self.error);
91        res.status_code(StatusCode::UNPROCESSABLE_ENTITY);
92        res.render(Json(body));
93    }
94}
95
96// ---------------------------------------------------------------------------
97// Helper: build a parse-error VldSalvoError
98// ---------------------------------------------------------------------------
99
100fn parse_error(msg: impl std::fmt::Display) -> VldSalvoError {
101    VldSalvoError {
102        error: vld::error::VldError::single(vld::error::IssueCode::ParseError, msg.to_string()),
103    }
104}
105
106// ============================= VldJson =======================================
107
108/// Salvo extractor that validates **JSON request bodies**.
109///
110/// Use as a `#[handler]` parameter:
111///
112/// ```rust,ignore
113/// #[handler]
114/// async fn create(body: VldJson<CreateUser>, res: &mut Response) {
115///     // body.0 is the validated CreateUser
116///     res.render(Json(body.0));
117/// }
118/// ```
119pub struct VldJson<T>(pub T);
120
121impl<T> std::ops::Deref for VldJson<T> {
122    type Target = T;
123    fn deref(&self) -> &T {
124        &self.0
125    }
126}
127
128impl<T> std::ops::DerefMut for VldJson<T> {
129    fn deref_mut(&mut self) -> &mut T {
130        &mut self.0
131    }
132}
133
134impl<'ex, T: VldParse + Send> Extractible<'ex> for VldJson<T> {
135    fn metadata() -> &'static Metadata {
136        static META: OnceLock<Metadata> = OnceLock::new();
137        META.get_or_init(|| {
138            Metadata::new("VldJson")
139                .add_default_source(Source::new(SourceFrom::Body, SourceParser::Json))
140        })
141    }
142
143    async fn extract(
144        req: &'ex mut Request,
145        _depot: &'ex mut Depot,
146    ) -> Result<Self, impl Writer + Send + std::fmt::Debug + 'static> {
147        let value: serde_json::Value = req
148            .parse_json()
149            .await
150            .map_err(|e| parse_error(format_args!("Invalid JSON: {e}")))?;
151        T::vld_parse_value(&value)
152            .map(VldJson)
153            .map_err(VldSalvoError::from)
154    }
155}
156
157// ============================= VldQuery ======================================
158
159/// Salvo extractor that validates **URL query parameters**.
160///
161/// Values are coerced: `"42"` → number, `"true"`/`"false"` → boolean,
162/// empty → null.
163///
164/// ```rust,ignore
165/// #[handler]
166/// async fn search(q: VldQuery<SearchParams>, res: &mut Response) {
167///     // q.page, q.limit, ...
168/// }
169/// ```
170pub struct VldQuery<T>(pub T);
171
172impl<T> std::ops::Deref for VldQuery<T> {
173    type Target = T;
174    fn deref(&self) -> &T {
175        &self.0
176    }
177}
178
179impl<T> std::ops::DerefMut for VldQuery<T> {
180    fn deref_mut(&mut self) -> &mut T {
181        &mut self.0
182    }
183}
184
185impl<'ex, T: VldParse + Send> Extractible<'ex> for VldQuery<T> {
186    fn metadata() -> &'static Metadata {
187        static META: OnceLock<Metadata> = OnceLock::new();
188        META.get_or_init(|| {
189            Metadata::new("VldQuery")
190                .add_default_source(Source::new(SourceFrom::Query, SourceParser::MultiMap))
191        })
192    }
193
194    async fn extract(
195        req: &'ex mut Request,
196        _depot: &'ex mut Depot,
197    ) -> Result<Self, impl Writer + Send + std::fmt::Debug + 'static> {
198        let qs = req.uri().query().unwrap_or("");
199        let value = vld_http_common::query_string_to_json(qs);
200        T::vld_parse_value(&value)
201            .map(VldQuery)
202            .map_err(VldSalvoError::from)
203    }
204}
205
206// ============================= VldForm =======================================
207
208/// Salvo extractor that validates **URL-encoded form bodies**.
209///
210/// ```rust,ignore
211/// #[handler]
212/// async fn login(form: VldForm<LoginForm>, res: &mut Response) {
213///     // form.username, form.password
214/// }
215/// ```
216pub struct VldForm<T>(pub T);
217
218impl<T> std::ops::Deref for VldForm<T> {
219    type Target = T;
220    fn deref(&self) -> &T {
221        &self.0
222    }
223}
224
225impl<T> std::ops::DerefMut for VldForm<T> {
226    fn deref_mut(&mut self) -> &mut T {
227        &mut self.0
228    }
229}
230
231impl<'ex, T: VldParse + Send> Extractible<'ex> for VldForm<T> {
232    fn metadata() -> &'static Metadata {
233        static META: OnceLock<Metadata> = OnceLock::new();
234        META.get_or_init(|| {
235            Metadata::new("VldForm")
236                .add_default_source(Source::new(SourceFrom::Body, SourceParser::MultiMap))
237        })
238    }
239
240    async fn extract(
241        req: &'ex mut Request,
242        _depot: &'ex mut Depot,
243    ) -> Result<Self, impl Writer + Send + std::fmt::Debug + 'static> {
244        let body_str = req
245            .parse_body::<String>()
246            .await
247            .map_err(|e| parse_error(format_args!("Invalid form body: {e}")))?;
248        let map = vld_http_common::parse_query_string(&body_str);
249        let value = serde_json::Value::Object(map);
250        T::vld_parse_value(&value)
251            .map(VldForm)
252            .map_err(VldSalvoError::from)
253    }
254}
255
256// ============================= VldPath =======================================
257
258/// Salvo extractor that validates **path parameters**.
259///
260/// Path segment values are coerced (numbers, booleans, etc.).
261///
262/// ```rust,ignore
263/// // Route: /users/{id}
264/// vld::schema! {
265///     #[derive(Debug, Clone)]
266///     pub struct UserId {
267///         pub id: i64 => vld::number().int().min(1),
268///     }
269/// }
270///
271/// #[handler]
272/// async fn get_user(p: VldPath<UserId>, res: &mut Response) {
273///     // p.id
274/// }
275/// ```
276pub struct VldPath<T>(pub T);
277
278impl<T> std::ops::Deref for VldPath<T> {
279    type Target = T;
280    fn deref(&self) -> &T {
281        &self.0
282    }
283}
284
285impl<T> std::ops::DerefMut for VldPath<T> {
286    fn deref_mut(&mut self) -> &mut T {
287        &mut self.0
288    }
289}
290
291impl<'ex, T: VldParse + Send> Extractible<'ex> for VldPath<T> {
292    fn metadata() -> &'static Metadata {
293        static META: OnceLock<Metadata> = OnceLock::new();
294        META.get_or_init(|| {
295            Metadata::new("VldPath")
296                .add_default_source(Source::new(SourceFrom::Param, SourceParser::MultiMap))
297        })
298    }
299
300    async fn extract(
301        req: &'ex mut Request,
302        _depot: &'ex mut Depot,
303    ) -> Result<Self, impl Writer + Send + std::fmt::Debug + 'static> {
304        let mut map = serde_json::Map::new();
305        for (key, value) in req.params().iter() {
306            map.insert(key.clone(), coerce_value(value));
307        }
308        let value = serde_json::Value::Object(map);
309        T::vld_parse_value(&value)
310            .map(VldPath)
311            .map_err(VldSalvoError::from)
312    }
313}
314
315// ============================= VldHeaders ====================================
316
317/// Salvo extractor that validates **HTTP headers**.
318///
319/// Header names are normalised to snake_case: `Content-Type` → `content_type`.
320/// Values are coerced (numbers, booleans, etc.).
321///
322/// ```rust,ignore
323/// #[handler]
324/// async fn handler(h: VldHeaders<AuthHeaders>, res: &mut Response) {
325///     // h.authorization
326/// }
327/// ```
328pub struct VldHeaders<T>(pub T);
329
330impl<T> std::ops::Deref for VldHeaders<T> {
331    type Target = T;
332    fn deref(&self) -> &T {
333        &self.0
334    }
335}
336
337impl<T> std::ops::DerefMut for VldHeaders<T> {
338    fn deref_mut(&mut self) -> &mut T {
339        &mut self.0
340    }
341}
342
343impl<'ex, T: VldParse + Send> Extractible<'ex> for VldHeaders<T> {
344    fn metadata() -> &'static Metadata {
345        static META: OnceLock<Metadata> = OnceLock::new();
346        META.get_or_init(|| {
347            Metadata::new("VldHeaders")
348                .add_default_source(Source::new(SourceFrom::Header, SourceParser::MultiMap))
349        })
350    }
351
352    async fn extract(
353        req: &'ex mut Request,
354        _depot: &'ex mut Depot,
355    ) -> Result<Self, impl Writer + Send + std::fmt::Debug + 'static> {
356        let mut map = serde_json::Map::new();
357        for (name, value) in req.headers().iter() {
358            let key = name.as_str().to_lowercase().replace('-', "_");
359            if let Ok(v) = value.to_str() {
360                map.insert(key, coerce_value(v));
361            }
362        }
363        let value = serde_json::Value::Object(map);
364        T::vld_parse_value(&value)
365            .map(VldHeaders)
366            .map_err(VldSalvoError::from)
367    }
368}
369
370// ============================= VldCookie =====================================
371
372/// Salvo extractor that validates **cookie values** from the `Cookie` header.
373///
374/// ```rust,ignore
375/// #[handler]
376/// async fn dashboard(c: VldCookie<SessionCookies>, res: &mut Response) {
377///     // c.session_id
378/// }
379/// ```
380pub struct VldCookie<T>(pub T);
381
382impl<T> std::ops::Deref for VldCookie<T> {
383    type Target = T;
384    fn deref(&self) -> &T {
385        &self.0
386    }
387}
388
389impl<T> std::ops::DerefMut for VldCookie<T> {
390    fn deref_mut(&mut self) -> &mut T {
391        &mut self.0
392    }
393}
394
395impl<'ex, T: VldParse + Send> Extractible<'ex> for VldCookie<T> {
396    fn metadata() -> &'static Metadata {
397        static META: OnceLock<Metadata> = OnceLock::new();
398        META.get_or_init(|| {
399            Metadata::new("VldCookie")
400                .add_default_source(Source::new(SourceFrom::Cookie, SourceParser::MultiMap))
401        })
402    }
403
404    async fn extract(
405        req: &'ex mut Request,
406        _depot: &'ex mut Depot,
407    ) -> Result<Self, impl Writer + Send + std::fmt::Debug + 'static> {
408        let cookie_header = req
409            .headers()
410            .get("cookie")
411            .and_then(|v| v.to_str().ok())
412            .unwrap_or("");
413        let value = vld_http_common::cookies_to_json(cookie_header);
414        T::vld_parse_value(&value)
415            .map(VldCookie)
416            .map_err(VldSalvoError::from)
417    }
418}
419
420// ---------------------------------------------------------------------------
421// Prelude
422// ---------------------------------------------------------------------------
423
424/// Prelude — import everything you need.
425pub mod prelude {
426    pub use crate::{VldCookie, VldForm, VldHeaders, VldJson, VldPath, VldQuery, VldSalvoError};
427    pub use vld::prelude::*;
428}