1mod compound;
23mod context;
24mod error;
25mod key;
26mod primitive;
27mod record;
28mod reference;
29mod trace;
30mod union;
31
32pub use context::{ValidationContext, ValidationOutput, ValidationState};
33pub use error::{ValidationError, ValidationWarning, ValidatorError};
34pub use trace::resolve_node_type_traces;
35
36use eure_document::document::node::NodeValue;
37use eure_document::document::{EureDocument, NodeId};
38use eure_document::parse::{DocumentParser, ParseContext};
39
40use crate::type_path_trace::{NodeTypeTraceMap, SchemaNodePathMap};
41use crate::{SchemaDocument, SchemaNodeContent, SchemaNodeId, identifiers};
42
43use compound::{ArrayValidator, MapValidator, TupleValidator};
44use primitive::{
45 AnyValidator, BooleanValidator, FloatValidator, IntegerValidator, LiteralValidator,
46 NullValidator, TextValidator,
47};
48use record::RecordValidator;
49use reference::ReferenceValidator;
50use union::UnionValidator;
51
52pub fn validate(document: &EureDocument, schema: &SchemaDocument) -> ValidationOutput {
71 let root_id = document.get_root_id();
72 validate_node(document, schema, root_id, schema.root)
73}
74
75#[derive(Debug, Clone, Default)]
77pub struct ValidationTraceOutput {
78 pub output: ValidationOutput,
79 pub node_type_traces: NodeTypeTraceMap,
80}
81
82pub fn validate_with_trace(
86 document: &EureDocument,
87 schema: &SchemaDocument,
88 schema_node_paths: &SchemaNodePathMap,
89) -> ValidationTraceOutput {
90 let output = validate(document, schema);
91 let node_type_traces = resolve_node_type_traces(document, schema, schema_node_paths);
92 ValidationTraceOutput {
93 output,
94 node_type_traces,
95 }
96}
97
98pub fn validate_node(
100 document: &EureDocument,
101 schema: &SchemaDocument,
102 node_id: NodeId,
103 schema_id: SchemaNodeId,
104) -> ValidationOutput {
105 let ctx = ValidationContext::new(document, schema);
106 let parse_ctx = ctx.parse_context(node_id);
107
108 let validator = SchemaValidator {
109 ctx: &ctx,
110 schema_node_id: schema_id,
111 };
112
113 let _ = parse_ctx.parse_with(validator);
115
116 ctx.finish()
117}
118
119pub struct SchemaValidator<'a, 'doc> {
127 pub ctx: &'a ValidationContext<'doc>,
128 pub schema_node_id: SchemaNodeId,
129}
130
131impl<'a, 'doc> DocumentParser<'doc> for SchemaValidator<'a, 'doc> {
132 type Output = ();
133 type Error = ValidatorError;
134
135 fn parse(&mut self, parse_ctx: &ParseContext<'doc>) -> Result<(), ValidatorError> {
136 let node = parse_ctx.node();
137
138 if node.get_extension(&identifiers::TYPE).is_some() {
139 return Ok(());
141 }
142
143 if matches!(&node.content, NodeValue::Hole(_)) {
145 self.ctx.mark_has_holes();
146 return Ok(());
147 }
148
149 let schema_node = self.ctx.schema.node(self.schema_node_id);
150
151 self.validate_extensions(parse_ctx)?;
153
154 match &schema_node.content {
156 SchemaNodeContent::Any => {
157 self.warn_unknown_extensions(parse_ctx);
158 let mut v = AnyValidator;
159 v.parse(parse_ctx)
160 }
161 SchemaNodeContent::Text(s) => {
162 self.warn_unknown_extensions(parse_ctx);
163 let mut v = TextValidator {
164 ctx: self.ctx,
165 schema: s,
166 schema_node_id: self.schema_node_id,
167 };
168 v.parse(parse_ctx)
169 }
170 SchemaNodeContent::Integer(s) => {
171 self.warn_unknown_extensions(parse_ctx);
172 let mut v = IntegerValidator {
173 ctx: self.ctx,
174 schema: s,
175 schema_node_id: self.schema_node_id,
176 };
177 v.parse(parse_ctx)
178 }
179 SchemaNodeContent::Float(s) => {
180 self.warn_unknown_extensions(parse_ctx);
181 let mut v = FloatValidator {
182 ctx: self.ctx,
183 schema: s,
184 schema_node_id: self.schema_node_id,
185 };
186 v.parse(parse_ctx)
187 }
188 SchemaNodeContent::Boolean => {
189 self.warn_unknown_extensions(parse_ctx);
190 let mut v = BooleanValidator {
191 ctx: self.ctx,
192 schema_node_id: self.schema_node_id,
193 };
194 v.parse(parse_ctx)
195 }
196 SchemaNodeContent::Null => {
197 self.warn_unknown_extensions(parse_ctx);
198 let mut v = NullValidator {
199 ctx: self.ctx,
200 schema_node_id: self.schema_node_id,
201 };
202 v.parse(parse_ctx)
203 }
204 SchemaNodeContent::Literal(expected) => {
205 self.warn_unknown_extensions(parse_ctx);
206 let mut v = LiteralValidator {
207 ctx: self.ctx,
208 expected,
209 schema_node_id: self.schema_node_id,
210 };
211 v.parse(parse_ctx)
212 }
213 SchemaNodeContent::Array(s) => {
214 self.warn_unknown_extensions(parse_ctx);
215 let mut v = ArrayValidator {
216 ctx: self.ctx,
217 schema: s,
218 schema_node_id: self.schema_node_id,
219 };
220 v.parse(parse_ctx)
221 }
222 SchemaNodeContent::Map(s) => {
223 self.warn_unknown_extensions(parse_ctx);
224 let mut v = MapValidator {
225 ctx: self.ctx,
226 schema: s,
227 schema_node_id: self.schema_node_id,
228 };
229 v.parse(parse_ctx)
230 }
231 SchemaNodeContent::Record(s) => {
232 self.warn_unknown_extensions(parse_ctx);
233 let mut v = RecordValidator {
234 ctx: self.ctx,
235 schema: s,
236 schema_node_id: self.schema_node_id,
237 };
238 v.parse(parse_ctx)
239 }
240 SchemaNodeContent::Tuple(s) => {
241 self.warn_unknown_extensions(parse_ctx);
242 let mut v = TupleValidator {
243 ctx: self.ctx,
244 schema: s,
245 schema_node_id: self.schema_node_id,
246 };
247 v.parse(parse_ctx)
248 }
249 SchemaNodeContent::Union(s) => {
250 self.warn_unknown_extensions(parse_ctx);
251 let mut v = UnionValidator {
252 ctx: self.ctx,
253 schema: s,
254 schema_node_id: self.schema_node_id,
255 };
256 v.parse(parse_ctx)
257 }
258 SchemaNodeContent::Reference(r) => {
259 let mut child_validator = ReferenceValidator {
262 ctx: self.ctx,
263 type_ref: r,
264 schema_node_id: self.schema_node_id,
265 };
266 child_validator.parse(parse_ctx)
267 }
268 }
269 }
270}
271
272impl<'a, 'doc> SchemaValidator<'a, 'doc> {
273 fn validate_extensions(&self, parse_ctx: &ParseContext<'doc>) -> Result<(), ValidatorError> {
278 let schema_node = self.ctx.schema.node(self.schema_node_id);
279 let ext_types = &schema_node.ext_types;
280 let node = parse_ctx.node();
281 let node_id = parse_ctx.node_id();
282
283 for (ext_ident, ext_schema) in ext_types {
285 if !ext_schema.optional && !node.extensions.contains_key(ext_ident) {
286 self.ctx
287 .record_error(ValidationError::MissingRequiredExtension {
288 extension: ext_ident.to_string(),
289 path: self.ctx.path(),
290 node_id,
291 schema_node_id: self.schema_node_id,
292 });
293 }
294 }
295
296 for (ext_ident, ext_schema) in ext_types {
298 if let Some(ext_ctx) = parse_ctx.ext_optional(ext_ident.as_ref()) {
299 self.ctx.push_path_extension(ext_ident.clone());
300
301 let child_validator = SchemaValidator {
302 ctx: self.ctx,
303 schema_node_id: ext_schema.schema,
304 };
305 let _ = ext_ctx.parse_with(child_validator);
306
307 self.ctx.pop_path();
308 }
309 }
310
311 Ok(())
312 }
313
314 fn warn_unknown_extensions(&self, parse_ctx: &ParseContext<'doc>) {
323 for (ext_ident, _) in parse_ctx.unknown_extensions() {
324 if Self::is_builtin_extension(ext_ident) {
326 continue;
327 }
328 self.ctx
329 .record_warning(ValidationWarning::UnknownExtension {
330 name: ext_ident.to_string(),
331 path: self.ctx.path(),
332 });
333 }
334 }
335
336 fn is_builtin_extension(ident: &eure_document::identifier::Identifier) -> bool {
346 ident == &identifiers::VARIANT
348 || ident == &identifiers::SCHEMA
349 || ident == &identifiers::EXT_TYPE
350 || ident == &identifiers::TYPE
351 || ident.as_ref() == "codegen"
353 || ident.as_ref() == "codegen-defaults"
354 || ident.as_ref() == "flatten"
356 }
357}
358
359#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::convert::document_to_schema_with_layout;
367 use crate::type_path_trace::{ResolvedTypeTrace, TypeTraceUnresolvedReason};
368 use crate::{
369 ArraySchema, Bound, CodegenDefaults, FieldCodegen, IntegerSchema, MapSchema,
370 RecordFieldSchema, RecordSchema, RootCodegen, TextSchema, TypeReference, UnionSchema,
371 UnknownFieldsPolicy,
372 };
373 use eure_document::identifier::Identifier;
374 use eure_document::text::Text;
375 use eure_document::value::{ObjectKey, PrimitiveValue};
376 use indexmap::{IndexMap, IndexSet};
377 use num_bigint::BigInt;
378
379 fn create_simple_schema(content: SchemaNodeContent) -> (SchemaDocument, SchemaNodeId) {
380 let mut schema = SchemaDocument {
381 nodes: Vec::new(),
382 root: SchemaNodeId(0),
383 types: IndexMap::new(),
384 exports: IndexSet::new(),
385 imports: IndexMap::new(),
386 root_codegen: RootCodegen::default(),
387 codegen_defaults: CodegenDefaults::default(),
388 };
389 let id = schema.create_node(content);
390 schema.root = id;
391 (schema, id)
392 }
393
394 fn create_doc_with_primitive(value: PrimitiveValue) -> EureDocument {
395 let mut doc = EureDocument::new();
396 let root_id = doc.get_root_id();
397 doc.node_mut(root_id).content = NodeValue::Primitive(value);
398 doc
399 }
400
401 #[test]
402 fn test_validate_text_basic() {
403 let (schema, _) = create_simple_schema(SchemaNodeContent::Text(TextSchema::default()));
404 let doc =
405 create_doc_with_primitive(PrimitiveValue::Text(Text::plaintext("hello".to_string())));
406 let result = validate(&doc, &schema);
407 assert!(result.is_valid);
408 }
409
410 #[test]
411 fn test_validate_text_pattern() {
412 let (schema, _) = create_simple_schema(SchemaNodeContent::Text(TextSchema {
413 pattern: Some(regex::Regex::new("^[a-z]+$").unwrap()),
414 ..Default::default()
415 }));
416
417 let doc =
418 create_doc_with_primitive(PrimitiveValue::Text(Text::plaintext("hello".to_string())));
419 let result = validate(&doc, &schema);
420 assert!(result.is_valid);
421
422 let doc = create_doc_with_primitive(PrimitiveValue::Text(Text::plaintext(
423 "Hello123".to_string(),
424 )));
425 let result = validate(&doc, &schema);
426 assert!(!result.is_valid);
427 }
428
429 #[test]
430 fn test_validate_integer() {
431 let (schema, _) = create_simple_schema(SchemaNodeContent::Integer(IntegerSchema {
432 min: Bound::Inclusive(BigInt::from(0)),
433 max: Bound::Inclusive(BigInt::from(100)),
434 multiple_of: None,
435 }));
436
437 let doc = create_doc_with_primitive(PrimitiveValue::Integer(BigInt::from(50)));
438 let result = validate(&doc, &schema);
439 assert!(result.is_valid);
440
441 let doc = create_doc_with_primitive(PrimitiveValue::Integer(BigInt::from(150)));
442 let result = validate(&doc, &schema);
443 assert!(!result.is_valid);
444 }
445
446 #[test]
447 fn test_validate_boolean() {
448 let (schema, _) = create_simple_schema(SchemaNodeContent::Boolean);
449
450 let doc = create_doc_with_primitive(PrimitiveValue::Bool(true));
451 let result = validate(&doc, &schema);
452 assert!(result.is_valid);
453
454 let doc = create_doc_with_primitive(PrimitiveValue::Integer(BigInt::from(1)));
455 let result = validate(&doc, &schema);
456 assert!(!result.is_valid);
457 }
458
459 #[test]
460 fn test_validate_array() {
461 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
462 let item_schema_id =
463 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
464 schema.node_mut(schema.root).content = SchemaNodeContent::Array(ArraySchema {
465 item: item_schema_id,
466 min_length: Some(1),
467 max_length: Some(3),
468 unique: false,
469 contains: None,
470 binding_style: None,
471 });
472
473 let mut doc = EureDocument::new();
474 let root_id = doc.get_root_id();
475 doc.node_mut(root_id).content = NodeValue::Array(Default::default());
476 let child1 = doc.add_array_element(None, root_id).unwrap().node_id;
477 doc.node_mut(child1).content =
478 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(1)));
479 let child2 = doc.add_array_element(None, root_id).unwrap().node_id;
480 doc.node_mut(child2).content =
481 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(2)));
482
483 let result = validate(&doc, &schema);
484 assert!(result.is_valid);
485 }
486
487 #[test]
488 fn test_validate_map_with_union_key_schema() {
489 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
490 let text_key_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
491 let int_key_schema_id =
492 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
493 let any_value_schema_id = schema.create_node(SchemaNodeContent::Any);
494
495 let mut variants = IndexMap::new();
496 variants.insert("text".to_string(), text_key_schema_id);
497 variants.insert("integer".to_string(), int_key_schema_id);
498 let union_key_schema_id = schema.create_node(SchemaNodeContent::Union(UnionSchema {
499 variants,
500 unambiguous: IndexSet::new(),
501 interop: crate::interop::UnionInterop::default(),
502 deny_untagged: IndexSet::new(),
503 }));
504
505 schema.node_mut(schema.root).content = SchemaNodeContent::Map(MapSchema {
506 key: union_key_schema_id,
507 value: any_value_schema_id,
508 min_size: None,
509 max_size: None,
510 });
511
512 let mut doc = EureDocument::new();
513 let root_id = doc.get_root_id();
514
515 let text_value_id = doc
516 .add_map_child(ObjectKey::String("name".to_string()), root_id)
517 .unwrap()
518 .node_id;
519 doc.node_mut(text_value_id).content =
520 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice".to_string())));
521
522 let int_value_id = doc
523 .add_map_child(ObjectKey::Number(BigInt::from(1)), root_id)
524 .unwrap()
525 .node_id;
526 doc.node_mut(int_value_id).content =
527 NodeValue::Primitive(PrimitiveValue::Integer(42.into()));
528
529 let result = validate(&doc, &schema);
530 assert!(
531 result.is_valid,
532 "Expected union key schema to validate: {:?}",
533 result.errors
534 );
535 }
536
537 #[test]
538 fn test_validate_map_with_reference_to_union_key_schema() {
539 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
540 let text_key_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
541 let int_key_schema_id =
542 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
543 let any_value_schema_id = schema.create_node(SchemaNodeContent::Any);
544
545 let mut variants = IndexMap::new();
546 variants.insert("text".to_string(), text_key_schema_id);
547 variants.insert("integer".to_string(), int_key_schema_id);
548 let union_key_schema_id = schema.create_node(SchemaNodeContent::Union(UnionSchema {
549 variants,
550 unambiguous: IndexSet::new(),
551 interop: crate::interop::UnionInterop::default(),
552 deny_untagged: IndexSet::new(),
553 }));
554 schema.register_type(Identifier::new_unchecked("key"), union_key_schema_id);
555
556 let key_ref_schema_id =
557 schema.create_node(SchemaNodeContent::Reference(TypeReference::Named {
558 namespace: None,
559 name: Identifier::new_unchecked("key"),
560 }));
561
562 schema.node_mut(schema.root).content = SchemaNodeContent::Map(MapSchema {
563 key: key_ref_schema_id,
564 value: any_value_schema_id,
565 min_size: None,
566 max_size: None,
567 });
568
569 let mut doc = EureDocument::new();
570 let root_id = doc.get_root_id();
571 let value_id = doc
572 .add_map_child(ObjectKey::Number(BigInt::from(7)), root_id)
573 .unwrap()
574 .node_id;
575 doc.node_mut(value_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
576
577 let result = validate(&doc, &schema);
578 assert!(
579 result.is_valid,
580 "Expected reference to union key schema to validate: {:?}",
581 result.errors
582 );
583 }
584
585 #[test]
586 fn test_validate_record_flattened_map_boolean_key() {
587 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
591 let bool_key_schema_id = schema.create_node(SchemaNodeContent::Boolean);
592 let any_value_schema_id = schema.create_node(SchemaNodeContent::Any);
593 let map_schema_id = schema.create_node(SchemaNodeContent::Map(MapSchema {
594 key: bool_key_schema_id,
595 value: any_value_schema_id,
596 min_size: None,
597 max_size: None,
598 }));
599 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
600 properties: IndexMap::new(),
601 flatten: vec![map_schema_id],
602 unknown_fields: UnknownFieldsPolicy::Deny,
603 });
604
605 let mut doc = EureDocument::new();
606 let root_id = doc.get_root_id();
607 let value_id = doc
608 .add_map_child(ObjectKey::String("true".to_string()), root_id)
609 .unwrap()
610 .node_id;
611 doc.node_mut(value_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
612
613 let result = validate(&doc, &schema);
614 assert!(
615 result.is_valid,
616 "Expected boolean key 'true' to be valid against Boolean key schema in flattened map: {:?}",
617 result.errors
618 );
619 }
620
621 #[test]
622 fn test_validate_record() {
623 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
624 let name_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
625 let age_schema_id =
626 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
627
628 let mut properties = IndexMap::new();
629 properties.insert(
630 "name".to_string(),
631 RecordFieldSchema {
632 schema: name_schema_id,
633 optional: false,
634 binding_style: None,
635 field_codegen: FieldCodegen::default(),
636 },
637 );
638 properties.insert(
639 "age".to_string(),
640 RecordFieldSchema {
641 schema: age_schema_id,
642 optional: true,
643 binding_style: None,
644 field_codegen: FieldCodegen::default(),
645 },
646 );
647
648 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
649 properties,
650 flatten: vec![],
651 unknown_fields: UnknownFieldsPolicy::Deny,
652 });
653
654 let mut doc = EureDocument::new();
655 let root_id = doc.get_root_id();
656 let name_id = doc
657 .add_map_child(ObjectKey::String("name".to_string()), root_id)
658 .unwrap()
659 .node_id;
660 doc.node_mut(name_id).content =
661 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice".to_string())));
662
663 let result = validate(&doc, &schema);
664 assert!(result.is_valid);
665 }
666
667 #[test]
668 fn test_validate_record_with_sibling_flatten_targets() {
669 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
670 let name_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
671 let age_schema_id =
672 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
673
674 let mut left_properties = IndexMap::new();
675 left_properties.insert(
676 "name".to_string(),
677 RecordFieldSchema {
678 schema: name_schema_id,
679 optional: false,
680 binding_style: None,
681 field_codegen: FieldCodegen::default(),
682 },
683 );
684 let left_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
685 properties: left_properties,
686 flatten: vec![],
687 unknown_fields: UnknownFieldsPolicy::Deny,
688 }));
689
690 let mut right_properties = IndexMap::new();
691 right_properties.insert(
692 "age".to_string(),
693 RecordFieldSchema {
694 schema: age_schema_id,
695 optional: false,
696 binding_style: None,
697 field_codegen: FieldCodegen::default(),
698 },
699 );
700 let right_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
701 properties: right_properties,
702 flatten: vec![],
703 unknown_fields: UnknownFieldsPolicy::Deny,
704 }));
705
706 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
707 properties: IndexMap::new(),
708 flatten: vec![left_schema_id, right_schema_id],
709 unknown_fields: UnknownFieldsPolicy::Deny,
710 });
711
712 let mut doc = EureDocument::new();
713 let root_id = doc.get_root_id();
714 let name_id = doc
715 .add_map_child(ObjectKey::String("name".to_string()), root_id)
716 .unwrap()
717 .node_id;
718 doc.node_mut(name_id).content =
719 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice".to_string())));
720 let age_id = doc
721 .add_map_child(ObjectKey::String("age".to_string()), root_id)
722 .unwrap()
723 .node_id;
724 doc.node_mut(age_id).content =
725 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
726
727 let result = validate(&doc, &schema);
728 assert!(
729 result.is_valid,
730 "Expected sibling flatten targets to validate, got errors: {:?}",
731 result.errors
732 );
733 }
734
735 #[test]
736 fn test_validate_record_with_flattened_union_and_sibling_flatten_targets() {
737 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
738 let name_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
739 let nickname_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
740 let age_schema_id =
741 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
742
743 let mut person_properties = IndexMap::new();
744 person_properties.insert(
745 "name".to_string(),
746 RecordFieldSchema {
747 schema: name_schema_id,
748 optional: false,
749 binding_style: None,
750 field_codegen: FieldCodegen::default(),
751 },
752 );
753 let person_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
754 properties: person_properties,
755 flatten: vec![],
756 unknown_fields: UnknownFieldsPolicy::Deny,
757 }));
758
759 let mut alias_properties = IndexMap::new();
760 alias_properties.insert(
761 "nickname".to_string(),
762 RecordFieldSchema {
763 schema: nickname_schema_id,
764 optional: false,
765 binding_style: None,
766 field_codegen: FieldCodegen::default(),
767 },
768 );
769 let alias_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
770 properties: alias_properties,
771 flatten: vec![],
772 unknown_fields: UnknownFieldsPolicy::Deny,
773 }));
774
775 let mut union_variants = IndexMap::new();
776 union_variants.insert("Person".to_string(), person_schema_id);
777 union_variants.insert("Alias".to_string(), alias_schema_id);
778 let union_schema_id = schema.create_node(SchemaNodeContent::Union(UnionSchema {
779 variants: union_variants,
780 unambiguous: IndexSet::new(),
781 interop: crate::interop::UnionInterop::default(),
782 deny_untagged: IndexSet::new(),
783 }));
784
785 let mut sibling_properties = IndexMap::new();
786 sibling_properties.insert(
787 "age".to_string(),
788 RecordFieldSchema {
789 schema: age_schema_id,
790 optional: false,
791 binding_style: None,
792 field_codegen: FieldCodegen::default(),
793 },
794 );
795 let sibling_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
796 properties: sibling_properties,
797 flatten: vec![],
798 unknown_fields: UnknownFieldsPolicy::Deny,
799 }));
800
801 let mut doc = EureDocument::new();
802 let root_id = doc.get_root_id();
803 let name_id = doc
804 .add_map_child(ObjectKey::String("name".to_string()), root_id)
805 .unwrap()
806 .node_id;
807 doc.node_mut(name_id).content =
808 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice".to_string())));
809 let age_id = doc
810 .add_map_child(ObjectKey::String("age".to_string()), root_id)
811 .unwrap()
812 .node_id;
813 doc.node_mut(age_id).content =
814 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
815
816 for flatten in [
817 vec![union_schema_id, sibling_schema_id],
818 vec![sibling_schema_id, union_schema_id],
819 ] {
820 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
821 properties: IndexMap::new(),
822 flatten,
823 unknown_fields: UnknownFieldsPolicy::Deny,
824 });
825
826 let result = validate(&doc, &schema);
827 assert!(
828 result.is_valid,
829 "Expected flattened union + sibling flatten target to validate, got errors: {:?}",
830 result.errors
831 );
832 }
833 }
834
835 #[test]
836 fn test_flattened_union_best_match_ignores_sibling_consumed_fields() {
837 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
838 let name_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
839 let nickname_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
840 let age_schema_id =
841 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
842
843 let mut person_properties = IndexMap::new();
844 person_properties.insert(
845 "name".to_string(),
846 RecordFieldSchema {
847 schema: name_schema_id,
848 optional: false,
849 binding_style: None,
850 field_codegen: FieldCodegen::default(),
851 },
852 );
853 let person_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
854 properties: person_properties,
855 flatten: vec![],
856 unknown_fields: UnknownFieldsPolicy::Deny,
857 }));
858
859 let mut alias_properties = IndexMap::new();
860 alias_properties.insert(
861 "nickname".to_string(),
862 RecordFieldSchema {
863 schema: nickname_schema_id,
864 optional: false,
865 binding_style: None,
866 field_codegen: FieldCodegen::default(),
867 },
868 );
869 let alias_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
870 properties: alias_properties,
871 flatten: vec![],
872 unknown_fields: UnknownFieldsPolicy::Deny,
873 }));
874
875 let mut union_variants = IndexMap::new();
876 union_variants.insert("Person".to_string(), person_schema_id);
877 union_variants.insert("Alias".to_string(), alias_schema_id);
878 let union_schema_id = schema.create_node(SchemaNodeContent::Union(UnionSchema {
879 variants: union_variants,
880 unambiguous: IndexSet::new(),
881 interop: crate::interop::UnionInterop::default(),
882 deny_untagged: IndexSet::new(),
883 }));
884
885 let mut sibling_properties = IndexMap::new();
886 sibling_properties.insert(
887 "age".to_string(),
888 RecordFieldSchema {
889 schema: age_schema_id,
890 optional: false,
891 binding_style: None,
892 field_codegen: FieldCodegen::default(),
893 },
894 );
895 let sibling_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
896 properties: sibling_properties,
897 flatten: vec![],
898 unknown_fields: UnknownFieldsPolicy::Deny,
899 }));
900
901 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
902 properties: IndexMap::new(),
903 flatten: vec![union_schema_id, sibling_schema_id],
904 unknown_fields: UnknownFieldsPolicy::Deny,
905 });
906
907 let mut doc = EureDocument::new();
908 let root_id = doc.get_root_id();
909 let age_id = doc
910 .add_map_child(ObjectKey::String("age".to_string()), root_id)
911 .unwrap()
912 .node_id;
913 doc.node_mut(age_id).content =
914 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
915 let fax_id = doc
916 .add_map_child(ObjectKey::String("fax".to_string()), root_id)
917 .unwrap()
918 .node_id;
919 doc.node_mut(fax_id).content =
920 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("123".to_string())));
921
922 let result = validate(&doc, &schema);
923 assert!(!result.is_valid);
924
925 let no_variant_error = result
926 .errors
927 .iter()
928 .find_map(|error| match error {
929 ValidationError::NoVariantMatched {
930 best_match: Some(best_match),
931 ..
932 } => Some(best_match),
933 _ => None,
934 })
935 .expect("expected flattened union best match");
936
937 assert!(
938 no_variant_error.all_errors.iter().any(|error| matches!(
939 error,
940 ValidationError::UnknownField { field, .. } if field == "fax"
941 )),
942 "expected best match to retain globally unknown field"
943 );
944 assert!(
945 !no_variant_error.all_errors.iter().any(|error| matches!(
946 error,
947 ValidationError::UnknownField { field, .. } if field == "age"
948 )),
949 "best match should not treat sibling-consumed field as unknown"
950 );
951 }
952
953 #[test]
954 fn test_validate_hole() {
955 let (schema, _) =
956 create_simple_schema(SchemaNodeContent::Integer(IntegerSchema::default()));
957
958 let mut doc = EureDocument::new();
959 let root_id = doc.get_root_id();
960 doc.node_mut(root_id).content = NodeValue::Hole(None);
961
962 let result = validate(&doc, &schema);
963 assert!(result.is_valid);
964 assert!(!result.is_complete);
965 }
966
967 fn create_literal_schema(
969 schema: &mut SchemaDocument,
970 literal_doc: EureDocument,
971 ) -> SchemaNodeId {
972 schema.create_node(SchemaNodeContent::Literal(literal_doc))
973 }
974
975 #[test]
976 fn test_validate_union_deny_untagged_without_tag() {
977 use eure_document::eure;
978
979 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
981
982 let literal_schema_id = create_literal_schema(&mut schema, eure!({ = "active" }));
984
985 let mut variants = IndexMap::new();
987 variants.insert("literal".to_string(), literal_schema_id);
988
989 let mut deny_untagged = IndexSet::new();
990 deny_untagged.insert("literal".to_string());
991
992 schema.node_mut(schema.root).content = SchemaNodeContent::Union(UnionSchema {
993 variants,
994 unambiguous: IndexSet::new(),
995 interop: crate::interop::UnionInterop::default(),
996 deny_untagged,
997 });
998
999 let doc = eure!({ = "active" });
1001
1002 let result = validate(&doc, &schema);
1004 assert!(!result.is_valid);
1005 assert!(result.errors.iter().any(|e| matches!(
1006 e,
1007 ValidationError::RequiresExplicitVariant { variant, .. } if variant == "literal"
1008 )));
1009 }
1010
1011 #[test]
1012 fn test_validate_union_deny_untagged_with_tag() {
1013 use eure_document::eure;
1014
1015 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
1017
1018 let literal_schema_id = create_literal_schema(&mut schema, eure!({ = "active" }));
1020
1021 let mut variants = IndexMap::new();
1023 variants.insert("literal".to_string(), literal_schema_id);
1024
1025 let mut deny_untagged = IndexSet::new();
1026 deny_untagged.insert("literal".to_string());
1027
1028 schema.node_mut(schema.root).content = SchemaNodeContent::Union(UnionSchema {
1029 variants,
1030 unambiguous: IndexSet::new(),
1031 interop: crate::interop::UnionInterop::default(),
1032 deny_untagged,
1033 });
1034
1035 let doc = eure!({
1037 = "active"
1038 %variant = "literal"
1039 });
1040
1041 let result = validate(&doc, &schema);
1043 assert!(
1044 result.is_valid,
1045 "Expected valid, got errors: {:?}",
1046 result.errors
1047 );
1048 }
1049
1050 #[test]
1051 fn test_validate_union_mixed_deny_untagged() {
1052 use eure_document::eure;
1053
1054 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
1056
1057 let literal_active_id = create_literal_schema(&mut schema, eure!({ = "active" }));
1059
1060 let text_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
1062
1063 let mut variants = IndexMap::new();
1065 variants.insert("literal".to_string(), literal_active_id);
1066 variants.insert("text".to_string(), text_schema_id);
1067
1068 let mut deny_untagged = IndexSet::new();
1069 deny_untagged.insert("literal".to_string());
1070
1071 schema.node_mut(schema.root).content = SchemaNodeContent::Union(UnionSchema {
1072 variants,
1073 unambiguous: IndexSet::new(),
1074 interop: crate::interop::UnionInterop::default(),
1075 deny_untagged,
1076 });
1077
1078 let doc = eure!({ = "active" });
1081
1082 let result = validate(&doc, &schema);
1083 assert!(!result.is_valid);
1084 assert!(result.errors.iter().any(|e| matches!(
1085 e,
1086 ValidationError::RequiresExplicitVariant { variant, .. } if variant == "literal"
1087 )));
1088
1089 let doc2 = eure!({ = "other text" });
1091
1092 let result2 = validate(&doc2, &schema);
1093 assert!(
1094 result2.is_valid,
1095 "Expected valid for text match, got errors: {:?}",
1096 result2.errors
1097 );
1098 }
1099
1100 #[test]
1101 fn test_validate_union_internal_interop_does_not_count_as_explicit_tag() {
1102 use eure_document::eure;
1103
1104 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
1105
1106 let type_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
1107 let mut properties = IndexMap::new();
1108 properties.insert(
1109 "type".to_string(),
1110 RecordFieldSchema {
1111 schema: type_schema_id,
1112 optional: false,
1113 binding_style: None,
1114 field_codegen: FieldCodegen::default(),
1115 },
1116 );
1117 let success_record_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
1118 properties,
1119 flatten: vec![],
1120 unknown_fields: UnknownFieldsPolicy::Deny,
1121 }));
1122
1123 let mut variants = IndexMap::new();
1124 variants.insert("success".to_string(), success_record_id);
1125
1126 let mut deny_untagged = IndexSet::new();
1127 deny_untagged.insert("success".to_string());
1128
1129 schema.node_mut(schema.root).content = SchemaNodeContent::Union(UnionSchema {
1130 variants,
1131 unambiguous: IndexSet::new(),
1132 interop: crate::interop::UnionInterop {
1133 variant_repr: Some(crate::interop::VariantRepr::Internal {
1134 tag: "type".to_string(),
1135 }),
1136 },
1137 deny_untagged,
1138 });
1139
1140 let doc = eure!({ type = "success" });
1142 let result = validate(&doc, &schema);
1143 assert!(!result.is_valid);
1144 assert!(result.errors.iter().any(|e| matches!(
1145 e,
1146 ValidationError::RequiresExplicitVariant { variant, .. } if variant == "success"
1147 )));
1148
1149 let tagged_doc = eure!({
1151 type = "success"
1152 %variant = "success"
1153 });
1154 let tagged_result = validate(&tagged_doc, &schema);
1155 assert!(
1156 tagged_result.is_valid,
1157 "Expected valid with explicit $variant, got errors: {:?}",
1158 tagged_result.errors
1159 );
1160 }
1161
1162 #[test]
1163 fn test_validate_literal_with_inline_code() {
1164 use eure_document::eure;
1165
1166 let mut schema = SchemaDocument::new();
1168
1169 let literal_doc = eure!({ = @code("boolean") });
1171
1172 schema.node_mut(schema.root).content = SchemaNodeContent::Literal(literal_doc);
1173
1174 let doc = eure!({ = @code("boolean") });
1176
1177 let result = validate(&doc, &schema);
1179 assert!(
1180 result.is_valid,
1181 "Expected valid, got errors: {:?}",
1182 result.errors
1183 );
1184 }
1185
1186 #[test]
1187 fn test_validate_with_trace_covers_all_node_ids_and_is_deterministic() {
1188 use eure_document::eure;
1189
1190 let schema_doc = eure!({
1191 profile {
1192 name = @code("text")
1193 tags = [@code("text")]
1194 }
1195 active = @code("boolean")
1196 });
1197 let (schema, layout, _source_map) =
1198 document_to_schema_with_layout(&schema_doc).expect("schema conversion should succeed");
1199
1200 let input_doc = eure!({
1201 profile {
1202 name = "Alice"
1203 tags = ["core", "ops"]
1204 }
1205 active = true
1206 });
1207
1208 let first = validate_with_trace(&input_doc, &schema, &layout.schema_node_paths);
1209 let second = validate_with_trace(&input_doc, &schema, &layout.schema_node_paths);
1210
1211 assert_eq!(first.node_type_traces, second.node_type_traces);
1212 assert_eq!(first.node_type_traces.len(), input_doc.node_count());
1213
1214 for index in 0..input_doc.node_count() {
1215 assert!(
1216 first.node_type_traces.contains_key(&NodeId(index)),
1217 "missing trace for NodeId({index})"
1218 );
1219 }
1220
1221 assert!(
1222 first.node_type_traces.values().all(|trace| !matches!(
1223 trace,
1224 ResolvedTypeTrace::Unresolved(TypeTraceUnresolvedReason::NotVisited)
1225 )),
1226 "all reachable document nodes must be visited"
1227 );
1228 }
1229}