1use crate::context::ValidationContext;
17use crate::error::LocationItem;
18use crate::rules;
19use crate::types::{BoundedI64, ConstrainedString, Email, PositiveInt, SecretString};
20use serde_json::{Map, Number, Value};
21use std::collections::{BTreeMap, HashMap};
22
23pub trait Validate {
28 fn prepare(_input: &mut Value, _ctx: &mut ValidationContext) {}
30
31 fn validate(&self, _ctx: &mut ValidationContext) {}
33}
34
35pub(crate) fn json_type(value: &Value) -> &'static str {
37 match value {
38 Value::Null => "null",
39 Value::Bool(_) => "boolean",
40 Value::Number(_) => "number",
41 Value::String(_) => "string",
42 Value::Array(_) => "array",
43 Value::Object(_) => "object",
44 }
45}
46
47fn type_error(ctx: &mut ValidationContext, code: &'static str, expected: &str, got: &Value) {
48 ctx.error(code, format!("expected {expected}, got {}", json_type(got)));
49}
50
51fn prepare_int(input: &mut Value, ctx: &mut ValidationContext, min: i128, max: i128) {
53 let candidate: Option<i128> = match &*input {
54 Value::Number(n) => {
55 if let Some(i) = n.as_i64() {
56 Some(i128::from(i))
57 } else if let Some(u) = n.as_u64() {
58 Some(i128::from(u))
59 } else if let Some(f) = n.as_f64() {
60 if ctx.is_strict() || f.fract() != 0.0 || !f.is_finite() {
61 ctx.error(
62 "int_from_float",
63 "expected an integer, got a number with a fractional part",
64 );
65 return;
66 }
67 #[allow(clippy::cast_possible_truncation)]
69 Some(f as i128)
70 } else {
71 None
72 }
73 }
74 Value::String(s) if ctx.allows_text_coercion() => match s.trim().parse::<i128>() {
75 Ok(i) => Some(i),
76 Err(_) => {
77 ctx.error("int_parsing", "input is not a valid integer");
78 return;
79 }
80 },
81 other => {
82 let other = other.clone();
83 type_error(ctx, "int_type", "an integer", &other);
84 return;
85 }
86 };
87 let Some(value) = candidate else {
88 ctx.error("int_parsing", "input is not a valid integer");
89 return;
90 };
91 if value < min || value > max {
92 ctx.error(
93 "int_out_of_range",
94 format!("integer must be between {min} and {max}"),
95 );
96 return;
97 }
98 *input = if value < 0 {
99 i64::try_from(value).map_or(Value::Null, |v| Value::Number(v.into()))
100 } else {
101 u64::try_from(value).map_or(Value::Null, |v| Value::Number(v.into()))
102 };
103}
104
105macro_rules! int_validate {
106 ($($t:ty),*) => {$(
107 impl Validate for $t {
108 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
109 prepare_int(input, ctx, i128::from(<$t>::MIN), i128::from(<$t>::MAX));
110 }
111 }
112 )*};
113}
114int_validate!(i8, i16, i32, i64, u8, u16, u32, u64);
115
116macro_rules! wide_int_validate {
119 ($($t:ty => $min:expr, $max:expr);* $(;)?) => {$(
120 impl Validate for $t {
121 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
122 prepare_int(input, ctx, $min, $max);
123 }
124 }
125 )*};
126}
127wide_int_validate! {
128 isize => isize::MIN as i128, isize::MAX as i128;
129 usize => 0, usize::MAX as i128;
130 i128 => i128::from(i64::MIN), i128::from(u64::MAX);
131 u128 => 0, i128::from(u64::MAX);
132}
133
134fn prepare_float(input: &mut Value, ctx: &mut ValidationContext) {
135 match &*input {
136 Value::Number(_) => {}
137 Value::String(s) if ctx.allows_text_coercion() => {
138 match s.trim().parse::<f64>().ok().and_then(Number::from_f64) {
139 Some(n) => *input = Value::Number(n),
140 None => ctx.error("float_parsing", "input is not a valid finite number"),
141 }
142 }
143 other => {
144 let other = other.clone();
145 type_error(ctx, "float_type", "a number", &other);
146 }
147 }
148}
149
150impl Validate for f32 {
151 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
152 prepare_float(input, ctx);
153 }
154}
155
156impl Validate for f64 {
157 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
158 prepare_float(input, ctx);
159 }
160}
161
162impl Validate for bool {
163 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
164 let coerced = match &*input {
165 Value::Bool(_) => return,
166 Value::String(s) if ctx.allows_text_coercion() => {
167 match s.trim().to_ascii_lowercase().as_str() {
168 "true" | "t" | "yes" | "y" | "on" | "1" => Some(true),
169 "false" | "f" | "no" | "n" | "off" | "0" => Some(false),
170 _ => None,
171 }
172 }
173 Value::Number(n) if !ctx.is_strict() => match n.as_u64() {
174 Some(0) => Some(false),
175 Some(1) => Some(true),
176 _ => None,
177 },
178 other => {
179 let other = other.clone();
180 type_error(ctx, "bool_type", "a boolean", &other);
181 return;
182 }
183 };
184 match coerced {
185 Some(b) => *input = Value::Bool(b),
186 None => ctx.error("bool_parsing", "input is not a valid boolean"),
187 }
188 }
189}
190
191fn prepare_string(input: &mut Value, ctx: &mut ValidationContext) -> Option<()> {
193 let Value::String(s) = input else {
194 let other = input.clone();
195 type_error(ctx, "string_type", "a string", &other);
196 return None;
197 };
198 let config = *ctx.config();
199 if config.str_strip_whitespace {
200 let trimmed = s.trim();
201 if trimmed.len() != s.len() {
202 *s = trimmed.to_owned();
203 }
204 }
205 if config.str_to_lower {
206 *s = s.to_lowercase();
207 } else if config.str_to_upper {
208 *s = s.to_uppercase();
209 }
210 Some(())
211}
212
213impl Validate for String {
214 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
215 prepare_string(input, ctx);
216 }
217}
218
219impl Validate for Value {}
220
221impl<T: Validate> Validate for Option<T> {
222 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
223 if !input.is_null() {
224 T::prepare(input, ctx);
225 }
226 }
227
228 fn validate(&self, ctx: &mut ValidationContext) {
229 if let Some(inner) = self {
230 inner.validate(ctx);
231 }
232 }
233}
234
235impl<T: Validate + ?Sized> Validate for Box<T> {
236 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
237 T::prepare(input, ctx);
238 }
239
240 fn validate(&self, ctx: &mut ValidationContext) {
241 (**self).validate(ctx);
242 }
243}
244
245impl<T: Validate> Validate for Vec<T> {
246 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
247 if !input.is_array() {
248 if ctx.input_kind() == crate::context::InputKind::Text && !input.is_null() {
250 *input = Value::Array(vec![input.take()]);
251 } else {
252 let other = input.clone();
253 type_error(ctx, "list_type", "an array", &other);
254 return;
255 }
256 }
257 if let Value::Array(items) = input {
258 for (index, item) in items.iter_mut().enumerate() {
259 ctx.at(LocationItem::index(index), |ctx| T::prepare(item, ctx));
260 }
261 }
262 }
263
264 fn validate(&self, ctx: &mut ValidationContext) {
265 for (index, item) in self.iter().enumerate() {
266 ctx.at(LocationItem::index(index), |ctx| item.validate(ctx));
267 }
268 }
269}
270
271fn prepare_map<T: Validate>(input: &mut Value, ctx: &mut ValidationContext) {
272 let Value::Object(map) = input else {
273 let other = input.clone();
274 type_error(ctx, "dict_type", "an object", &other);
275 return;
276 };
277 for (key, value) in map.iter_mut() {
278 ctx.at(key.as_str(), |ctx| T::prepare(value, ctx));
279 }
280}
281
282impl<T: Validate> Validate for BTreeMap<String, T> {
283 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
284 prepare_map::<T>(input, ctx);
285 }
286
287 fn validate(&self, ctx: &mut ValidationContext) {
288 for (key, value) in self {
289 ctx.at(key.as_str(), |ctx| value.validate(ctx));
290 }
291 }
292}
293
294impl<T: Validate, S> Validate for HashMap<String, T, S> {
295 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
296 prepare_map::<T>(input, ctx);
297 }
298
299 fn validate(&self, ctx: &mut ValidationContext) {
300 for (key, value) in self {
301 ctx.at(key.as_str(), |ctx| value.validate(ctx));
302 }
303 }
304}
305
306impl Validate for Map<String, Value> {
307 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
308 prepare_map::<Value>(input, ctx);
309 }
310}
311
312macro_rules! tuple_validate {
313 ($($t:ident $i:tt),+) => {
314 impl<$($t: Validate),+> Validate for ($($t,)+) {
316 fn validate(&self, ctx: &mut ValidationContext) {
317 $(ctx.at(LocationItem::index($i), |ctx| self.$i.validate(ctx));)+
318 }
319 }
320 };
321}
322tuple_validate!(A 0);
323tuple_validate!(A 0, B 1);
324tuple_validate!(A 0, B 1, C 2);
325tuple_validate!(A 0, B 1, C 2, D 3);
326
327impl Validate for char {}
328
329impl<const MIN: usize, const MAX: usize> Validate for ConstrainedString<MIN, MAX> {
330 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
331 if prepare_string(input, ctx).is_some() {
332 let s = input.as_str().unwrap_or_default();
333 ctx.check(rules::min_length(s, MIN));
334 ctx.check(rules::max_length(s, MAX));
335 }
336 }
337}
338
339impl Validate for Email {
340 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
341 if prepare_string(input, ctx).is_some() {
342 ctx.check(rules::email(input.as_str().unwrap_or_default()));
343 }
344 }
345}
346
347impl Validate for SecretString {
348 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
349 prepare_string(input, ctx);
350 }
351}
352
353impl<const MIN: i64, const MAX: i64> Validate for BoundedI64<MIN, MAX> {
354 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
355 prepare_int(input, ctx, i128::from(MIN), i128::from(MAX));
356 }
357}
358
359impl Validate for PositiveInt {
360 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
361 let before = ctx.error_count();
362 prepare_int(input, ctx, i128::from(i64::MIN), i128::from(i64::MAX));
363 if ctx.error_count() == before {
364 ctx.check(rules::gt(&input.as_i64().unwrap_or_default(), &0));
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::context::ModelConfig;
373 use serde_json::json;
374
375 fn run<T: Validate>(mut input: Value, ctx: &mut ValidationContext) -> (Value, Vec<String>) {
376 T::prepare(&mut input, ctx);
377 let codes = ctx
378 .take_errors()
379 .errors
380 .into_iter()
381 .map(|e| e.code.into_owned())
382 .collect();
383 (input, codes)
384 }
385
386 fn lax<T: Validate>(input: Value) -> (Value, Vec<String>) {
387 run::<T>(input, &mut ValidationContext::new())
388 }
389
390 fn strict<T: Validate>(input: Value) -> (Value, Vec<String>) {
391 run::<T>(input, &mut ValidationContext::new().with_strict(true))
392 }
393
394 #[test]
395 fn integers_coerce_in_lax_mode_only() {
396 assert_eq!(lax::<i32>(json!("42")), (json!(42), vec![]));
397 assert_eq!(lax::<i32>(json!(3.0)), (json!(3), vec![]));
398 assert_eq!(lax::<i32>(json!(3.5)).1, ["int_from_float"]);
399 assert_eq!(lax::<u8>(json!(300)).1, ["int_out_of_range"]);
400 assert_eq!(lax::<u8>(json!(-1)).1, ["int_out_of_range"]);
401 assert_eq!(lax::<i64>(json!("x")).1, ["int_parsing"]);
402 assert_eq!(strict::<i32>(json!("42")).1, ["int_type"]);
403 assert_eq!(strict::<i32>(json!(3.0)).1, ["int_from_float"]);
404 assert_eq!(lax::<i32>(json!(true)).1, ["int_type"]);
405 assert_eq!(lax::<usize>(json!("5")), (json!(5), vec![]));
406 assert_eq!(lax::<usize>(json!(-1)).1, ["int_out_of_range"]);
407 assert_eq!(lax::<i128>(json!(-3)), (json!(-3), vec![]));
408 assert_eq!(lax::<u128>(json!(-3)).1, ["int_out_of_range"]);
409 }
410
411 #[test]
412 fn text_input_coerces_even_when_strict() {
413 let mut ctx = ValidationContext::for_text().with_strict(true);
414 assert_eq!(run::<i64>(json!("7"), &mut ctx), (json!(7), vec![]));
415 assert_eq!(run::<Vec<u8>>(json!("7"), &mut ctx), (json!([7]), vec![]));
416 }
417
418 #[test]
419 fn floats_and_bools() {
420 assert_eq!(lax::<f64>(json!("1.5")), (json!(1.5), vec![]));
421 assert_eq!(lax::<f64>(json!("nan")).1, ["float_parsing"]);
422 assert_eq!(lax::<bool>(json!("Yes")), (json!(true), vec![]));
423 assert_eq!(lax::<bool>(json!(0)), (json!(false), vec![]));
424 assert_eq!(lax::<bool>(json!("maybe")).1, ["bool_parsing"]);
425 assert_eq!(strict::<bool>(json!(1)).1, ["bool_type"]);
426 }
427
428 #[test]
429 fn strings_apply_model_transforms() {
430 let config = ModelConfig {
431 str_strip_whitespace: true,
432 str_to_lower: true,
433 ..ModelConfig::DEFAULT
434 };
435 let mut ctx = ValidationContext::new();
436 let out = ctx.with_config(config, |ctx| run::<String>(json!(" HeLLo "), ctx));
437 assert_eq!(out, (json!("hello"), vec![]));
438 assert_eq!(lax::<String>(json!(5)).1, ["string_type"]);
439 }
440
441 #[test]
442 fn containers_collect_every_error_with_locations() {
443 let mut ctx = ValidationContext::new().at_root("body");
444 let mut input = json!({"a": [1, "x", 2.5], "b": null});
445 <BTreeMap<String, Vec<Option<i32>>>>::prepare(&mut input, &mut ctx);
446 let errors = ctx.take_errors().errors;
447 assert_eq!(errors.len(), 3);
448 assert_eq!(
449 errors[0].location,
450 vec!["body".into(), "a".into(), LocationItem::index(1)]
451 );
452 assert_eq!(errors[2].location, vec!["body".into(), "b".into()]);
453 assert_eq!(errors[2].code, "list_type");
454 }
455
456 #[test]
457 fn constrained_types_check_during_prepare() {
458 assert_eq!(
459 lax::<ConstrainedString<2, 3>>(json!("a")).1,
460 ["string_too_short"]
461 );
462 assert_eq!(lax::<Email>(json!("nope")).1, ["invalid_email"]);
463 assert_eq!(lax::<PositiveInt>(json!("0")).1, ["greater_than"]);
464 assert_eq!(lax::<BoundedI64<1, 5>>(json!(9)).1, ["int_out_of_range"]);
465 }
466}