1use indexmap::IndexMap;
43use regex::Regex;
44use std::sync::LazyLock;
45
46use crate::document::{self, Scalar as DocScalar};
47use crate::error::SchemaError;
48
49pub(crate) static DATE_RE: LazyLock<Regex> =
56 LazyLock::new(|| Regex::new(r"^(?P<y>\d{4})-(?P<mo>\d{2})-(?P<da>\d{2})$").unwrap());
57
58pub(crate) static TIME_RE: LazyLock<Regex> = LazyLock::new(|| {
60 Regex::new(
61 r"^(?P<h>\d{2}):(?P<m>\d{2})(:(?P<s>\d{2})(\.(?P<f>\d{1,6}))?)?(?P<off>[+\-]\d{2}:\d{2})?$",
62 )
63 .unwrap()
64});
65
66pub(crate) static DATETIME_RE: LazyLock<Regex> = LazyLock::new(|| {
68 Regex::new(
69 r"^(?P<y>\d{4})-(?P<mo>\d{2})-(?P<da>\d{2})T(?P<h>\d{2}):(?P<m>\d{2})(:(?P<s>\d{2})(\.(?P<f>\d{1,6}))?)?(?P<off>[+\-]\d{2}:\d{2})?$",
70 )
71 .unwrap()
72});
73
74pub(crate) fn is_leap_year(y: u32) -> bool {
75 (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
76}
77
78pub(crate) fn days_in_month(y: u32, m: u32) -> u32 {
79 match m {
80 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
81 4 | 6 | 9 | 11 => 30,
82 2 => {
83 if is_leap_year(y) {
84 29
85 } else {
86 28
87 }
88 }
89 _ => 0,
90 }
91}
92
93pub(crate) fn valid_ymd(y: u32, m: u32, d: u32) -> bool {
96 (1..=9999).contains(&y) && (1..=12).contains(&m) && d >= 1 && d <= days_in_month(y, m)
97}
98
99pub(crate) fn valid_hms(h: u32, m: u32, s: u32) -> bool {
101 h <= 23 && m <= 59 && s <= 59
102}
103
104fn valid_offset(off: Option<regex::Match<'_>>) -> bool {
107 match off {
108 None => true,
109 Some(m) => {
110 let text = m.as_str();
111 let oh: u32 = text[1..3].parse().unwrap_or(u32::MAX);
113 let om: u32 = text[4..6].parse().unwrap_or(u32::MAX);
114 oh <= 23 && om <= 59
115 }
116 }
117}
118
119fn mandatory_u32(caps: ®ex::Captures<'_>, name: &str) -> u32 {
128 caps.name(name)
129 .expect("group is mandatory in the pattern")
130 .as_str()
131 .parse()
132 .expect("group is all-digits per the pattern")
133}
134
135pub(crate) fn is_iso_date(s: &str) -> bool {
139 let Some(caps) = DATE_RE.captures(s) else {
140 return false;
141 };
142 valid_ymd(
143 mandatory_u32(&caps, "y"),
144 mandatory_u32(&caps, "mo"),
145 mandatory_u32(&caps, "da"),
146 )
147}
148
149pub(crate) fn is_iso_time(s: &str) -> bool {
152 let Some(caps) = TIME_RE.captures(s) else {
153 return false;
154 };
155 is_valid_time_captures(&caps)
156}
157
158fn is_valid_time_captures(caps: ®ex::Captures<'_>) -> bool {
159 let h = mandatory_u32(caps, "h");
160 let m = mandatory_u32(caps, "m");
161 let s = caps.name("s").map_or(0, |c| c.as_str().parse().unwrap());
165 valid_hms(h, m, s) && valid_offset(caps.name("off"))
166}
167
168pub(crate) fn is_iso_datetime(s: &str) -> bool {
177 let Some(caps) = DATETIME_RE.captures(s) else {
178 return false;
179 };
180 let ok_date = valid_ymd(
181 mandatory_u32(&caps, "y"),
182 mandatory_u32(&caps, "mo"),
183 mandatory_u32(&caps, "da"),
184 );
185 ok_date && is_valid_time_captures(&caps)
186}
187
188pub(crate) fn canonicalize_iso_time(s: &str) -> String {
200 let caps = TIME_RE
201 .captures(s)
202 .expect("caller must validate with is_iso_time first");
203 canonicalize_time_captures(&caps)
204}
205
206pub(crate) fn canonicalize_iso_datetime(s: &str) -> String {
215 let caps = DATETIME_RE
216 .captures(s)
217 .expect("caller must validate with is_iso_datetime first");
218 let y = caps.name("y").expect("mandatory group").as_str();
219 let mo = caps.name("mo").expect("mandatory group").as_str();
220 let da = caps.name("da").expect("mandatory group").as_str();
221 format!("{y}-{mo}-{da}T{}", canonicalize_time_captures(&caps))
222}
223
224fn canonicalize_time_captures(caps: ®ex::Captures<'_>) -> String {
228 let h = caps.name("h").expect("mandatory group").as_str();
229 let m = caps.name("m").expect("mandatory group").as_str();
230 let s = caps.name("s").map_or("00", |c| c.as_str());
231 let mut out = format!("{h}:{m}:{s}");
232 if let Some(f) = caps.name("f") {
233 out.push('.');
234 let digits = f.as_str();
235 out.push_str(digits);
236 for _ in digits.len()..6 {
237 out.push('0');
238 }
239 }
240 if let Some(off) = caps.name("off") {
241 out.push_str(off.as_str());
242 }
243 out
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
252pub enum ScalarKind {
253 String,
255 Integer,
257 Number,
259 Boolean,
261 Date,
263 Time,
265 Datetime,
267}
268
269impl ScalarKind {
270 pub const ALL: [ScalarKind; 7] = [
272 ScalarKind::String,
273 ScalarKind::Integer,
274 ScalarKind::Number,
275 ScalarKind::Boolean,
276 ScalarKind::Date,
277 ScalarKind::Time,
278 ScalarKind::Datetime,
279 ];
280
281 pub fn as_str(&self) -> &'static str {
283 match self {
284 ScalarKind::String => "string",
285 ScalarKind::Integer => "integer",
286 ScalarKind::Number => "number",
287 ScalarKind::Boolean => "boolean",
288 ScalarKind::Date => "date",
289 ScalarKind::Time => "time",
290 ScalarKind::Datetime => "datetime",
291 }
292 }
293
294 pub fn parse(name: &str) -> Result<ScalarKind, SchemaError> {
297 ScalarKind::ALL
298 .into_iter()
299 .find(|k| k.as_str() == name)
300 .ok_or_else(|| {
301 let names: Vec<&str> = ScalarKind::ALL.iter().map(|k| k.as_str()).collect();
302 SchemaError::new(
303 name,
304 "schema.unknown-type",
305 format!("unknown scalar {name:?}; expected one of {names:?}"),
306 )
307 })
308 }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
313pub struct Scalar {
314 kind: ScalarKind,
315 nullable: bool,
316}
317
318impl Scalar {
319 pub const fn new(kind: ScalarKind, nullable: bool) -> Self {
321 Scalar { kind, nullable }
322 }
323
324 pub fn named(name: &str, nullable: bool) -> Result<Self, SchemaError> {
327 Ok(Scalar::new(ScalarKind::parse(name)?, nullable))
328 }
329
330 pub fn kind(&self) -> ScalarKind {
332 self.kind
333 }
334
335 pub fn is_nullable(&self) -> bool {
337 self.nullable
338 }
339}
340
341impl std::fmt::Display for Scalar {
342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343 write!(
344 f,
345 "{}{}",
346 self.kind.as_str(),
347 if self.nullable { "?" } else { "" }
348 )
349 }
350}
351
352pub const STRING: Scalar = Scalar::new(ScalarKind::String, false);
354pub const INTEGER: Scalar = Scalar::new(ScalarKind::Integer, false);
356pub const NUMBER: Scalar = Scalar::new(ScalarKind::Number, false);
358pub const BOOLEAN: Scalar = Scalar::new(ScalarKind::Boolean, false);
360pub const DATE: Scalar = Scalar::new(ScalarKind::Date, false);
362pub const TIME: Scalar = Scalar::new(ScalarKind::Time, false);
364pub const DATETIME: Scalar = Scalar::new(ScalarKind::Datetime, false);
366
367pub fn nullable(scalar: Scalar) -> Scalar {
369 Scalar::new(scalar.kind, true)
370}
371
372fn is_valid_name(name: &str) -> bool {
378 let mut chars = name.chars();
379 chars
380 .next()
381 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
382 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Hash)]
387pub struct Ref {
388 pub name: String,
390}
391
392impl Ref {
393 pub fn new(name: impl Into<String>) -> Self {
395 Ref { name: name.into() }
396 }
397}
398
399impl std::fmt::Display for Ref {
400 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
401 write!(f, "ref({})", self.name)
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Hash)]
412pub enum FieldType {
413 Scalar(Scalar),
415 Ref(Ref),
417 Any,
419}
420
421impl From<Scalar> for FieldType {
422 fn from(s: Scalar) -> Self {
423 FieldType::Scalar(s)
424 }
425}
426
427impl From<Ref> for FieldType {
428 fn from(r: Ref) -> Self {
429 FieldType::Ref(r)
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Hash)]
440pub struct Field {
441 pub label: String,
443 pub ty: FieldType,
445 pub min: usize,
447 pub max: Option<usize>,
449}
450
451impl Field {
452 pub fn new(
454 label: impl Into<String>,
455 ty: impl Into<FieldType>,
456 min: usize,
457 max: Option<usize>,
458 ) -> Result<Self, SchemaError> {
459 let label = label.into();
460 if let Some(max) = max
461 && max < min
462 {
463 return Err(SchemaError::new(
464 label.clone(),
465 "schema.invalid-cardinality",
466 format!("field {label:?} has an invalid cardinality [{min},{max}]"),
467 ));
468 }
469 Ok(Field {
470 label,
471 ty: ty.into(),
472 min,
473 max,
474 })
475 }
476
477 pub fn required(
479 label: impl Into<String>,
480 ty: impl Into<FieldType>,
481 ) -> Result<Self, SchemaError> {
482 Field::new(label, ty, 1, Some(1))
483 }
484
485 pub fn cardinality_str(&self) -> String {
487 match (self.min, self.max) {
488 (1, Some(1)) => "exactly 1".to_string(),
489 (0, Some(1)) => "0 or 1".to_string(),
490 (min, None) => format!("at least {min}"),
491 (min, Some(max)) => format!("between {min} and {max}"),
492 }
493 }
494}
495
496#[derive(Debug, Clone)]
509pub struct Record {
510 fields: Vec<Field>,
511 by_label: IndexMap<String, usize>,
512}
513
514impl PartialEq for Record {
515 fn eq(&self, other: &Self) -> bool {
516 if self.fields.len() != other.fields.len() {
517 return false;
518 }
519 let mut a: Vec<&Field> = self.fields.iter().collect();
523 let mut b: Vec<&Field> = other.fields.iter().collect();
524 a.sort_by(|x, y| x.label.cmp(&y.label));
525 b.sort_by(|x, y| x.label.cmp(&y.label));
526 a == b
527 }
528}
529
530impl Eq for Record {}
531
532impl Record {
533 pub fn new(fields: Vec<Field>) -> Result<Self, SchemaError> {
535 let mut by_label = IndexMap::with_capacity(fields.len());
536 for (i, f) in fields.iter().enumerate() {
537 if by_label.insert(f.label.clone(), i).is_some() {
538 return Err(SchemaError::new(
539 &f.label,
540 "schema.duplicate-field",
541 format!("duplicate field label {:?} in a record", f.label),
542 ));
543 }
544 }
545 Ok(Record { fields, by_label })
546 }
547
548 pub fn fields(&self) -> &[Field] {
550 &self.fields
551 }
552
553 pub fn field(&self, label: &str) -> Option<&Field> {
555 self.by_label.get(label).map(|&i| &self.fields[i])
556 }
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq)]
566pub enum ErrorCode {
567 UnexpectedField,
569 Cardinality,
571 TypeMismatch,
573 NullNotAllowed,
575 ShapeMismatch,
577}
578
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
587pub enum ErrorFamily {
588 Validate,
590 Materialize,
592}
593
594impl ErrorCode {
595 pub fn as_str(&self, family: ErrorFamily) -> &'static str {
605 use ErrorFamily::{Materialize, Validate};
606 match (self, family) {
607 (ErrorCode::UnexpectedField, _) => "validate.unexpected-field",
612 (ErrorCode::Cardinality, _) => "validate.cardinality",
613 (ErrorCode::TypeMismatch, Validate) => "validate.type-mismatch",
614 (ErrorCode::TypeMismatch, Materialize) => "materialize.inexact-conversion",
615 (ErrorCode::NullNotAllowed, _) => "validate.null-not-allowed",
616 (ErrorCode::ShapeMismatch, _) => "validate.shape-mismatch",
617 }
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq)]
623pub struct ValidationError {
624 pub path: String,
626 pub message: String,
628 pub code: ErrorCode,
630}
631
632#[derive(Debug, Clone, Default, PartialEq, Eq)]
635pub struct ValidationResult {
636 errors: Vec<ValidationError>,
637}
638
639impl ValidationResult {
640 pub fn new() -> Self {
642 ValidationResult::default()
643 }
644
645 pub fn ok(&self) -> bool {
647 self.errors.is_empty()
648 }
649
650 pub fn errors(&self) -> &[ValidationError] {
652 &self.errors
653 }
654
655 pub(crate) fn add(
661 &mut self,
662 path: impl Into<String>,
663 message: impl Into<String>,
664 code: ErrorCode,
665 ) {
666 self.errors.push(ValidationError {
667 path: path.into(),
668 message: message.into(),
669 code,
670 });
671 }
672}
673
674impl std::fmt::Display for ValidationResult {
675 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
676 if self.ok() {
677 return write!(f, "valid");
678 }
679 writeln!(f, "invalid:")?;
680 for (i, e) in self.errors.iter().enumerate() {
681 if i > 0 {
682 writeln!(f)?;
683 }
684 write!(f, " at {}: {}", e.path, e.message)?;
685 }
686 Ok(())
687 }
688}
689
690pub fn matches_kind(value: &DocScalar, kind: ScalarKind) -> bool {
713 match kind {
714 ScalarKind::String => matches!(value, DocScalar::Str(_)),
715 ScalarKind::Boolean => matches!(value, DocScalar::Bool(_)),
716 ScalarKind::Integer => matches!(value, DocScalar::Int(_)),
717 ScalarKind::Number => matches!(value, DocScalar::Int(_) | DocScalar::Float(_)),
718 ScalarKind::Date => {
719 matches!(value, DocScalar::Date(_))
720 || matches!(value, DocScalar::Str(s) if is_iso_date(s))
721 }
722 ScalarKind::Time => {
723 matches!(value, DocScalar::Time(_))
724 || matches!(value, DocScalar::Str(s) if is_iso_time(s))
725 }
726 ScalarKind::Datetime => {
731 matches!(value, DocScalar::Datetime(_))
732 || matches!(value, DocScalar::Str(s) if is_iso_datetime(s) && !is_iso_date(s))
733 }
734 }
735}
736
737pub(crate) fn value_kind_name(v: &DocScalar) -> &'static str {
741 match v {
742 DocScalar::Null => "null",
743 DocScalar::Bool(_) => "boolean",
744 DocScalar::Int(_) => "integer",
745 DocScalar::Float(_) => "number",
746 DocScalar::Str(_) => "string",
747 DocScalar::Date(_) => "date",
748 DocScalar::Time(_) => "time",
749 DocScalar::Datetime(_) => "datetime",
750 }
751}
752
753pub enum Resolved<'a> {
759 Record(&'a Record),
761 Scalar(Scalar),
763 Any,
765}
766
767#[derive(Debug, Clone, PartialEq, Eq)]
769pub struct Schema {
770 root: Ref,
771 env: IndexMap<String, Record>,
772}
773
774impl Schema {
775 pub fn new(root: Ref, env: IndexMap<String, Record>) -> Result<Self, SchemaError> {
793 Self::check_names(&root, &env)?;
794 Self::check_reserved_names(&env)?;
795 let schema = Schema { root, env };
796 schema.check_refs()?;
797 Ok(schema)
798 }
799
800 fn check_names(root: &Ref, env: &IndexMap<String, Record>) -> Result<(), SchemaError> {
806 let refs = env.values().flat_map(|rec| {
807 rec.fields().iter().filter_map(|f| match &f.ty {
808 FieldType::Ref(r) => Some(&r.name),
809 _ => None,
810 })
811 });
812 let bad = std::iter::once(&root.name)
813 .chain(env.keys())
814 .chain(refs)
815 .find(|n| !is_valid_name(n));
816 match bad {
817 Some(name) => Err(SchemaError::new(
818 "$",
819 "schema.invalid-name",
820 format!("{name:?} is not a valid name; expected [A-Za-z_][A-Za-z0-9_]* (S-8)"),
821 )),
822 None => Ok(()),
823 }
824 }
825
826 fn check_reserved_names(env: &IndexMap<String, Record>) -> Result<(), SchemaError> {
827 for name in env.keys() {
828 if name == "any" {
829 return Err(SchemaError::new(
830 "any",
831 "schema.reserved-name",
832 format!(
833 "'any' is a reserved type name and cannot be used as a record name (record {name:?})"
834 ),
835 ));
836 }
837 if ScalarKind::ALL.iter().any(|k| k.as_str() == name) {
838 return Err(SchemaError::new(
839 name,
840 "schema.reserved-name",
841 format!(
842 "{name:?} is a reserved scalar name; a record cannot be defined with this name, or it could never be referenced (a bare name in a type position always means the builtin scalar)"
843 ),
844 ));
845 }
846 }
847 Ok(())
848 }
849
850 pub fn root(&self) -> &Ref {
852 &self.root
853 }
854
855 pub fn env(&self) -> &IndexMap<String, Record> {
857 &self.env
858 }
859
860 fn check_refs(&self) -> Result<(), SchemaError> {
861 if !self.env.contains_key(&self.root.name) {
862 return Err(SchemaError::new(
863 "$",
864 "schema.unknown-type",
865 format!("unknown type {:?}", self.root.name),
866 ));
867 }
868 for (rec_name, rec) in &self.env {
869 for f in rec.fields() {
870 if let FieldType::Ref(r) = &f.ty
871 && !self.env.contains_key(&r.name)
872 {
873 return Err(SchemaError::new(
874 format!("{rec_name}.{}", f.label),
875 "schema.unknown-type",
876 format!("unknown type {:?}", r.name),
877 ));
878 }
879 }
880 }
881 Ok(())
882 }
883
884 pub fn resolve(&self, ty: &FieldType) -> Resolved<'_> {
888 match ty {
889 FieldType::Scalar(s) => Resolved::Scalar(*s),
890 FieldType::Any => Resolved::Any,
891 FieldType::Ref(r) => Resolved::Record(
892 self.env
893 .get(&r.name)
894 .expect("check_refs guarantees every Ref resolves"),
895 ),
896 }
897 }
898
899 pub fn validate(&self, cursor: &document::Cursor<'_>) -> ValidationResult {
903 let mut res = ValidationResult::new();
904 let mut path = cursor.path.clone();
911 self.conform(
912 cursor,
913 &FieldType::Ref(self.root.clone()),
914 &mut res,
915 &mut path,
916 );
917 res
918 }
919
920 pub fn accepts(&self, cursor: &document::Cursor<'_>) -> bool {
922 self.validate(cursor).ok()
923 }
924
925 fn conform(
926 &self,
927 cursor: &document::Cursor<'_>,
928 ty: &FieldType,
929 res: &mut ValidationResult,
930 path: &mut String,
931 ) {
932 match self.resolve(ty) {
933 Resolved::Any => {}
937 Resolved::Scalar(s) => self.conform_scalar(cursor, s, res, path),
938 Resolved::Record(r) => self.conform_record(cursor, r, res, path),
939 }
940 }
941
942 fn conform_scalar(
943 &self,
944 cursor: &document::Cursor<'_>,
945 s: Scalar,
946 res: &mut ValidationResult,
947 path: &str,
948 ) {
949 if !cursor.is_leaf() {
950 res.add(
951 path,
952 format!("expected a {} value, got an object", s.kind().as_str()),
953 ErrorCode::ShapeMismatch,
954 );
955 return;
956 }
957 let v = cursor
958 .value()
959 .expect("is_leaf() true implies value() succeeds");
960 if matches!(v, DocScalar::Null) {
961 if !s.is_nullable() {
962 res.add(path, "null not allowed here", ErrorCode::NullNotAllowed);
963 }
964 return;
965 }
966 if !matches_kind(v, s.kind()) {
967 res.add(
968 path,
969 format!(
970 "expected {}, got {} ({})",
971 s.kind().as_str(),
972 value_kind_name(v),
973 v
974 ),
975 ErrorCode::TypeMismatch,
976 );
977 }
978 }
979
980 fn conform_record(
990 &self,
991 cursor: &document::Cursor<'_>,
992 rec: &Record,
993 res: &mut ValidationResult,
994 path: &mut String,
995 ) {
996 if cursor.is_leaf() {
997 res.add(
998 path.as_str(),
999 "expected an object, got a value",
1000 ErrorCode::ShapeMismatch,
1001 );
1002 return;
1003 }
1004 let edges = cursor
1005 .raw_edges()
1006 .expect("is_leaf() false implies raw_edges() succeeds");
1007 let mut counts: IndexMap<&str, usize> = IndexMap::new();
1008 for (label, index, child_id) in &edges {
1009 *counts.entry(*label).or_insert(0) += 1;
1010 let base = path.len();
1011 crate::report::push_child_path(path, label, *index);
1012 match rec.field(label) {
1013 None => res.add(
1014 path.as_str(),
1015 "unexpected field",
1016 ErrorCode::UnexpectedField,
1017 ),
1018 Some(f) => {
1019 let child = cursor.seek(*child_id);
1020 self.conform(&child, &f.ty, res, path);
1021 }
1022 }
1023 path.truncate(base);
1024 }
1025 for f in rec.fields() {
1026 let c = counts.get(f.label.as_str()).copied().unwrap_or(0);
1027 if c < f.min || f.max.is_some_and(|max| c > max) {
1028 res.add(
1029 path.as_str(),
1030 format!(
1031 "field {:?} occurs {} time(s), expected {}",
1032 f.label,
1033 c,
1034 f.cardinality_str()
1035 ),
1036 ErrorCode::Cardinality,
1037 );
1038 }
1039 }
1040 }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use super::*;
1046 use crate::document::{Doc, Value};
1047 use indexmap::IndexMap as Map;
1048
1049 #[test]
1050 fn record_partial_eq_treats_field_order_as_insignificant_but_field_count_as_significant() {
1051 let r1 = Record::new(vec![
1052 Field::required("x", STRING).unwrap(),
1053 Field::required("y", STRING).unwrap(),
1054 ])
1055 .unwrap();
1056 let r2 = Record::new(vec![
1057 Field::required("y", STRING).unwrap(),
1058 Field::required("x", STRING).unwrap(),
1059 ])
1060 .unwrap();
1061 assert_eq!(r1, r2, "declaration order must not affect Record equality");
1062
1063 let r3 = Record::new(vec![Field::required("x", STRING).unwrap()]).unwrap();
1064 assert_ne!(
1065 r1, r3,
1066 "a different field count must never compare equal, regardless of order"
1067 );
1068 }
1069
1070 fn obj(pairs: &[(&str, Value)]) -> Value {
1071 let mut m = Map::new();
1072 for (k, v) in pairs {
1073 m.insert((*k).to_string(), v.clone());
1074 }
1075 Value::Object(m)
1076 }
1077
1078 #[test]
1081 fn scalar_construction_and_equality() {
1082 let a = Scalar::new(ScalarKind::String, false);
1083 let b = Scalar::new(ScalarKind::String, false);
1084 assert_eq!(a, b);
1085 assert_ne!(a, Scalar::new(ScalarKind::String, true));
1086 assert_eq!(a, STRING);
1087 }
1088
1089 #[test]
1090 fn scalar_nullable_flag() {
1091 assert!(!STRING.is_nullable());
1092 let n = nullable(STRING);
1093 assert!(n.is_nullable());
1094 assert_eq!(n.kind(), ScalarKind::String);
1095 }
1096
1097 #[test]
1098 fn scalar_named_accepts_every_known_name() {
1099 for k in ScalarKind::ALL {
1100 assert_eq!(Scalar::named(k.as_str(), false).unwrap().kind(), k);
1101 }
1102 }
1103
1104 #[test]
1105 fn scalar_named_rejects_unknown_name() {
1106 let err = Scalar::named("bogus", false).unwrap_err();
1107 assert!(err.to_string().contains("unknown scalar"));
1108 assert!(err.to_string().contains("bogus"));
1109 }
1110
1111 #[test]
1112 fn scalar_display_shows_nullable_suffix() {
1113 assert_eq!(STRING.to_string(), "string");
1114 assert_eq!(nullable(STRING).to_string(), "string?");
1115 }
1116
1117 #[test]
1120 fn field_rejects_max_less_than_min() {
1121 let err = Field::new("a", STRING, 2, Some(1)).unwrap_err();
1122 assert!(err.to_string().contains("invalid cardinality"));
1123 assert!(err.to_string().contains("[2,1]"));
1124 }
1125
1126 #[test]
1127 fn field_accepts_max_equal_to_min_and_unbounded_max() {
1128 assert!(Field::new("a", STRING, 1, Some(1)).is_ok());
1129 assert!(Field::new("a", STRING, 0, None).is_ok());
1130 }
1131
1132 #[test]
1133 fn field_cardinality_str_matches_python_phrasing() {
1134 assert_eq!(
1135 Field::new("a", STRING, 1, Some(1))
1136 .unwrap()
1137 .cardinality_str(),
1138 "exactly 1"
1139 );
1140 assert_eq!(
1141 Field::new("a", STRING, 0, Some(1))
1142 .unwrap()
1143 .cardinality_str(),
1144 "0 or 1"
1145 );
1146 assert_eq!(
1147 Field::new("a", STRING, 1, None).unwrap().cardinality_str(),
1148 "at least 1"
1149 );
1150 assert_eq!(
1151 Field::new("a", STRING, 2, Some(5))
1152 .unwrap()
1153 .cardinality_str(),
1154 "between 2 and 5"
1155 );
1156 }
1157
1158 #[test]
1161 fn record_rejects_duplicate_field_label() {
1162 let a1 = Field::required("a", STRING).unwrap();
1163 let a2 = Field::required("a", INTEGER).unwrap();
1164 let err = Record::new(vec![a1, a2]).unwrap_err();
1165 assert!(err.to_string().contains("duplicate field label"));
1166 assert!(err.to_string().contains("\"a\""));
1167 }
1168
1169 #[test]
1170 fn record_field_lookup_and_ordering() {
1171 let f_b = Field::required("b", STRING).unwrap();
1172 let f_a = Field::required("a", INTEGER).unwrap();
1173 let rec = Record::new(vec![f_b.clone(), f_a.clone()]).unwrap();
1174 assert_eq!(rec.fields(), &[f_b, f_a]);
1175 assert_eq!(rec.field("a").unwrap().label, "a");
1176 assert!(rec.field("missing").is_none());
1177 }
1178
1179 #[test]
1182 fn schema_rejects_unknown_root_ref() {
1183 let err = Schema::new(Ref::new("Missing"), Map::new()).unwrap_err();
1184 assert!(err.to_string().contains("unknown type"));
1185 assert!(err.to_string().contains("Missing"));
1186 }
1187
1188 #[test]
1189 fn schema_rejects_unknown_field_ref() {
1190 let mut env = Map::new();
1191 env.insert(
1192 "Root".to_string(),
1193 Record::new(vec![Field::required("x", Ref::new("Missing")).unwrap()]).unwrap(),
1194 );
1195 let err = Schema::new(Ref::new("Root"), env).unwrap_err();
1196 assert!(err.to_string().contains("unknown type"));
1197 assert!(err.to_string().contains("Missing"));
1198 }
1199
1200 #[test]
1201 fn schema_rejects_a_record_named_after_a_scalar_keyword() {
1202 let mut env = Map::new();
1206 env.insert(
1207 "Root".to_string(),
1208 Record::new(vec![Field::required("x", STRING).unwrap()]).unwrap(),
1209 );
1210 env.insert("string".to_string(), Record::new(vec![]).unwrap());
1211 let err = Schema::new(Ref::new("Root"), env).unwrap_err();
1212 assert!(err.to_string().contains("reserved scalar name"));
1213 assert!(err.to_string().contains("\"string\""));
1214 }
1215
1216 #[test]
1217 fn schema_rejects_a_record_named_any() {
1218 let mut env = Map::new();
1220 env.insert(
1221 "Root".to_string(),
1222 Record::new(vec![Field::required("x", STRING).unwrap()]).unwrap(),
1223 );
1224 env.insert("any".to_string(), Record::new(vec![]).unwrap());
1225 let err = Schema::new(Ref::new("Root"), env).unwrap_err();
1226 assert!(err.to_string().contains("reserved type name"));
1227 }
1228
1229 #[test]
1230 fn schema_accepts_a_valid_self_referential_environment() {
1231 let mut env = Map::new();
1232 env.insert(
1233 "Node".to_string(),
1234 Record::new(vec![
1235 Field::required("value", STRING).unwrap(),
1236 Field::new("child", Ref::new("Node"), 0, Some(1)).unwrap(),
1237 ])
1238 .unwrap(),
1239 );
1240 assert!(Schema::new(Ref::new("Node"), env).is_ok());
1241 }
1242
1243 fn service_schema() -> Schema {
1244 let mut env = Map::new();
1245 env.insert(
1246 "Database".to_string(),
1247 Record::new(vec![
1248 Field::required("type", STRING).unwrap(),
1249 Field::required("server", STRING).unwrap(),
1250 Field::required("port", INTEGER).unwrap(),
1251 ])
1252 .unwrap(),
1253 );
1254 env.insert(
1255 "Service".to_string(),
1256 Record::new(vec![
1257 Field::required("host", STRING).unwrap(),
1258 Field::required("port", INTEGER).unwrap(),
1259 Field::new("databases", Ref::new("Database"), 1, None).unwrap(),
1260 Field::new("tags", STRING, 0, None).unwrap(),
1261 ])
1262 .unwrap(),
1263 );
1264 Schema::new(Ref::new("Service"), env).unwrap()
1265 }
1266
1267 fn valid_service_doc() -> Value {
1268 obj(&[
1269 ("host", Value::Str("api.internal".into())),
1270 ("port", Value::Int((8443).into())),
1271 (
1272 "databases",
1273 Value::Array(vec![obj(&[
1274 ("type", Value::Str("prod".into())),
1275 ("server", Value::Str("db1".into())),
1276 ("port", Value::Int((5432).into())),
1277 ])]),
1278 ),
1279 (
1280 "tags",
1281 Value::Array(vec![
1282 Value::Str("prod".into()),
1283 Value::Str("us-east".into()),
1284 ]),
1285 ),
1286 ])
1287 }
1288
1289 #[test]
1292 fn cardinality_semantics_worked_example() {
1293 let schema = service_schema();
1294 let doc = Doc::of(&valid_service_doc()).unwrap();
1295 let res = schema.validate(&doc.root());
1296 assert!(res.ok(), "{res}");
1297 }
1298
1299 #[test]
1300 fn cardinality_rejects_too_few_databases() {
1301 let schema = service_schema();
1302 let v = obj(&[
1303 ("host", Value::Str("h".into())),
1304 ("port", Value::Int((1).into())),
1305 ]);
1306 let doc = Doc::of(&v).unwrap();
1307 let res = schema.validate(&doc.root());
1308 assert!(!res.ok());
1309 assert!(
1310 res.errors()
1311 .iter()
1312 .any(|e| e.code == ErrorCode::Cardinality && e.message.contains("\"databases\""))
1313 );
1314 }
1315
1316 #[test]
1317 fn cardinality_ignores_order() {
1318 let schema = service_schema();
1321 let v = obj(&[
1322 (
1323 "databases",
1324 Value::Array(vec![obj(&[
1325 ("port", Value::Int((1).into())),
1326 ("type", Value::Str("t".into())),
1327 ("server", Value::Str("s".into())),
1328 ])]),
1329 ),
1330 ("port", Value::Int((8443).into())),
1331 ("host", Value::Str("h".into())),
1332 ]);
1333 let doc = Doc::of(&v).unwrap();
1334 assert!(schema.accepts(&doc.root()));
1335 }
1336
1337 #[test]
1340 fn unexpected_field_is_rejected() {
1341 let schema = service_schema();
1342 let mut v = valid_service_doc();
1343 if let Value::Object(m) = &mut v {
1344 m.insert("extra".to_string(), Value::Int((1).into()));
1345 }
1346 let doc = Doc::of(&v).unwrap();
1347 let res = schema.validate(&doc.root());
1348 assert!(!res.ok());
1349 assert!(
1350 res.errors()
1351 .iter()
1352 .any(|e| e.code == ErrorCode::UnexpectedField && e.path.contains("extra"))
1353 );
1354 }
1355
1356 #[test]
1359 fn scalar_expected_but_object_found_is_shape_mismatch() {
1360 let schema = service_schema();
1361 let v = obj(&[
1362 ("host", obj(&[])),
1363 ("port", Value::Int((1).into())),
1364 (
1365 "databases",
1366 Value::Array(vec![obj(&[
1367 ("type", Value::Str("t".into())),
1368 ("server", Value::Str("s".into())),
1369 ("port", Value::Int((1).into())),
1370 ])]),
1371 ),
1372 ]);
1373 let doc = Doc::of(&v).unwrap();
1374 let res = schema.validate(&doc.root());
1375 assert!(
1376 res.errors()
1377 .iter()
1378 .any(|e| e.code == ErrorCode::ShapeMismatch)
1379 );
1380 }
1381
1382 #[test]
1383 fn record_expected_but_scalar_found_is_shape_mismatch() {
1384 let mut env = Map::new();
1385 env.insert("Root".to_string(), Record::new(vec![]).unwrap());
1386 let schema = Schema::new(Ref::new("Root"), env).unwrap();
1387 let doc = Doc::of(&Value::Int((1).into())).unwrap();
1388 let res = schema.validate(&doc.root());
1389 assert!(!res.ok());
1390 assert_eq!(res.errors()[0].code, ErrorCode::ShapeMismatch);
1391 }
1392
1393 #[test]
1394 fn type_mismatch_reports_expected_and_actual() {
1395 let schema = service_schema();
1396 let mut v = valid_service_doc();
1397 if let Value::Object(m) = &mut v {
1398 m.insert("port".to_string(), Value::Str("not a number".into()));
1399 }
1400 let doc = Doc::of(&v).unwrap();
1401 let res = schema.validate(&doc.root());
1402 let e = res
1403 .errors()
1404 .iter()
1405 .find(|e| e.code == ErrorCode::TypeMismatch)
1406 .unwrap();
1407 assert!(e.message.contains("expected integer"));
1408 assert!(e.message.contains("got string"));
1409 }
1410
1411 #[test]
1412 fn null_rejected_for_non_nullable_scalar_but_accepted_when_nullable() {
1413 let mut env = Map::new();
1414 env.insert(
1415 "Root".to_string(),
1416 Record::new(vec![Field::required("v", STRING).unwrap()]).unwrap(),
1417 );
1418 let schema = Schema::new(Ref::new("Root"), env).unwrap();
1419 let doc = Doc::of(&obj(&[("v", Value::Null)])).unwrap();
1420 let res = schema.validate(&doc.root());
1421 assert!(!res.ok());
1422 assert_eq!(res.errors()[0].code, ErrorCode::NullNotAllowed);
1423
1424 let mut env2 = Map::new();
1425 env2.insert(
1426 "Root".to_string(),
1427 Record::new(vec![Field::required("v", nullable(STRING)).unwrap()]).unwrap(),
1428 );
1429 let schema2 = Schema::new(Ref::new("Root"), env2).unwrap();
1430 let doc2 = Doc::of(&obj(&[("v", Value::Null)])).unwrap();
1431 assert!(schema2.accepts(&doc2.root()));
1432 }
1433
1434 #[test]
1435 fn accepts_and_validation_result_display() {
1436 let schema = service_schema();
1437 let doc = Doc::of(&valid_service_doc()).unwrap();
1438 assert!(schema.accepts(&doc.root()));
1439 assert_eq!(schema.validate(&doc.root()).to_string(), "valid");
1440
1441 let bad = Doc::of(&obj(&[])).unwrap();
1442 let res = schema.validate(&bad.root());
1443 assert!(!res.ok());
1444 let s = res.to_string();
1445 assert!(s.starts_with("invalid:\n at "));
1446 }
1447
1448 #[test]
1451 fn bool_never_satisfies_integer_or_number() {
1452 assert!(!matches_kind(&DocScalar::Bool(true), ScalarKind::Integer));
1453 assert!(!matches_kind(&DocScalar::Bool(true), ScalarKind::Number));
1454 assert!(matches_kind(&DocScalar::Bool(true), ScalarKind::Boolean));
1455 }
1456
1457 #[test]
1458 fn integer_satisfies_number_but_not_vice_versa() {
1459 assert!(matches_kind(
1460 &DocScalar::Int((3).into()),
1461 ScalarKind::Number
1462 ));
1463 assert!(!matches_kind(&DocScalar::Float(3.0), ScalarKind::Integer));
1464 }
1465
1466 #[test]
1469 fn is_iso_date_accepts_valid_dates() {
1470 assert!(is_iso_date("2024-01-01"));
1471 assert!(is_iso_date("9999-12-31"));
1472 }
1473
1474 #[test]
1475 fn is_iso_date_rejects_wrong_shape_and_invalid_calendar_dates() {
1476 assert!(!is_iso_date("20240101"));
1479 assert!(!is_iso_date("2024-1-1"));
1480 assert!(!is_iso_date("2024-W01-1"));
1481 assert!(!is_iso_date("not-a-date"));
1482 assert!(!is_iso_date("2024-13-01"));
1484 assert!(!is_iso_date("2024-02-30"));
1485 assert!(!is_iso_date("2024-00-01"));
1486 assert!(!is_iso_date("2024-01-00"));
1487 assert!(!is_iso_date("0000-01-01"));
1488 }
1489
1490 #[test]
1491 fn is_iso_date_thirty_day_months() {
1492 assert!(is_iso_date("2024-04-30"));
1493 assert!(!is_iso_date("2024-04-31"));
1494 assert!(is_iso_date("2024-06-30"));
1495 assert!(is_iso_date("2024-09-30"));
1496 assert!(is_iso_date("2024-11-30"));
1497 }
1498
1499 #[test]
1500 fn days_in_month_rejects_an_out_of_range_month_directly() {
1501 assert_eq!(days_in_month(2024, 13), 0);
1506 assert_eq!(days_in_month(2024, 0), 0);
1507 }
1508
1509 #[test]
1510 fn is_iso_date_leap_year_boundary() {
1511 assert!(is_iso_date("2024-02-29")); assert!(!is_iso_date("2023-02-29")); assert!(is_iso_date("2000-02-29")); assert!(!is_iso_date("1900-02-29")); }
1516
1517 #[test]
1520 fn is_iso_time_accepts_valid_times() {
1521 assert!(is_iso_time("12:00:00"));
1522 assert!(is_iso_time("12:00"));
1523 assert!(is_iso_time("12:00:00.5"));
1524 assert!(is_iso_time("12:00:00.123456"));
1525 assert!(is_iso_time("12:00:00+02:00"));
1526 assert!(is_iso_time("23:59:59"));
1527 }
1528
1529 #[test]
1530 fn is_iso_time_rejects_out_of_range_and_malformed() {
1531 assert!(!is_iso_time("25:00:00"));
1532 assert!(!is_iso_time("12:60:00"));
1533 assert!(!is_iso_time("24:00:00"));
1534 assert!(!is_iso_time("12:00:00+24:00"));
1535 assert!(!is_iso_time("12:00:00+99:99"));
1536 assert!(!is_iso_time("12:00:00.1234567")); assert!(!is_iso_time("1:00:00")); assert!(is_iso_time("12:00:00+23:59"));
1539 }
1540
1541 #[test]
1545 fn is_iso_datetime_accepts_valid_timestamps() {
1546 assert!(is_iso_datetime("2024-01-01T12:00:00"));
1547 assert!(is_iso_datetime("2024-01-01T12:00"));
1548 assert!(is_iso_datetime("2024-01-01T12:00:00+02:00"));
1549 assert!(is_iso_datetime("2024-01-01T12:00:00.123456"));
1550 }
1551
1552 #[test]
1553 fn is_iso_datetime_rejects_bare_date_and_invalid_components() {
1554 assert!(!is_iso_datetime("2024-01-01"));
1555 assert!(!is_iso_datetime("2024-01-01T25:00:00"));
1556 assert!(!is_iso_datetime("2024-13-01T12:00:00"));
1557 }
1558
1559 #[test]
1560 fn matches_kind_datetime_excludes_bare_date_string_and_a_real_date() {
1561 let v = DocScalar::Str("2024-01-01".to_string());
1569 assert!(matches_kind(&v, ScalarKind::Date));
1570 assert!(!matches_kind(&v, ScalarKind::Datetime));
1571
1572 let dt = DocScalar::Str("2024-01-01T00:00:00".to_string());
1573 assert!(matches_kind(&dt, ScalarKind::Datetime));
1574 assert!(!matches_kind(&dt, ScalarKind::Date));
1575
1576 let d_real = DocScalar::Date("2024-01-01".to_string());
1577 assert!(matches_kind(&d_real, ScalarKind::Date));
1578 assert!(!matches_kind(&d_real, ScalarKind::Datetime));
1579
1580 let dt_real = DocScalar::Datetime("2024-01-01T00:00:00".to_string());
1581 assert!(matches_kind(&dt_real, ScalarKind::Datetime));
1582 assert!(!matches_kind(&dt_real, ScalarKind::Date));
1583 }
1584
1585 #[test]
1586 fn matches_kind_date_and_time_directly() {
1587 assert!(matches_kind(
1591 &DocScalar::Date("2024-01-01".into()),
1592 ScalarKind::Date
1593 ));
1594 assert!(matches_kind(
1595 &DocScalar::Str("2024-01-01".into()),
1596 ScalarKind::Date
1597 ));
1598 assert!(!matches_kind(
1599 &DocScalar::Str("not-a-date".into()),
1600 ScalarKind::Date
1601 ));
1602 assert!(matches_kind(
1603 &DocScalar::Time("12:00:00".into()),
1604 ScalarKind::Time
1605 ));
1606 assert!(matches_kind(
1607 &DocScalar::Str("12:00:00".into()),
1608 ScalarKind::Time
1609 ));
1610 assert!(!matches_kind(
1611 &DocScalar::Str("25:00:00".into()),
1612 ScalarKind::Time
1613 ));
1614 assert!(!matches_kind(&DocScalar::Int((1).into()), ScalarKind::Date));
1615 assert!(!matches_kind(&DocScalar::Int((1).into()), ScalarKind::Time));
1616 }
1617
1618 #[test]
1619 fn schema_root_and_env_accessors() {
1620 let schema = service_schema();
1621 assert_eq!(schema.root().name, "Service");
1622 assert!(schema.env().contains_key("Database"));
1623 assert!(schema.env().contains_key("Service"));
1624 }
1625
1626 #[test]
1631 fn any_field_accepts_scalars_and_objects_unchecked() {
1632 let mut env = Map::new();
1633 env.insert(
1634 "Root".to_string(),
1635 Record::new(vec![Field::required("x", FieldType::Any).unwrap()]).unwrap(),
1636 );
1637 let schema = Schema::new(Ref::new("Root"), env).unwrap();
1638
1639 for v in [
1645 Value::Str("hi".into()),
1646 Value::Int((1).into()),
1647 Value::Float(1.5),
1648 Value::Bool(true),
1649 Value::Null,
1650 obj(&[("nested", Value::Int((1).into()))]),
1651 ] {
1652 let doc = Doc::of(&obj(&[("x", v.clone())])).unwrap();
1653 assert!(schema.accepts(&doc.root()), "any field rejected {v:?}");
1654 }
1655 }
1656
1657 #[test]
1658 fn any_field_with_array_cardinality_accepts_repeated_values_unchecked() {
1659 let mut env = Map::new();
1660 env.insert(
1661 "Root".to_string(),
1662 Record::new(vec![Field::new("x", FieldType::Any, 0, None).unwrap()]).unwrap(),
1663 );
1664 let schema = Schema::new(Ref::new("Root"), env).unwrap();
1665 let doc = Doc::of(&obj(&[(
1666 "x",
1667 Value::Array(vec![Value::Int((1).into()), Value::Str("mixed".into())]),
1668 )]))
1669 .unwrap();
1670 assert!(schema.accepts(&doc.root()));
1671 }
1672
1673 #[test]
1674 fn any_resolves_without_touching_env() {
1675 let env: Map<String, Record> = Map::new();
1676 let schema = Schema::new(Ref::new("Root"), {
1677 let mut e = env;
1678 e.insert("Root".to_string(), Record::new(vec![]).unwrap());
1679 e
1680 })
1681 .unwrap();
1682 assert!(matches!(schema.resolve(&FieldType::Any), Resolved::Any));
1683 }
1684
1685 #[test]
1686 fn field_type_from_conversions() {
1687 let ft: FieldType = STRING.into();
1688 assert_eq!(ft, FieldType::Scalar(STRING));
1689 let ft2: FieldType = Ref::new("X").into();
1690 assert_eq!(ft2, FieldType::Ref(Ref::new("X")));
1691 }
1692
1693 #[test]
1694 fn ref_display() {
1695 assert_eq!(Ref::new("Foo").to_string(), "ref(Foo)");
1696 }
1697
1698 #[test]
1699 fn error_code_as_str_covers_every_variant() {
1700 assert_eq!(
1701 ErrorCode::UnexpectedField.as_str(ErrorFamily::Validate),
1702 "validate.unexpected-field"
1703 );
1704 assert_eq!(
1705 ErrorCode::UnexpectedField.as_str(ErrorFamily::Materialize),
1706 "validate.unexpected-field"
1707 );
1708 assert_eq!(
1709 ErrorCode::Cardinality.as_str(ErrorFamily::Validate),
1710 "validate.cardinality"
1711 );
1712 assert_eq!(
1713 ErrorCode::Cardinality.as_str(ErrorFamily::Materialize),
1714 "validate.cardinality"
1715 );
1716 assert_eq!(
1717 ErrorCode::TypeMismatch.as_str(ErrorFamily::Validate),
1718 "validate.type-mismatch"
1719 );
1720 assert_eq!(
1721 ErrorCode::TypeMismatch.as_str(ErrorFamily::Materialize),
1722 "materialize.inexact-conversion"
1723 );
1724 assert_eq!(
1725 ErrorCode::NullNotAllowed.as_str(ErrorFamily::Validate),
1726 "validate.null-not-allowed"
1727 );
1728 assert_eq!(
1729 ErrorCode::NullNotAllowed.as_str(ErrorFamily::Materialize),
1730 "validate.null-not-allowed"
1731 );
1732 assert_eq!(
1733 ErrorCode::ShapeMismatch.as_str(ErrorFamily::Validate),
1734 "validate.shape-mismatch"
1735 );
1736 assert_eq!(
1737 ErrorCode::ShapeMismatch.as_str(ErrorFamily::Materialize),
1738 "validate.shape-mismatch"
1739 );
1740 }
1741
1742 #[test]
1743 fn value_kind_name_covers_every_variant() {
1744 assert_eq!(value_kind_name(&DocScalar::Null), "null");
1745 assert_eq!(value_kind_name(&DocScalar::Bool(true)), "boolean");
1746 assert_eq!(value_kind_name(&DocScalar::Int((1).into())), "integer");
1747 assert_eq!(value_kind_name(&DocScalar::Float(1.0)), "number");
1748 assert_eq!(value_kind_name(&DocScalar::Str("x".into())), "string");
1749 assert_eq!(
1750 value_kind_name(&DocScalar::Date("2024-01-01".into())),
1751 "date"
1752 );
1753 assert_eq!(value_kind_name(&DocScalar::Time("12:00:00".into())), "time");
1754 assert_eq!(
1755 value_kind_name(&DocScalar::Datetime("2024-01-01T12:00:00".into())),
1756 "datetime"
1757 );
1758 }
1759}