Skip to main content

vld_ntex/
lib.rs

1//! # vld-ntex — ntex integration for the `vld` validation library
2//!
3//! Provides extractors that validate request data using `vld` schemas:
4//!
5//! | Extractor | Replaces | Source |
6//! |---|---|---|
7//! | [`VldJson<T>`] | `ntex::web::types::Json<T>` | JSON request body |
8//! | [`VldQuery<T>`] | `ntex::web::types::Query<T>` | URL query parameters |
9//! | [`VldPath<T>`] | `ntex::web::types::Path<T>` | URL path parameters |
10//! | [`VldForm<T>`] | `ntex::web::types::Form<T>` | URL-encoded form body |
11//! | [`VldHeaders<T>`] | manual header extraction | HTTP headers |
12//! | [`VldCookie<T>`] | manual cookie parsing | Cookie values |
13//!
14//! All extractors return **422 Unprocessable Entity** on validation failure.
15//!
16//! # Quick example
17//!
18//! ```ignore
19//! use ntex::web::{self, App, HttpResponse};
20//! use vld::prelude::*;
21//! use vld_ntex::{VldJson, VldQuery, VldPath, VldHeaders};
22//!
23//! vld::schema! {
24//!     #[derive(Debug)]
25//!     pub struct PathParams {
26//!         pub id: i64 => vld::number().int().min(1),
27//!     }
28//! }
29//!
30//! vld::schema! {
31//!     #[derive(Debug)]
32//!     pub struct Body {
33//!         pub name: String => vld::string().min(2),
34//!     }
35//! }
36//!
37//! async fn handler(
38//!     path: VldPath<PathParams>,
39//!     body: VldJson<Body>,
40//! ) -> HttpResponse {
41//!     HttpResponse::Ok().body(format!("id={} name={}", path.id, body.name))
42//! }
43//! ```
44
45use ntex::http::StatusCode;
46use ntex::web::error::WebResponseError;
47use ntex::web::{ErrorRenderer, FromRequest, HttpRequest, HttpResponse};
48use std::fmt;
49
50// ============================= Error / Rejection =============================
51
52/// Error type returned when validation fails.
53///
54/// Used by all `Vld*` extractors in this crate.
55#[derive(Debug)]
56pub struct VldNtexError {
57    error: vld::error::VldError,
58}
59
60impl fmt::Display for VldNtexError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "Validation failed: {}", self.error)
63    }
64}
65
66impl<Err: ErrorRenderer> WebResponseError<Err> for VldNtexError {
67    fn status_code(&self) -> StatusCode {
68        StatusCode::UNPROCESSABLE_ENTITY
69    }
70
71    fn error_response(&self, _: &HttpRequest) -> HttpResponse {
72        let body = vld_http_common::format_vld_error(&self.error);
73
74        HttpResponse::build(StatusCode::UNPROCESSABLE_ENTITY)
75            .header("content-type", "application/json")
76            .body(body.to_string())
77    }
78}
79
80// ============================= VldJson =======================================
81
82/// ntex extractor that validates **JSON request bodies**.
83///
84/// Drop-in replacement for `ntex::web::types::Json<T>`.
85pub struct VldJson<T>(pub T);
86
87impl<T> std::ops::Deref for VldJson<T> {
88    type Target = T;
89    fn deref(&self) -> &T {
90        &self.0
91    }
92}
93
94impl<T> std::ops::DerefMut for VldJson<T> {
95    fn deref_mut(&mut self) -> &mut T {
96        &mut self.0
97    }
98}
99
100impl<T: vld::schema::VldParse, Err: ErrorRenderer> FromRequest<Err> for VldJson<T> {
101    type Error = VldNtexError;
102
103    async fn from_request(
104        req: &HttpRequest,
105        payload: &mut ntex::http::Payload,
106    ) -> Result<Self, Self::Error> {
107        let json_value =
108            <ntex::web::types::Json<serde_json::Value> as FromRequest<Err>>::from_request(
109                req, payload,
110            )
111            .await
112            .map_err(|e| VldNtexError {
113                error: vld::error::VldError::single(
114                    vld::error::IssueCode::ParseError,
115                    format!("JSON parse error: {}", e),
116                ),
117            })?;
118
119        let parsed = T::vld_parse_value(&json_value).map_err(|error| VldNtexError { error })?;
120
121        Ok(VldJson(parsed))
122    }
123}
124
125// ============================= VldQuery ======================================
126
127/// ntex extractor that validates **URL query parameters**.
128///
129/// Drop-in replacement for `ntex::web::types::Query<T>`.
130///
131/// Values are coerced: `"42"` -> number, `"true"`/`"false"` -> boolean, empty -> null.
132pub struct VldQuery<T>(pub T);
133
134impl<T> std::ops::Deref for VldQuery<T> {
135    type Target = T;
136    fn deref(&self) -> &T {
137        &self.0
138    }
139}
140
141impl<T> std::ops::DerefMut for VldQuery<T> {
142    fn deref_mut(&mut self) -> &mut T {
143        &mut self.0
144    }
145}
146
147impl<T: vld::schema::VldParse, Err: ErrorRenderer> FromRequest<Err> for VldQuery<T> {
148    type Error = VldNtexError;
149
150    async fn from_request(
151        req: &HttpRequest,
152        _payload: &mut ntex::http::Payload,
153    ) -> Result<Self, Self::Error> {
154        let query_string = req.query_string();
155        let value = query_string_to_json(query_string);
156        let parsed = T::vld_parse_value(&value).map_err(|error| VldNtexError { error })?;
157        Ok(VldQuery(parsed))
158    }
159}
160
161// ============================= VldPath =======================================
162
163/// ntex extractor that validates **URL path parameters**.
164///
165/// Drop-in replacement for `ntex::web::types::Path<T>`.
166///
167/// Path segment values are coerced the same way as query parameters.
168pub struct VldPath<T>(pub T);
169
170impl<T> std::ops::Deref for VldPath<T> {
171    type Target = T;
172    fn deref(&self) -> &T {
173        &self.0
174    }
175}
176
177impl<T> std::ops::DerefMut for VldPath<T> {
178    fn deref_mut(&mut self) -> &mut T {
179        &mut self.0
180    }
181}
182
183impl<T: vld::schema::VldParse, Err: ErrorRenderer> FromRequest<Err> for VldPath<T> {
184    type Error = VldNtexError;
185
186    async fn from_request(
187        req: &HttpRequest,
188        _payload: &mut ntex::http::Payload,
189    ) -> Result<Self, Self::Error> {
190        let mut map = serde_json::Map::new();
191        for (name, value) in req.match_info().iter() {
192            map.insert(name.to_owned(), coerce_value(value));
193        }
194
195        let value = serde_json::Value::Object(map);
196        let parsed = T::vld_parse_value(&value).map_err(|error| VldNtexError { error })?;
197        Ok(VldPath(parsed))
198    }
199}
200
201// ============================= VldForm =======================================
202
203/// ntex extractor that validates **URL-encoded form bodies**
204/// (`application/x-www-form-urlencoded`).
205///
206/// Drop-in replacement for `ntex::web::types::Form<T>`.
207///
208/// Values are coerced the same way as query parameters.
209pub struct VldForm<T>(pub T);
210
211impl<T> std::ops::Deref for VldForm<T> {
212    type Target = T;
213    fn deref(&self) -> &T {
214        &self.0
215    }
216}
217
218impl<T> std::ops::DerefMut for VldForm<T> {
219    fn deref_mut(&mut self) -> &mut T {
220        &mut self.0
221    }
222}
223
224impl<T: vld::schema::VldParse, Err: ErrorRenderer> FromRequest<Err> for VldForm<T> {
225    type Error = VldNtexError;
226
227    async fn from_request(
228        req: &HttpRequest,
229        payload: &mut ntex::http::Payload,
230    ) -> Result<Self, Self::Error> {
231        let bytes = <ntex::util::Bytes as FromRequest<Err>>::from_request(req, payload)
232            .await
233            .map_err(|e| VldNtexError {
234                error: vld::error::VldError::single(
235                    vld::error::IssueCode::ParseError,
236                    format!("Failed to read form body: {}", e),
237                ),
238            })?;
239
240        let body_bytes: &[u8] = &bytes;
241        let body_str = std::str::from_utf8(body_bytes).map_err(|_| VldNtexError {
242            error: vld::error::VldError::single(
243                vld::error::IssueCode::ParseError,
244                "Form body is not valid UTF-8",
245            ),
246        })?;
247
248        let value = query_string_to_json(body_str);
249        let parsed = T::vld_parse_value(&value).map_err(|error| VldNtexError { error })?;
250        Ok(VldForm(parsed))
251    }
252}
253
254// ============================= VldHeaders ====================================
255
256/// ntex extractor that validates **HTTP headers**.
257///
258/// Header names are normalised to snake_case for schema matching:
259/// `Content-Type` -> `content_type`, `X-Request-Id` -> `x_request_id`.
260///
261/// Values are coerced: `"42"` -> number, `"true"` -> boolean, etc.
262pub struct VldHeaders<T>(pub T);
263
264impl<T> std::ops::Deref for VldHeaders<T> {
265    type Target = T;
266    fn deref(&self) -> &T {
267        &self.0
268    }
269}
270
271impl<T> std::ops::DerefMut for VldHeaders<T> {
272    fn deref_mut(&mut self) -> &mut T {
273        &mut self.0
274    }
275}
276
277impl<T: vld::schema::VldParse, Err: ErrorRenderer> FromRequest<Err> for VldHeaders<T> {
278    type Error = VldNtexError;
279
280    async fn from_request(
281        req: &HttpRequest,
282        _payload: &mut ntex::http::Payload,
283    ) -> Result<Self, Self::Error> {
284        let value = headers_to_json(req.headers());
285        let parsed = T::vld_parse_value(&value).map_err(|error| VldNtexError { error })?;
286        Ok(VldHeaders(parsed))
287    }
288}
289
290// ============================= VldCookie =====================================
291
292/// ntex extractor that validates **cookie values** from the `Cookie` header.
293///
294/// Cookie names are used as-is for schema field matching.
295/// Values are coerced the same way as query parameters.
296pub struct VldCookie<T>(pub T);
297
298impl<T> std::ops::Deref for VldCookie<T> {
299    type Target = T;
300    fn deref(&self) -> &T {
301        &self.0
302    }
303}
304
305impl<T> std::ops::DerefMut for VldCookie<T> {
306    fn deref_mut(&mut self) -> &mut T {
307        &mut self.0
308    }
309}
310
311impl<T: vld::schema::VldParse, Err: ErrorRenderer> FromRequest<Err> for VldCookie<T> {
312    type Error = VldNtexError;
313
314    async fn from_request(
315        req: &HttpRequest,
316        _payload: &mut ntex::http::Payload,
317    ) -> Result<Self, Self::Error> {
318        let cookie_header = req
319            .headers()
320            .get(ntex::http::header::COOKIE)
321            .and_then(|v| v.to_str().ok())
322            .unwrap_or("");
323
324        let value = cookies_to_json(cookie_header);
325        let parsed = T::vld_parse_value(&value).map_err(|error| VldNtexError { error })?;
326        Ok(VldCookie(parsed))
327    }
328}
329
330// ========================= Helper functions ==================================
331
332use vld_http_common::{coerce_value, cookies_to_json, query_string_to_json};
333
334fn headers_to_json(headers: &ntex::http::HeaderMap) -> serde_json::Value {
335    let mut map = serde_json::Map::new();
336
337    for (name, value) in headers.iter() {
338        let key = name.as_str().replace('-', "_");
339        if let Ok(v) = value.to_str() {
340            map.insert(key, coerce_value(v));
341        }
342    }
343
344    serde_json::Value::Object(map)
345}
346
347/// Prelude — import everything you need.
348pub mod prelude {
349    pub use crate::{VldCookie, VldForm, VldHeaders, VldJson, VldNtexError, VldPath, VldQuery};
350    pub use vld::prelude::*;
351}