1use crate::datetime::{validate_date, validate_datetime};
8use crate::error::{Code, Issue, Path, Segment, ValidationError};
9use crate::input::{Input, Kind};
10use crate::limits::Limits;
11use crate::schema::{Field, IntType, ObjectType, Presence, Rule, Schema, Type, UnionType};
12use crate::value::{Int, Slot, Value};
13
14pub fn validate<I: Input>(
15 schema: &Schema,
16 type_name: &str,
17 input: &I,
18 limits: Limits,
19) -> Result<(), ValidationError> {
20 let limits = limits.clamped();
24 let mut v = Validator { schema, limits, path: Vec::new(), issues: Vec::new() };
25
26 match schema.get(type_name) {
27 Some(ty) => v.object(ty, input, 0),
28 None => match schema.union(type_name) {
29 Some(u) => v.union(u, input, 0),
30 None => v.push(
31 Code::UnknownType,
32 format!("schema declares no type named `{type_name}`"),
33 ),
34 },
35 }
36
37 if v.issues.is_empty() {
38 Ok(())
39 } else {
40 Err(ValidationError { issues: v.issues })
41 }
42}
43
44enum Step<'a> {
45 Key(&'a str),
46 Index(usize),
47}
48
49struct Validator<'a> {
50 schema: &'a Schema,
51 limits: Limits,
52 path: Vec<Step<'a>>,
53 issues: Vec<Issue>,
54}
55
56impl<'a> Validator<'a> {
57 fn here(&self) -> Path {
58 Path(
59 self.path
60 .iter()
61 .map(|step| match step {
62 Step::Key(k) => Segment::Key((*k).to_string()),
63 Step::Index(i) => Segment::Index(*i),
64 })
65 .collect(),
66 )
67 }
68
69 fn push(&mut self, code: Code, message: String) {
70 let path = self.here();
71 self.issues.push(Issue { path, code, message });
72 }
73
74 fn push_under(&mut self, key: String, code: Code, message: String) {
75 let mut path = self.here();
76 path.0.push(Segment::Key(key));
77 self.issues.push(Issue { path, code, message });
78 }
79
80 fn within<F: FnOnce(&mut Self)>(&mut self, step: Step<'a>, f: F) {
81 self.path.push(step);
82 f(self);
83 self.path.pop();
84 }
85
86 fn depth_ok(&mut self, depth: usize) -> bool {
87 if depth > self.limits.max_depth {
88 self.push(
89 Code::DepthExceeded,
90 format!("nesting deeper than the limit of {}", self.limits.max_depth),
91 );
92 return false;
93 }
94 true
95 }
96
97 fn mismatch<I: Input>(&mut self, expected: &str, got: &I) {
98 self.push(
99 Code::TypeMismatch,
100 format!("expected {expected}, found {}", got.kind().name()),
101 );
102 }
103
104 fn object<I: Input>(&mut self, ty: &'a ObjectType, input: &I, depth: usize) {
105 self.object_with(ty, input, depth, None);
106 }
107
108 fn object_with<I: Input>(
111 &mut self,
112 ty: &'a ObjectType,
113 input: &I,
114 depth: usize,
115 tag: Option<&str>,
116 ) {
117 if !self.depth_ok(depth) {
118 return;
119 }
120 if input.kind() != Kind::Object {
121 self.mismatch("object", input);
122 return;
123 }
124
125 if input.len() > self.limits.max_object_keys {
126 self.push(
127 Code::SizeExceeded,
128 format!(
129 "{} keys exceeds the limit of {}",
130 input.len(),
131 self.limits.max_object_keys
132 ),
133 );
134 return;
135 }
136
137 for field in &ty.fields {
138 self.field(field, input, depth);
139 }
140
141 if ty.deny_unknown_fields {
142 let mut unknown = Vec::new();
145 input.each_key(&mut |key| {
146 if ty.field(key).is_none() && tag != Some(key) {
147 unknown.push(key.to_string());
148 }
149 });
150 let owner = ty.name.clone();
151 for key in unknown {
152 let message = format!("`{owner}` declares no field named `{key}`");
153 self.push_under(key, Code::UnknownField, message);
154 }
155 }
156 }
157
158 fn union<I: Input>(&mut self, u: &'a UnionType, input: &I, depth: usize) {
165 if !self.depth_ok(depth) {
166 return;
167 }
168 if input.kind() != Kind::Object {
169 self.mismatch("object", input);
170 return;
171 }
172
173 let chosen = match input.slot(&u.tag) {
174 Slot::Absent => {
175 let message = format!("`{}` decides which variant of `{}` this is", u.tag, u.name);
176 self.push_under(u.tag.clone(), Code::Required, message);
177 None
178 }
179 Slot::Null => {
180 let message = format!("the tag of `{}` cannot be null", u.name);
181 self.push_under(u.tag.clone(), Code::NullNotAllowed, message);
182 None
183 }
184 Slot::Present(value) if value.kind() != Kind::String => {
185 let message = format!("expected string, found {}", value.kind().name());
186 self.push_under(u.tag.clone(), Code::TypeMismatch, message);
187 None
188 }
189 Slot::Present(value) => match value.as_str() {
190 None => None,
191 Some(found) => match u.variant(&found) {
192 Some(variant) => Some(variant),
193 None => {
194 let listed: Vec<&str> = u.variants.iter().map(|v| v.tag.as_str()).collect();
195 let message = format!(
196 "`{found}` is not a variant of `{}`; expected one of: {}",
197 u.name,
198 listed.join(", ")
199 );
200 self.push_under(u.tag.clone(), Code::UnknownVariant, message);
201 None
202 }
203 },
204 },
205 };
206
207 let Some(variant) = chosen else {
208 return;
209 };
210
211 let target = self.schema.get(&variant.type_name);
213 match target {
214 Some(obj) => self.object_with(obj, input, depth, Some(&u.tag)),
217 None => self.push(
218 Code::UnknownType,
219 format!("schema declares no type named `{}`", variant.type_name),
220 ),
221 }
222 }
223
224 fn field<I: Input>(&mut self, field: &'a Field, input: &I, depth: usize) {
225 let slot = input.slot(&field.name);
226 self.within(Step::Key(&field.name), |v| match slot {
227 Slot::Absent => {
228 if !field.presence.optional {
229 v.push(Code::Required, required_message(field.presence));
230 }
231 }
232 Slot::Null => {
233 if !field.presence.nullable {
234 v.push(Code::NullNotAllowed, null_message(field.presence));
235 }
236 }
237 Slot::Present(value) => {
238 v.value(&field.ty, &value, depth + 1);
239 v.rules(&field.rules, &value);
240 }
241 });
242 }
243
244 fn value<I: Input>(&mut self, ty: &'a Type, input: &I, depth: usize) {
245 if input.kind() == Kind::String {
248 if let Some(s) = input.as_str() {
249 if s.len() > self.limits.max_string_bytes {
250 self.push(
251 Code::SizeExceeded,
252 format!(
253 "{} bytes exceeds the limit of {}",
254 s.len(),
255 self.limits.max_string_bytes
256 ),
257 );
258 return;
259 }
260 }
261 }
262
263 match ty {
264 Type::Bool => {
265 if input.as_bool().is_none() {
266 self.mismatch("bool", input);
267 }
268 }
269 Type::Float => match input.kind() {
270 Kind::Int => {}
271 Kind::Float => match input.as_f64() {
272 Some(f) if f.is_finite() => {}
273 _ => self.push(
274 Code::NotFinite,
275 "NaN and infinity are not valid values".to_string(),
276 ),
277 },
278 _ => self.mismatch("float", input),
279 },
280 Type::String => {
281 if input.kind() != Kind::String {
282 self.mismatch("string", input);
283 }
284 }
285 Type::Int(int_ty) => self.integer(*int_ty, input),
286 Type::Date => self.temporal(input, "date", validate_date),
287 Type::DateTime => self.temporal(input, "datetime", validate_datetime),
288 Type::Enum(allowed) => self.enumeration(allowed, input),
289 Type::Array { item, item_nullable } => self.array(item, *item_nullable, input, depth),
290 Type::Object(obj) => self.object(obj, input, depth),
291 Type::Ref(name) => {
292 let target = self.schema.get(name);
295 match target {
296 Some(obj) => self.object(obj, input, depth),
297 None => match self.schema.union(name) {
298 Some(u) => self.union(u, input, depth),
299 None => self.push(
300 Code::UnknownType,
301 format!("schema declares no type named `{name}`"),
302 ),
303 },
304 }
305 }
306 }
307 }
308
309 fn integer<I: Input>(&mut self, ty: IntType, input: &I) {
310 if input.kind() == Kind::UnsafeInteger {
311 self.push(
312 Code::UnsafeInteger,
313 "arrived in a numeric type that cannot hold it exactly; send it as the host's arbitrary-precision integer"
314 .to_string(),
315 );
316 return;
317 }
318 if input.kind() == Kind::IntegerTooWide {
319 self.push(
320 Code::IntegerTooWide,
321 "integer is wider than 64 bits".to_string(),
322 );
323 return;
324 }
325 let Some(n) = input.as_int() else {
326 self.mismatch(ty.name(), input);
327 return;
328 };
329 let (min, max) = ty.range();
330 let v = n.as_i128();
331 if v < min || v > max {
332 self.push(
333 Code::OutOfRange,
334 format!("{v} is outside the range of {} ({min}..={max})", ty.name()),
335 );
336 }
337 }
338
339 fn temporal<I: Input>(
340 &mut self,
341 input: &I,
342 expected: &str,
343 check: fn(&str) -> Result<(), Code>,
344 ) {
345 let Some(s) = input.as_str() else {
346 self.mismatch(expected, input);
347 return;
348 };
349 if let Err(code) = check(&s) {
350 let message = match code {
351 Code::MissingTimezone => {
352 format!("`{s}` has no UTC offset; Seam does not assume local time")
353 }
354 _ => format!("`{s}` is not a valid {expected}"),
355 };
356 self.push(code, message);
357 }
358 }
359
360 fn enumeration<I: Input>(&mut self, allowed: &[String], input: &I) {
361 let Some(s) = input.as_str() else {
362 self.mismatch("string", input);
363 return;
364 };
365 if !allowed.iter().any(|a| a.as_str() == s.as_ref()) {
366 self.push(
367 Code::NotInEnum,
368 format!("`{s}` is not one of: {}", allowed.join(", ")),
369 );
370 }
371 }
372
373 fn array<I: Input>(&mut self, item: &'a Type, item_nullable: bool, input: &I, depth: usize) {
374 if !self.depth_ok(depth) {
375 return;
376 }
377 if input.kind() != Kind::Array {
378 self.mismatch("array", input);
379 return;
380 }
381 let len = input.len();
382 if len > self.limits.max_items {
383 self.push(
384 Code::SizeExceeded,
385 format!("{len} items exceeds the limit of {}", self.limits.max_items),
386 );
387 return;
388 }
389 for i in 0..len {
390 let Some(child) = input.item(i) else { continue };
391 self.within(Step::Index(i), |v| match child.kind() {
392 Kind::Null if item_nullable => {}
393 Kind::Null => v.push(
394 Code::NullNotAllowed,
395 "must not be null; declare the element as `T?` to allow it".to_string(),
396 ),
397 _ => v.value(item, &child, depth + 1),
398 });
399 }
400 }
401
402 fn rules<I: Input>(&mut self, rules: &[Rule], input: &I) {
403 let chars = rules
406 .iter()
407 .any(|r| matches!(r, Rule::MinLen(_) | Rule::MaxLen(_)))
408 .then(|| input.as_str().map(|s| s.chars().count()))
409 .flatten();
410
411 for rule in rules {
412 match rule {
413 Rule::MinLen(n) => {
414 if let Some(len) = chars {
415 if len < *n {
416 self.push(
417 Code::TooShort,
418 format!("length {len} is below the minimum of {n}"),
419 );
420 }
421 }
422 }
423 Rule::MaxLen(n) => {
424 if let Some(len) = chars {
425 if len > *n {
426 self.push(
427 Code::TooLong,
428 format!("length {len} exceeds the maximum of {n}"),
429 );
430 }
431 }
432 }
433 Rule::Range { min, max } => {
434 if let Some(n) = input.as_int() {
435 let v = n.as_i128();
436 if v < *min || v > *max {
437 self.push(Code::OutOfRange, format!("{v} is outside {min}..={max}"));
438 }
439 }
440 }
441 Rule::Format(f) => {
442 if let Some(s) = input.as_str() {
446 if !f.matches(&s) {
447 self.push(Code::InvalidFormat, format!("not a valid {}", f.name()));
448 }
449 }
450 }
451 Rule::MinItems(n) => {
452 if input.kind() == Kind::Array && input.len() < *n {
453 self.push(
454 Code::TooFewItems,
455 format!("{} items is below the minimum of {n}", input.len()),
456 );
457 }
458 }
459 Rule::MaxItems(n) => {
460 if input.kind() == Kind::Array && input.len() > *n {
461 self.push(
462 Code::TooManyItems,
463 format!("{} items exceeds the maximum of {n}", input.len()),
464 );
465 }
466 }
467 }
468 }
469 }
470}
471
472fn required_message(p: Presence) -> String {
473 if p.nullable {
474 "required; may be null but must be present".to_string()
475 } else {
476 "required".to_string()
477 }
478}
479
480fn null_message(p: Presence) -> String {
481 if p.optional {
482 "may be absent, but must not be null when present".to_string()
483 } else {
484 "must not be null".to_string()
485 }
486}
487
488impl From<Int> for Value {
489 fn from(i: Int) -> Self {
490 Value::Int(i)
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::schema::IntWidth;
498 use std::collections::BTreeMap;
499
500 fn user_schema() -> Schema {
501 let user = ObjectType {
502 name: "User".into(),
503 deny_unknown_fields: true,
504 fields: vec![
505 Field {
506 name: "id".into(),
507 ty: Type::Int(IntType { width: IntWidth::W64, signed: false }),
508 presence: Presence::required(),
509 rules: vec![],
510 },
511 Field {
512 name: "name".into(),
513 ty: Type::String,
514 presence: Presence::required(),
515 rules: vec![Rule::MinLen(3), Rule::MaxLen(64)],
516 },
517 Field {
518 name: "plan".into(),
519 ty: Type::Enum(vec!["free".into(), "pro".into(), "enterprise".into()]),
520 presence: Presence::required(),
521 rules: vec![],
522 },
523 Field {
524 name: "nickname".into(),
525 ty: Type::String,
526 presence: Presence::nullable(),
527 rules: vec![],
528 },
529 Field {
530 name: "bio".into(),
531 ty: Type::String,
532 presence: Presence::optional(),
533 rules: vec![],
534 },
535 Field {
536 name: "signup_date".into(),
537 ty: Type::Date,
538 presence: Presence::optional_nullable(),
539 rules: vec![],
540 },
541 Field {
542 name: "tags".into(),
543 ty: Type::Array { item: Box::new(Type::String), item_nullable: false },
544 presence: Presence::optional(),
545 rules: vec![Rule::MaxItems(3)],
546 },
547 ],
548 };
549 let mut schema = Schema::default();
550 schema.types.insert("User".into(), user);
551 schema
552 }
553
554 fn obj(pairs: &[(&str, Value)]) -> Value {
555 let mut m = BTreeMap::new();
556 for (k, v) in pairs {
557 m.insert((*k).to_string(), v.clone());
558 }
559 Value::Object(m)
560 }
561
562 fn valid_user() -> Value {
563 obj(&[
564 ("id", Value::Int(Int::from(1_u64))),
565 ("name", Value::String("Gabriel".into())),
566 ("plan", Value::String("pro".into())),
567 ("nickname", Value::Null),
568 ])
569 }
570
571 fn with(key: &str, value: Value) -> Value {
572 let mut v = valid_user();
573 if let Value::Object(m) = &mut v {
574 m.insert(key.into(), value);
575 }
576 v
577 }
578
579 fn codes(v: &Value) -> Vec<(String, Code)> {
580 match validate(&user_schema(), "User", v, Limits::DEFAULT) {
581 Ok(()) => vec![],
582 Err(e) => e
583 .issues
584 .into_iter()
585 .map(|i| (i.path.render(), i.code))
586 .collect(),
587 }
588 }
589
590 #[test]
591 fn a_valid_payload_passes() {
592 assert_eq!(codes(&valid_user()), vec![]);
593 }
594
595 #[test]
596 fn the_readme_payload_keeps_its_u64_exactly() {
597 let v = with("id", Value::Int(Int::from(9_007_199_254_740_993_u64)));
598 assert_eq!(codes(&v), vec![]);
599 }
600
601 #[test]
602 fn absent_and_null_are_judged_separately() {
603 let mut m = BTreeMap::new();
605 m.insert("id".into(), Value::Int(Int::from(1_u64)));
606 m.insert("name".into(), Value::String("Gabriel".into()));
607 m.insert("plan".into(), Value::String("pro".into()));
608 assert_eq!(
609 codes(&Value::Object(m)),
610 vec![("nickname".into(), Code::Required)]
611 );
612
613 assert_eq!(
615 codes(&with("bio", Value::Null)),
616 vec![("bio".into(), Code::NullNotAllowed)]
617 );
618
619 assert_eq!(codes(&with("signup_date", Value::Null)), vec![]);
621 }
622
623 #[test]
624 fn a_naive_datetime_never_slips_through() {
625 let v = with("signup_date", Value::String("2026-08-29T00:00:00".into()));
626 assert_eq!(codes(&v), vec![("signup_date".into(), Code::InvalidDate)]);
627 }
628
629 #[test]
630 fn errors_carry_the_path_into_arrays() {
631 let v = with(
632 "tags",
633 Value::Array(vec![
634 Value::String("ok".into()),
635 Value::Int(Int::from(7_i64)),
636 ]),
637 );
638 assert_eq!(codes(&v), vec![("tags[1]".into(), Code::TypeMismatch)]);
639 }
640
641 #[test]
642 fn every_issue_is_reported_not_just_the_first() {
643 let v = obj(&[
644 ("id", Value::String("not a number".into())),
645 ("name", Value::String("ab".into())),
646 ("plan", Value::String("platinum".into())),
647 ("nickname", Value::Null),
648 ("surprise", Value::Bool(true)),
649 ]);
650 let found = codes(&v);
651 assert_eq!(found.len(), 4, "expected four issues, got {found:?}");
652 assert!(found.contains(&("id".into(), Code::TypeMismatch)));
653 assert!(found.contains(&("name".into(), Code::TooShort)));
654 assert!(found.contains(&("plan".into(), Code::NotInEnum)));
655 assert!(found.contains(&("surprise".into(), Code::UnknownField)));
656 }
657
658 #[test]
659 fn integer_width_is_enforced_against_the_declared_type() {
660 let mut schema = Schema::default();
661 schema.types.insert(
662 "T".into(),
663 ObjectType {
664 name: "T".into(),
665 deny_unknown_fields: false,
666 fields: vec![Field {
667 name: "n".into(),
668 ty: Type::Int(IntType { width: IntWidth::W32, signed: true }),
669 presence: Presence::required(),
670 rules: vec![],
671 }],
672 },
673 );
674 let too_big = obj(&[("n", Value::Int(Int::from(i64::from(i32::MAX) + 1)))]);
675 let err = validate(&schema, "T", &too_big, Limits::DEFAULT);
676 assert!(matches!(err, Err(ref e) if e.issues.len() == 1));
677 if let Err(e) = err {
678 assert_eq!(e.issues.first().map(|i| i.code), Some(Code::OutOfRange));
679 }
680 }
681
682 #[test]
683 fn limits_stop_an_oversized_array() {
684 let limits = Limits { max_items: 2, ..Limits::DEFAULT };
685 let v = with("tags", Value::Array(vec![Value::String("a".into()); 5]));
686 let err = validate(&user_schema(), "User", &v, limits);
687 assert!(matches!(err, Err(ref e) if e.issues.iter().any(|i| i.code == Code::SizeExceeded)));
688 }
689
690 #[test]
691 fn limits_stop_an_oversized_string() {
692 let limits = Limits { max_string_bytes: 8, ..Limits::DEFAULT };
693 let v = with("name", Value::String("a".repeat(9)));
694 let err = validate(&user_schema(), "User", &v, limits);
695 assert!(matches!(err, Err(ref e) if e.issues.iter().any(|i| i.code == Code::SizeExceeded)));
696
697 let ok = with("name", Value::String("a".repeat(8)));
698 assert!(validate(&user_schema(), "User", &ok, limits).is_ok());
699 }
700
701 #[test]
704 fn the_string_limit_counts_bytes_not_characters() {
705 let limits = Limits { max_string_bytes: 4, ..Limits::DEFAULT };
706 let v = with("name", Value::String("ñññ".into()));
708 let err = validate(&user_schema(), "User", &v, limits);
709 assert!(matches!(err, Err(ref e) if e.issues.iter().any(|i| i.code == Code::SizeExceeded)));
710 }
711
712 #[test]
713 fn a_null_array_item_is_rejected_unless_the_element_allows_it() {
714 let with_items = |item_nullable| {
715 let mut schema = Schema::default();
716 schema.types.insert(
717 "T".into(),
718 ObjectType {
719 name: "T".into(),
720 deny_unknown_fields: false,
721 fields: vec![Field {
722 name: "xs".into(),
723 ty: Type::Array { item: Box::new(Type::String), item_nullable },
724 presence: Presence::required(),
725 rules: vec![],
726 }],
727 },
728 );
729 schema
730 };
731 let payload = obj(&[(
732 "xs",
733 Value::Array(vec![Value::String("a".into()), Value::Null]),
734 )]);
735
736 let err = validate(&with_items(false), "T", &payload, Limits::DEFAULT)
737 .expect_err("a null element must not pass `[String]`");
738 assert_eq!(err.issues.len(), 1);
739 assert_eq!(
740 err.issues.first().map(|i| (i.path.render(), i.code)),
741 Some(("xs[1]".to_string(), Code::NullNotAllowed))
742 );
743
744 assert!(validate(&with_items(true), "T", &payload, Limits::DEFAULT).is_ok());
745 }
746}