Skip to main content

vld_poem/
lib.rs

1//! # vld-poem — Poem integration for `vld`
2//!
3//! Validation extractors for [Poem](https://docs.rs/poem). Validates request
4//! data against `vld` schemas and returns `422 Unprocessable Entity` with
5//! structured JSON errors on failure.
6//!
7//! # Extractors
8//!
9//! | Extractor | Source |
10//! |-----------|--------|
11//! | `VldJson<T>` | JSON body |
12//! | `VldQuery<T>` | Query string |
13//! | `VldPath<T>` | Path parameters |
14//! | `VldForm<T>` | Form body |
15//! | `VldHeaders<T>` | HTTP headers |
16//! | `VldCookie<T>` | Cookie values |
17
18use poem::error::ResponseError;
19use poem::http::StatusCode;
20use poem::{FromRequest, Request, RequestBody, Result};
21use std::fmt;
22use std::ops::{Deref, DerefMut};
23use vld::schema::VldParse;
24
25// ---------------------------------------------------------------------------
26// Error type
27// ---------------------------------------------------------------------------
28
29/// Validation error returned by vld-poem extractors.
30#[derive(Debug)]
31pub struct VldPoemError(pub serde_json::Value);
32
33impl fmt::Display for VldPoemError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(f, "{}", self.0)
36    }
37}
38
39impl std::error::Error for VldPoemError {}
40
41impl ResponseError for VldPoemError {
42    fn status(&self) -> StatusCode {
43        StatusCode::UNPROCESSABLE_ENTITY
44    }
45
46    fn as_response(&self) -> poem::Response {
47        poem::Response::builder()
48            .status(StatusCode::UNPROCESSABLE_ENTITY)
49            .content_type("application/json")
50            .body(serde_json::to_string(&self.0).unwrap_or_default())
51    }
52}
53
54// ---------------------------------------------------------------------------
55// VldJson<T>
56// ---------------------------------------------------------------------------
57
58/// Validated JSON body extractor for Poem.
59#[derive(Debug, Clone)]
60pub struct VldJson<T>(pub T);
61
62impl<T> Deref for VldJson<T> {
63    type Target = T;
64    fn deref(&self) -> &T {
65        &self.0
66    }
67}
68
69impl<T> DerefMut for VldJson<T> {
70    fn deref_mut(&mut self) -> &mut T {
71        &mut self.0
72    }
73}
74
75impl<'a, T: VldParse + Send + Sync + 'static> FromRequest<'a> for VldJson<T> {
76    async fn from_request(_req: &'a Request, body: &mut RequestBody) -> Result<Self> {
77        let bytes = body.take()?.into_bytes().await?;
78        let value: serde_json::Value = serde_json::from_slice(&bytes)
79            .map_err(|e| VldPoemError(format_json_parse_error(&e.to_string())))?;
80
81        T::vld_parse_value(&value)
82            .map(VldJson)
83            .map_err(|e| VldPoemError(format_vld_error(&e)).into())
84    }
85}
86
87// ---------------------------------------------------------------------------
88// VldQuery<T>
89// ---------------------------------------------------------------------------
90
91/// Validated query string extractor for Poem.
92#[derive(Debug, Clone)]
93pub struct VldQuery<T>(pub T);
94
95impl<T> Deref for VldQuery<T> {
96    type Target = T;
97    fn deref(&self) -> &T {
98        &self.0
99    }
100}
101
102impl<T> DerefMut for VldQuery<T> {
103    fn deref_mut(&mut self) -> &mut T {
104        &mut self.0
105    }
106}
107
108impl<'a, T: VldParse + Send + Sync + 'static> FromRequest<'a> for VldQuery<T> {
109    async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
110        let qs = req.uri().query().unwrap_or("");
111        let map = parse_query_to_json(qs);
112        let value = serde_json::Value::Object(map);
113
114        T::vld_parse_value(&value)
115            .map(VldQuery)
116            .map_err(|e| VldPoemError(format_vld_error(&e)).into())
117    }
118}
119
120// ---------------------------------------------------------------------------
121// VldForm<T>
122// ---------------------------------------------------------------------------
123
124/// Validated form body extractor for Poem.
125#[derive(Debug, Clone)]
126pub struct VldForm<T>(pub T);
127
128impl<T> Deref for VldForm<T> {
129    type Target = T;
130    fn deref(&self) -> &T {
131        &self.0
132    }
133}
134
135impl<T> DerefMut for VldForm<T> {
136    fn deref_mut(&mut self) -> &mut T {
137        &mut self.0
138    }
139}
140
141impl<'a, T: VldParse + Send + Sync + 'static> FromRequest<'a> for VldForm<T> {
142    async fn from_request(_req: &'a Request, body: &mut RequestBody) -> Result<Self> {
143        let bytes = body.take()?.into_bytes().await?;
144        let body_str = String::from_utf8(bytes.to_vec())
145            .map_err(|_| VldPoemError(vld_http_common::format_utf8_error()))?;
146
147        let map = parse_query_to_json(&body_str);
148        let value = serde_json::Value::Object(map);
149
150        T::vld_parse_value(&value)
151            .map(VldForm)
152            .map_err(|e| VldPoemError(format_vld_error(&e)).into())
153    }
154}
155
156// ---------------------------------------------------------------------------
157// Helpers
158// ---------------------------------------------------------------------------
159
160use vld_http_common::{
161    coerce_value, cookies_to_json, format_json_parse_error, format_vld_error,
162    parse_query_string as parse_query_to_json,
163};
164
165// ---------------------------------------------------------------------------
166// VldPath<T>
167// ---------------------------------------------------------------------------
168
169/// Validated path parameters extractor for Poem.
170///
171/// Path values are coerced: `"42"` → number, `"true"` → bool, etc.
172#[derive(Debug, Clone)]
173pub struct VldPath<T>(pub T);
174
175impl<T> Deref for VldPath<T> {
176    type Target = T;
177    fn deref(&self) -> &T {
178        &self.0
179    }
180}
181
182impl<T> DerefMut for VldPath<T> {
183    fn deref_mut(&mut self) -> &mut T {
184        &mut self.0
185    }
186}
187
188impl<'a, T: VldParse + Send + Sync + 'static> FromRequest<'a> for VldPath<T> {
189    async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
190        let params = req.params::<Vec<(String, String)>>().unwrap_or_default();
191
192        let mut map = serde_json::Map::new();
193        for (k, v) in &params {
194            map.insert(k.clone(), coerce_value(v));
195        }
196        let value = serde_json::Value::Object(map);
197
198        T::vld_parse_value(&value)
199            .map(VldPath)
200            .map_err(|e| VldPoemError(format_vld_error(&e)).into())
201    }
202}
203
204// ---------------------------------------------------------------------------
205// VldHeaders<T>
206// ---------------------------------------------------------------------------
207
208/// Validated HTTP headers extractor for Poem.
209///
210/// Header names are normalised to snake_case: `Content-Type` → `content_type`.
211/// Values are coerced: `"42"` → number, `"true"` → bool, etc.
212#[derive(Debug, Clone)]
213pub struct VldHeaders<T>(pub T);
214
215impl<T> Deref for VldHeaders<T> {
216    type Target = T;
217    fn deref(&self) -> &T {
218        &self.0
219    }
220}
221
222impl<T> DerefMut for VldHeaders<T> {
223    fn deref_mut(&mut self) -> &mut T {
224        &mut self.0
225    }
226}
227
228impl<'a, T: VldParse + Send + Sync + 'static> FromRequest<'a> for VldHeaders<T> {
229    async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
230        let mut map = serde_json::Map::new();
231        for (name, value) in req.headers().iter() {
232            let key = name.as_str().to_lowercase().replace('-', "_");
233            if let Ok(v) = value.to_str() {
234                map.insert(key, coerce_value(v));
235            }
236        }
237        let value = serde_json::Value::Object(map);
238
239        T::vld_parse_value(&value)
240            .map(VldHeaders)
241            .map_err(|e| VldPoemError(format_vld_error(&e)).into())
242    }
243}
244
245// ---------------------------------------------------------------------------
246// VldCookie<T>
247// ---------------------------------------------------------------------------
248
249/// Validated cookie extractor for Poem.
250///
251/// Reads cookies from the `Cookie` header and validates against the schema.
252#[derive(Debug, Clone)]
253pub struct VldCookie<T>(pub T);
254
255impl<T> Deref for VldCookie<T> {
256    type Target = T;
257    fn deref(&self) -> &T {
258        &self.0
259    }
260}
261
262impl<T> DerefMut for VldCookie<T> {
263    fn deref_mut(&mut self) -> &mut T {
264        &mut self.0
265    }
266}
267
268impl<'a, T: VldParse + Send + Sync + 'static> FromRequest<'a> for VldCookie<T> {
269    async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
270        let cookie_header = req
271            .headers()
272            .get("cookie")
273            .and_then(|v| v.to_str().ok())
274            .unwrap_or("");
275
276        let value = cookies_to_json(cookie_header);
277
278        T::vld_parse_value(&value)
279            .map(VldCookie)
280            .map_err(|e| VldPoemError(format_vld_error(&e)).into())
281    }
282}
283
284/// Prelude — import everything you need.
285pub mod prelude {
286    pub use crate::{VldCookie, VldForm, VldHeaders, VldJson, VldPath, VldQuery};
287    pub use vld::prelude::*;
288}