Skip to main content

a3s_boot/
validation.rs

1use crate::{BootError, BootRequest, Result};
2use serde::de::DeserializeOwned;
3use serde::Serialize;
4use serde_json::Value;
5use std::collections::{BTreeMap, BTreeSet};
6use std::sync::Arc;
7
8/// DTO validation hook used by validating route helpers and controller macros.
9pub trait Validate {
10    fn validate(&self) -> Result<()> {
11        Ok(())
12    }
13}
14
15/// Field metadata used by Nest-style whitelist validation options.
16pub trait ValidationSchema {
17    fn allowed_fields() -> &'static [&'static str];
18}
19
20/// Options for route-level DTO validation.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub struct ValidationOptions {
23    pub transform: bool,
24    pub whitelist: bool,
25    pub forbid_non_whitelisted: bool,
26}
27
28impl ValidationOptions {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    pub fn whitelist(mut self, enabled: bool) -> Self {
34        self.whitelist = enabled;
35        self
36    }
37
38    pub fn transform(mut self, enabled: bool) -> Self {
39        self.transform = enabled;
40        self
41    }
42
43    pub fn forbid_non_whitelisted(mut self, enabled: bool) -> Self {
44        self.forbid_non_whitelisted = enabled;
45        self
46    }
47
48    pub fn merge(self, other: Self) -> Self {
49        Self {
50            transform: self.transform || other.transform,
51            whitelist: self.whitelist || other.whitelist,
52            forbid_non_whitelisted: self.forbid_non_whitelisted || other.forbid_non_whitelisted,
53        }
54    }
55
56    pub(crate) fn checks_unknown_fields(self) -> bool {
57        self.whitelist || self.forbid_non_whitelisted
58    }
59}
60
61pub(crate) type RequestValidator =
62    Arc<dyn Fn(BootRequest, ValidationOptions) -> Result<BootRequest> + Send + Sync>;
63
64pub(crate) fn body_validator<T>() -> RequestValidator
65where
66    T: DeserializeOwned + Validate + 'static,
67{
68    Arc::new(|request, _| {
69        request.require_json_content_type()?;
70        validate_value(request.json::<T>()?).map(|_| request)
71    })
72}
73
74pub(crate) fn body_validator_with_options<T>(options: ValidationOptions) -> RequestValidator
75where
76    T: DeserializeOwned + Serialize + Validate + ValidationSchema + 'static,
77{
78    Arc::new(move |mut request, inherited_options| {
79        let options = inherited_options.merge(options);
80        request.require_json_content_type()?;
81        let value = request.json::<Value>()?;
82        let value = validate_json_value_with_options::<T>(value, options, "body property")?;
83        if options.transform || options.whitelist {
84            let body =
85                serde_json::to_vec(&value).map_err(|err| BootError::Internal(err.to_string()))?;
86            rewrite_request_body(&mut request, body);
87        }
88        Ok(request)
89    })
90}
91
92pub(crate) fn params_validator<T>() -> RequestValidator
93where
94    T: DeserializeOwned + Validate + 'static,
95{
96    Arc::new(|request, _| validate_value(request.params::<T>()?).map(|_| request))
97}
98
99pub(crate) fn params_validator_with_options<T>(options: ValidationOptions) -> RequestValidator
100where
101    T: DeserializeOwned + Serialize + Validate + ValidationSchema + 'static,
102{
103    Arc::new(move |mut request, inherited_options| {
104        let options = inherited_options.merge(options);
105        apply_map_field_options::<T>(&mut request.params, options, "path parameter")?;
106        let value = validate_value(request.params::<T>()?)?;
107        if options.transform {
108            request.params = serialize_urlencoded_map(&value)?;
109        }
110        Ok(request)
111    })
112}
113
114pub(crate) fn query_validator<T>() -> RequestValidator
115where
116    T: DeserializeOwned + Validate + 'static,
117{
118    Arc::new(|request, _| validate_value(request.query::<T>()?).map(|_| request))
119}
120
121pub(crate) fn query_validator_with_options<T>(options: ValidationOptions) -> RequestValidator
122where
123    T: DeserializeOwned + Serialize + Validate + ValidationSchema + 'static,
124{
125    Arc::new(move |mut request, inherited_options| {
126        let options = inherited_options.merge(options);
127        let pairs = request.query_pairs()?;
128        apply_pair_field_options::<T>(&pairs, &mut request.query, options, "query parameter")?;
129        if options.whitelist {
130            request.query_string = None;
131        }
132        let value = validate_value(request.query::<T>()?)?;
133        if options.transform {
134            request.query = serialize_urlencoded_map(&value)?;
135            request.query_string = None;
136        }
137        Ok(request)
138    })
139}
140
141pub(crate) fn validate_value<T>(value: T) -> Result<T>
142where
143    T: Validate,
144{
145    value
146        .validate()
147        .map_err(|error| validation_bad_request(error, std::any::type_name::<T>()))?;
148    Ok(value)
149}
150
151fn validation_bad_request(error: BootError, type_name: &'static str) -> BootError {
152    match error {
153        BootError::BadRequest(message) => {
154            BootError::BadRequest(format!("validation failed for {type_name}: {message}"))
155        }
156        error => error,
157    }
158}
159
160pub(crate) fn validate_json_value_with_options<T>(
161    value: Value,
162    options: ValidationOptions,
163    label: &'static str,
164) -> Result<Value>
165where
166    T: DeserializeOwned + Serialize + Validate + ValidationSchema,
167{
168    let value = apply_json_field_options::<T>(value, options, label)?;
169    let typed = validate_value(
170        serde_json::from_value::<T>(value.clone())
171            .map_err(|err| BootError::BadRequest(err.to_string()))?,
172    )?;
173    if options.transform {
174        serde_json::to_value(&typed).map_err(|err| BootError::Internal(err.to_string()))
175    } else {
176        Ok(value)
177    }
178}
179
180fn apply_json_field_options<T>(
181    mut value: Value,
182    options: ValidationOptions,
183    label: &'static str,
184) -> Result<Value>
185where
186    T: ValidationSchema,
187{
188    if !options.checks_unknown_fields() {
189        return Ok(value);
190    }
191
192    let Some(object) = value.as_object_mut() else {
193        return Ok(value);
194    };
195    let allowed = allowed_fields::<T>();
196    let unknown = object
197        .keys()
198        .filter(|field| !allowed.contains(field.as_str()))
199        .cloned()
200        .collect::<Vec<_>>();
201    handle_unknown_fields(label, &unknown, options)?;
202    if options.whitelist {
203        object.retain(|field, _| allowed.contains(field.as_str()));
204    }
205    Ok(value)
206}
207
208fn apply_map_field_options<T>(
209    values: &mut BTreeMap<String, String>,
210    options: ValidationOptions,
211    label: &'static str,
212) -> Result<()>
213where
214    T: ValidationSchema,
215{
216    if !options.checks_unknown_fields() {
217        return Ok(());
218    }
219
220    let allowed = allowed_fields::<T>();
221    let unknown = values
222        .keys()
223        .filter(|field| !allowed.contains(field.as_str()))
224        .cloned()
225        .collect::<Vec<_>>();
226    handle_unknown_fields(label, &unknown, options)?;
227    if options.whitelist {
228        values.retain(|field, _| allowed.contains(field.as_str()));
229    }
230    Ok(())
231}
232
233fn apply_pair_field_options<T>(
234    pairs: &[(String, String)],
235    values: &mut BTreeMap<String, String>,
236    options: ValidationOptions,
237    label: &'static str,
238) -> Result<()>
239where
240    T: ValidationSchema,
241{
242    if !options.checks_unknown_fields() {
243        return Ok(());
244    }
245
246    let allowed = allowed_fields::<T>();
247    let unknown = pairs
248        .iter()
249        .map(|(field, _)| field)
250        .filter(|field| !allowed.contains(field.as_str()))
251        .cloned()
252        .collect::<BTreeSet<_>>()
253        .into_iter()
254        .collect::<Vec<_>>();
255    handle_unknown_fields(label, &unknown, options)?;
256    if options.whitelist {
257        values.retain(|field, _| allowed.contains(field.as_str()));
258    }
259    Ok(())
260}
261
262fn allowed_fields<T>() -> BTreeSet<&'static str>
263where
264    T: ValidationSchema,
265{
266    T::allowed_fields().iter().copied().collect()
267}
268
269fn handle_unknown_fields(
270    label: &'static str,
271    unknown: &[String],
272    options: ValidationOptions,
273) -> Result<()> {
274    if unknown.is_empty() || !options.forbid_non_whitelisted {
275        return Ok(());
276    }
277
278    let fields = unknown.join(", ");
279    Err(BootError::BadRequest(format!(
280        "non-whitelisted {}: {fields}",
281        plural_label(label)
282    )))
283}
284
285fn plural_label(label: &'static str) -> &'static str {
286    match label {
287        "body property" => "body properties",
288        "message property" => "message properties",
289        "path parameter" => "path parameters",
290        "query parameter" => "query parameters",
291        value => value,
292    }
293}
294
295fn rewrite_request_body(request: &mut BootRequest, body: Vec<u8>) {
296    let body_len = body.len();
297    request.body = body;
298    if request.header("content-length").is_some() {
299        request
300            .headers
301            .insert("content-length".to_string(), body_len.to_string());
302        request
303            .appended_headers
304            .retain(|(name, _)| !name.eq_ignore_ascii_case("content-length"));
305    }
306}
307
308fn serialize_urlencoded_map<T>(value: &T) -> Result<BTreeMap<String, String>>
309where
310    T: Serialize,
311{
312    let encoded =
313        serde_urlencoded::to_string(value).map_err(|err| BootError::Internal(err.to_string()))?;
314    if encoded.is_empty() {
315        return Ok(BTreeMap::new());
316    }
317
318    serde_urlencoded::from_str(&encoded).map_err(|err| BootError::Internal(err.to_string()))
319}