1use crate::context::{Extra, ModelConfig, ValidationContext};
7use crate::error::FieldError;
8use crate::rules;
9use crate::validate::{Validate, json_type};
10use regex::Regex;
11use serde::de::DeserializeOwned;
12use serde_json::{Number, Value};
13
14#[derive(Debug, Clone, Copy)]
16pub struct FieldSpec {
17 pub key: &'static str,
19 pub aliases: &'static [&'static str],
23 pub required: bool,
25}
26
27impl FieldSpec {
28 pub fn input_name(&self) -> &'static str {
31 self.aliases.first().copied().unwrap_or(self.key)
32 }
33}
34
35pub fn prepare_object(
39 input: &mut Value,
40 ctx: &mut ValidationContext,
41 config: ModelConfig,
42 specs: &[FieldSpec],
43 mut field: impl FnMut(usize, &mut Value, &mut ValidationContext),
44) {
45 ctx.with_config(config, |ctx| {
46 let Value::Object(map) = input else {
47 let got = json_type(input);
48 ctx.error("model_type", format!("expected an object, got {got}"));
49 return;
50 };
51 for (index, spec) in specs.iter().enumerate() {
52 let mut names = std::iter::once(spec.key)
56 .chain(spec.aliases.iter().copied())
57 .filter(|name| map.contains_key(*name));
58 let sent_as = names.next().unwrap_or(spec.key);
59 for duplicate in names.collect::<Vec<_>>() {
60 map.remove(duplicate);
61 ctx.at(duplicate, |ctx| {
62 ctx.error(
63 "duplicate_field",
64 format!("`{sent_as}` and `{duplicate}` both set this field"),
65 );
66 });
67 }
68 if sent_as != spec.key
69 && let Some(value) = map.remove(sent_as)
70 {
71 map.insert(spec.key.to_owned(), value);
72 }
73 match map.get_mut(spec.key) {
74 Some(slot) => ctx.at(sent_as, |ctx| field(index, slot, ctx)),
75 None if spec.required => {
76 ctx.at(spec.input_name(), |ctx| {
77 ctx.error("missing", "field required")
78 });
79 }
80 None => {}
81 }
82 }
83 let known = |k: &str| specs.iter().any(|s| s.key == k || s.aliases.contains(&k));
84 let extras: Vec<String> = map.keys().filter(|k| !known(k)).cloned().collect();
85 for key in extras {
86 if config.extra == Extra::Forbid {
87 ctx.at(key.as_str(), |ctx| {
88 ctx.error("extra_forbidden", "extra inputs are not permitted")
89 });
90 } else {
91 map.remove(&key);
93 }
94 }
95 });
96}
97
98#[derive(Debug, Clone, Copy)]
103pub enum Constraint<'a> {
104 MinLength(usize),
106 MaxLength(usize),
108 Pattern(Option<&'a Regex>),
110 Email,
112 Url,
114 Gt(&'a Number),
116 Ge(&'a Number),
118 Lt(&'a Number),
120 Le(&'a Number),
122 MultipleOf(&'a Number),
124}
125
126fn cmp_numbers(a: &Number, b: &Number) -> Option<std::cmp::Ordering> {
128 match (
129 a.as_i64().map(i128::from).or(a.as_u64().map(i128::from)),
130 b.as_i64().map(i128::from).or(b.as_u64().map(i128::from)),
131 ) {
132 (Some(x), Some(y)) => Some(x.cmp(&y)),
133 _ => a.as_f64()?.partial_cmp(&b.as_f64()?),
134 }
135}
136
137pub fn check(value: &Value, constraint: Constraint<'_>, ctx: &mut ValidationContext) {
139 use std::cmp::Ordering::{Equal, Greater, Less};
140 let len = match value {
141 Value::String(s) => Some(s.chars().count()),
142 Value::Array(a) => Some(a.len()),
143 Value::Object(o) => Some(o.len()),
144 _ => None,
145 };
146 let result = match (constraint, value) {
147 (_, Value::Null) => Ok(()),
148 (Constraint::MinLength(min), _) => match len {
149 Some(n) if n < min => Err(FieldError::new(
150 "too_short",
151 format!("should have at least {min} items/characters"),
152 )),
153 _ => Ok(()),
154 },
155 (Constraint::MaxLength(max), _) => match len {
156 Some(n) if n > max => Err(FieldError::new(
157 "too_long",
158 format!("should have at most {max} items/characters"),
159 )),
160 _ => Ok(()),
161 },
162 (Constraint::Pattern(Some(re)), Value::String(s)) => rules::pattern(s, re),
163 (Constraint::Pattern(None), _) => Err(FieldError::new(
164 "pattern_invalid",
165 "the field pattern is not a valid regex",
166 )),
167 (Constraint::Email, Value::String(s)) => rules::email(s),
168 (Constraint::Url, Value::String(s)) => rules::parse_url(s).map(drop),
169 (c, Value::Number(n)) => {
170 let bound =
171 |limit: &Number, ok: &[std::cmp::Ordering], code: &'static str, op: &str| {
172 match cmp_numbers(n, limit) {
173 Some(o) if ok.contains(&o) => Ok(()),
174 _ => Err(FieldError::new(code, format!("should be {op} {limit}"))),
175 }
176 };
177 match c {
178 Constraint::Gt(l) => bound(l, &[Greater], "greater_than", ">"),
179 Constraint::Ge(l) => bound(l, &[Greater, Equal], "greater_than_equal", ">="),
180 Constraint::Lt(l) => bound(l, &[Less], "less_than", "<"),
181 Constraint::Le(l) => bound(l, &[Less, Equal], "less_than_equal", "<="),
182 Constraint::MultipleOf(m) => match (n.as_i64(), m.as_i64()) {
183 (Some(x), Some(y)) => rules::multiple_of(x, y),
184 _ => match (n.as_f64(), m.as_f64()) {
185 (Some(x), Some(y))
186 if y != 0.0 && ((x / y) - (x / y).round()).abs() < 1e-9 =>
187 {
188 Ok(())
189 }
190 _ => Err(FieldError::new(
191 "multiple_of",
192 format!("should be a multiple of {m}"),
193 )),
194 },
195 },
196 _ => Ok(()),
197 }
198 }
199 _ => Ok(()),
200 };
201 ctx.check(result);
202}
203
204#[doc(hidden)]
211pub fn prepare_agrees_with_serde<T>(inputs: &[Value]) -> Result<(), String>
212where
213 T: DeserializeOwned + Validate,
214{
215 for input in inputs {
216 let mut prepared = input.clone();
217 let mut ctx = ValidationContext::new();
218 T::prepare(&mut prepared, &mut ctx);
219 let prepare_ok = !ctx.has_errors();
220 let serde_ok = serde_json::from_value::<T>(prepared.clone()).is_ok();
221 if prepare_ok && !serde_ok {
222 return Err(format!(
223 "prepare accepted but serde rejected: {input} (prepared {prepared})"
224 ));
225 }
226 }
227 Ok(())
228}
229
230#[cfg(test)]
231#[allow(clippy::needless_borrow)]
232mod tests {
233 use super::*;
254 use crate::context::ModelConfig;
255 #[allow(unused_imports)]
256 use crate::hooks::{ModelHooks, Probe, ViaDefault as _, ViaHooks as _};
257 use serde::Deserialize;
258 use serde_json::json;
259
260 #[derive(Debug, Deserialize)]
261 #[serde(rename_all = "camelCase")]
262 struct Signup {
263 user_name: String,
264 age: Option<u8>,
265 tags: Vec<String>,
266 }
267
268 const CONFIG: ModelConfig = ModelConfig {
269 extra: Extra::Forbid,
270 str_strip_whitespace: true,
271 ..ModelConfig::DEFAULT
272 };
273 const FIELDS: &[FieldSpec] = &[
274 FieldSpec {
275 key: "userName",
276 aliases: &["login"],
277 required: true,
278 },
279 FieldSpec {
280 key: "age",
281 aliases: &[],
282 required: false,
283 },
284 FieldSpec {
285 key: "tags",
286 aliases: &[],
287 required: true,
288 },
289 ];
290
291 impl Validate for Signup {
292 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
293 (&&Probe::<Self>::new()).before_model(input, ctx);
294 prepare_object(input, ctx, CONFIG, FIELDS, |index, slot, ctx| match index {
295 0 => {
296 (&&Probe::<Self>::new()).before_field("user_name", slot, ctx);
297 let before = ctx.error_count();
298 <String as Validate>::prepare(slot, ctx);
299 if ctx.error_count() == before {
300 check(slot, Constraint::MinLength(2), ctx);
301 }
302 }
303 1 => {
304 (&&Probe::<Self>::new()).before_field("age", slot, ctx);
305 let before = ctx.error_count();
306 <Option<u8> as Validate>::prepare(slot, ctx);
307 if ctx.error_count() == before {
308 check(slot, Constraint::Ge(&Number::from(18)), ctx);
309 }
310 }
311 2 => {
312 (&&Probe::<Self>::new()).before_field("tags", slot, ctx);
313 <Vec<String> as Validate>::prepare(slot, ctx);
314 }
315 _ => {}
316 });
317 }
318
319 fn validate(&self, ctx: &mut ValidationContext) {
320 ctx.with_config(CONFIG, |ctx| {
321 ctx.at("userName", |ctx| self.user_name.validate(ctx));
322 ctx.at("age", |ctx| self.age.validate(ctx));
323 ctx.at("tags", |ctx| self.tags.validate(ctx));
324 (&&Probe::<Self>::new()).after_fields(self, ctx);
325 (&&Probe::<Self>::new()).after_model(self, ctx);
326 });
327 }
328 }
329
330 impl ModelHooks for Signup {
331 fn after_model(&self, ctx: &mut ValidationContext) {
332 if self.user_name == "admin" {
333 ctx.error("reserved", "admin is reserved");
334 }
335 }
336 }
337
338 fn codes(input: Value) -> Vec<(String, String)> {
339 match crate::parse_value::<Signup>(input, ValidationContext::new().at_root("body")) {
340 Ok(_) => vec![],
341 Err(e) => e
342 .errors
343 .into_iter()
344 .map(|e| {
345 (
346 e.location
347 .iter()
348 .map(ToString::to_string)
349 .collect::<Vec<_>>()
350 .join("."),
351 e.code.into_owned(),
352 )
353 })
354 .collect(),
355 }
356 }
357
358 #[test]
359 fn valid_input_with_alias_and_coercion() {
360 let s: Signup = crate::parse_value(
361 json!({"login": " bob ", "age": "30", "tags": []}),
362 ValidationContext::new(),
363 )
364 .unwrap();
365 assert_eq!(s.user_name, "bob");
366 assert_eq!(s.age, Some(30));
367 }
368
369 #[test]
370 fn every_error_is_reported_with_input_locations() {
371 let got = codes(json!({"login": "b", "age": 10, "extra": 1}));
372 assert_eq!(
373 got,
374 vec![
375 ("body.login".into(), "too_short".into()),
376 ("body.age".into(), "greater_than_equal".into()),
377 ("body.tags".into(), "missing".into()),
378 ("body.extra".into(), "extra_forbidden".into()),
379 ]
380 );
381 }
382
383 #[test]
384 fn key_and_alias_together_are_a_duplicate() {
385 assert_eq!(
386 codes(json!({"userName": "ab", "login": "cd", "tags": []})),
387 vec![("body.login".into(), "duplicate_field".into())]
388 );
389 }
390
391 #[test]
392 fn model_validator_runs_after_fields() {
393 assert_eq!(
394 codes(json!({"userName": "admin", "tags": []})),
395 vec![("body".into(), "reserved".into())]
396 );
397 }
398
399 #[test]
400 fn non_object_input() {
401 assert_eq!(
402 codes(json!([1])),
403 vec![("body".into(), "model_type".into())]
404 );
405 }
406
407 #[test]
408 fn prepare_and_serde_agree() {
409 let inputs = [
410 json!({}),
411 json!(null),
412 json!({"userName": "ab", "tags": ["x"]}),
413 json!({"userName": 5, "tags": "x"}),
414 json!({"userName": "ab", "tags": [1]}),
415 json!({"login": "ab", "userName": "cd", "tags": []}),
416 json!({"userName": "ab", "age": 300, "tags": []}),
417 json!({"userName": "ab", "age": null, "tags": []}),
418 json!({"userName": "ab", "age": "19", "tags": []}),
419 ];
420 prepare_agrees_with_serde::<Signup>(&inputs).unwrap();
421 }
422}