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 root_codegen: RootCodegen::default(),
385 codegen_defaults: CodegenDefaults::default(),
386 };
387 let id = schema.create_node(content);
388 schema.root = id;
389 (schema, id)
390 }
391
392 fn create_doc_with_primitive(value: PrimitiveValue) -> EureDocument {
393 let mut doc = EureDocument::new();
394 let root_id = doc.get_root_id();
395 doc.node_mut(root_id).content = NodeValue::Primitive(value);
396 doc
397 }
398
399 #[test]
400 fn test_validate_text_basic() {
401 let (schema, _) = create_simple_schema(SchemaNodeContent::Text(TextSchema::default()));
402 let doc =
403 create_doc_with_primitive(PrimitiveValue::Text(Text::plaintext("hello".to_string())));
404 let result = validate(&doc, &schema);
405 assert!(result.is_valid);
406 }
407
408 #[test]
409 fn test_validate_text_pattern() {
410 let (schema, _) = create_simple_schema(SchemaNodeContent::Text(TextSchema {
411 pattern: Some(regex::Regex::new("^[a-z]+$").unwrap()),
412 ..Default::default()
413 }));
414
415 let doc =
416 create_doc_with_primitive(PrimitiveValue::Text(Text::plaintext("hello".to_string())));
417 let result = validate(&doc, &schema);
418 assert!(result.is_valid);
419
420 let doc = create_doc_with_primitive(PrimitiveValue::Text(Text::plaintext(
421 "Hello123".to_string(),
422 )));
423 let result = validate(&doc, &schema);
424 assert!(!result.is_valid);
425 }
426
427 #[test]
428 fn test_validate_integer() {
429 let (schema, _) = create_simple_schema(SchemaNodeContent::Integer(IntegerSchema {
430 min: Bound::Inclusive(BigInt::from(0)),
431 max: Bound::Inclusive(BigInt::from(100)),
432 multiple_of: None,
433 }));
434
435 let doc = create_doc_with_primitive(PrimitiveValue::Integer(BigInt::from(50)));
436 let result = validate(&doc, &schema);
437 assert!(result.is_valid);
438
439 let doc = create_doc_with_primitive(PrimitiveValue::Integer(BigInt::from(150)));
440 let result = validate(&doc, &schema);
441 assert!(!result.is_valid);
442 }
443
444 #[test]
445 fn test_validate_boolean() {
446 let (schema, _) = create_simple_schema(SchemaNodeContent::Boolean);
447
448 let doc = create_doc_with_primitive(PrimitiveValue::Bool(true));
449 let result = validate(&doc, &schema);
450 assert!(result.is_valid);
451
452 let doc = create_doc_with_primitive(PrimitiveValue::Integer(BigInt::from(1)));
453 let result = validate(&doc, &schema);
454 assert!(!result.is_valid);
455 }
456
457 #[test]
458 fn test_validate_array() {
459 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
460 let item_schema_id =
461 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
462 schema.node_mut(schema.root).content = SchemaNodeContent::Array(ArraySchema {
463 item: item_schema_id,
464 min_length: Some(1),
465 max_length: Some(3),
466 unique: false,
467 contains: None,
468 binding_style: None,
469 });
470
471 let mut doc = EureDocument::new();
472 let root_id = doc.get_root_id();
473 doc.node_mut(root_id).content = NodeValue::Array(Default::default());
474 let child1 = doc.add_array_element(None, root_id).unwrap().node_id;
475 doc.node_mut(child1).content =
476 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(1)));
477 let child2 = doc.add_array_element(None, root_id).unwrap().node_id;
478 doc.node_mut(child2).content =
479 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(2)));
480
481 let result = validate(&doc, &schema);
482 assert!(result.is_valid);
483 }
484
485 #[test]
486 fn test_validate_map_with_union_key_schema() {
487 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
488 let text_key_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
489 let int_key_schema_id =
490 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
491 let any_value_schema_id = schema.create_node(SchemaNodeContent::Any);
492
493 let mut variants = IndexMap::new();
494 variants.insert("text".to_string(), text_key_schema_id);
495 variants.insert("integer".to_string(), int_key_schema_id);
496 let union_key_schema_id = schema.create_node(SchemaNodeContent::Union(UnionSchema {
497 variants,
498 unambiguous: IndexSet::new(),
499 interop: crate::interop::UnionInterop::default(),
500 deny_untagged: IndexSet::new(),
501 }));
502
503 schema.node_mut(schema.root).content = SchemaNodeContent::Map(MapSchema {
504 key: union_key_schema_id,
505 value: any_value_schema_id,
506 min_size: None,
507 max_size: None,
508 });
509
510 let mut doc = EureDocument::new();
511 let root_id = doc.get_root_id();
512
513 let text_value_id = doc
514 .add_map_child(ObjectKey::String("name".to_string()), root_id)
515 .unwrap()
516 .node_id;
517 doc.node_mut(text_value_id).content =
518 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice".to_string())));
519
520 let int_value_id = doc
521 .add_map_child(ObjectKey::Number(BigInt::from(1)), root_id)
522 .unwrap()
523 .node_id;
524 doc.node_mut(int_value_id).content =
525 NodeValue::Primitive(PrimitiveValue::Integer(42.into()));
526
527 let result = validate(&doc, &schema);
528 assert!(
529 result.is_valid,
530 "Expected union key schema to validate: {:?}",
531 result.errors
532 );
533 }
534
535 #[test]
536 fn test_validate_map_with_reference_to_union_key_schema() {
537 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
538 let text_key_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
539 let int_key_schema_id =
540 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
541 let any_value_schema_id = schema.create_node(SchemaNodeContent::Any);
542
543 let mut variants = IndexMap::new();
544 variants.insert("text".to_string(), text_key_schema_id);
545 variants.insert("integer".to_string(), int_key_schema_id);
546 let union_key_schema_id = schema.create_node(SchemaNodeContent::Union(UnionSchema {
547 variants,
548 unambiguous: IndexSet::new(),
549 interop: crate::interop::UnionInterop::default(),
550 deny_untagged: IndexSet::new(),
551 }));
552 schema.register_type(Identifier::new_unchecked("key"), union_key_schema_id);
553
554 let key_ref_schema_id = schema.create_node(SchemaNodeContent::Reference(TypeReference {
555 namespace: None,
556 name: Identifier::new_unchecked("key"),
557 }));
558
559 schema.node_mut(schema.root).content = SchemaNodeContent::Map(MapSchema {
560 key: key_ref_schema_id,
561 value: any_value_schema_id,
562 min_size: None,
563 max_size: None,
564 });
565
566 let mut doc = EureDocument::new();
567 let root_id = doc.get_root_id();
568 let value_id = doc
569 .add_map_child(ObjectKey::Number(BigInt::from(7)), root_id)
570 .unwrap()
571 .node_id;
572 doc.node_mut(value_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
573
574 let result = validate(&doc, &schema);
575 assert!(
576 result.is_valid,
577 "Expected reference to union key schema to validate: {:?}",
578 result.errors
579 );
580 }
581
582 #[test]
583 fn test_validate_record_flattened_map_boolean_key() {
584 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
588 let bool_key_schema_id = schema.create_node(SchemaNodeContent::Boolean);
589 let any_value_schema_id = schema.create_node(SchemaNodeContent::Any);
590 let map_schema_id = schema.create_node(SchemaNodeContent::Map(MapSchema {
591 key: bool_key_schema_id,
592 value: any_value_schema_id,
593 min_size: None,
594 max_size: None,
595 }));
596 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
597 properties: IndexMap::new(),
598 flatten: vec![map_schema_id],
599 unknown_fields: UnknownFieldsPolicy::Deny,
600 });
601
602 let mut doc = EureDocument::new();
603 let root_id = doc.get_root_id();
604 let value_id = doc
605 .add_map_child(ObjectKey::String("true".to_string()), root_id)
606 .unwrap()
607 .node_id;
608 doc.node_mut(value_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
609
610 let result = validate(&doc, &schema);
611 assert!(
612 result.is_valid,
613 "Expected boolean key 'true' to be valid against Boolean key schema in flattened map: {:?}",
614 result.errors
615 );
616 }
617
618 #[test]
619 fn test_validate_record() {
620 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
621 let name_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
622 let age_schema_id =
623 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
624
625 let mut properties = IndexMap::new();
626 properties.insert(
627 "name".to_string(),
628 RecordFieldSchema {
629 schema: name_schema_id,
630 optional: false,
631 binding_style: None,
632 field_codegen: FieldCodegen::default(),
633 },
634 );
635 properties.insert(
636 "age".to_string(),
637 RecordFieldSchema {
638 schema: age_schema_id,
639 optional: true,
640 binding_style: None,
641 field_codegen: FieldCodegen::default(),
642 },
643 );
644
645 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
646 properties,
647 flatten: vec![],
648 unknown_fields: UnknownFieldsPolicy::Deny,
649 });
650
651 let mut doc = EureDocument::new();
652 let root_id = doc.get_root_id();
653 let name_id = doc
654 .add_map_child(ObjectKey::String("name".to_string()), root_id)
655 .unwrap()
656 .node_id;
657 doc.node_mut(name_id).content =
658 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice".to_string())));
659
660 let result = validate(&doc, &schema);
661 assert!(result.is_valid);
662 }
663
664 #[test]
665 fn test_validate_record_with_sibling_flatten_targets() {
666 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
667 let name_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
668 let age_schema_id =
669 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
670
671 let mut left_properties = IndexMap::new();
672 left_properties.insert(
673 "name".to_string(),
674 RecordFieldSchema {
675 schema: name_schema_id,
676 optional: false,
677 binding_style: None,
678 field_codegen: FieldCodegen::default(),
679 },
680 );
681 let left_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
682 properties: left_properties,
683 flatten: vec![],
684 unknown_fields: UnknownFieldsPolicy::Deny,
685 }));
686
687 let mut right_properties = IndexMap::new();
688 right_properties.insert(
689 "age".to_string(),
690 RecordFieldSchema {
691 schema: age_schema_id,
692 optional: false,
693 binding_style: None,
694 field_codegen: FieldCodegen::default(),
695 },
696 );
697 let right_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
698 properties: right_properties,
699 flatten: vec![],
700 unknown_fields: UnknownFieldsPolicy::Deny,
701 }));
702
703 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
704 properties: IndexMap::new(),
705 flatten: vec![left_schema_id, right_schema_id],
706 unknown_fields: UnknownFieldsPolicy::Deny,
707 });
708
709 let mut doc = EureDocument::new();
710 let root_id = doc.get_root_id();
711 let name_id = doc
712 .add_map_child(ObjectKey::String("name".to_string()), root_id)
713 .unwrap()
714 .node_id;
715 doc.node_mut(name_id).content =
716 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice".to_string())));
717 let age_id = doc
718 .add_map_child(ObjectKey::String("age".to_string()), root_id)
719 .unwrap()
720 .node_id;
721 doc.node_mut(age_id).content =
722 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
723
724 let result = validate(&doc, &schema);
725 assert!(
726 result.is_valid,
727 "Expected sibling flatten targets to validate, got errors: {:?}",
728 result.errors
729 );
730 }
731
732 #[test]
733 fn test_validate_record_with_flattened_union_and_sibling_flatten_targets() {
734 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
735 let name_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
736 let nickname_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
737 let age_schema_id =
738 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
739
740 let mut person_properties = IndexMap::new();
741 person_properties.insert(
742 "name".to_string(),
743 RecordFieldSchema {
744 schema: name_schema_id,
745 optional: false,
746 binding_style: None,
747 field_codegen: FieldCodegen::default(),
748 },
749 );
750 let person_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
751 properties: person_properties,
752 flatten: vec![],
753 unknown_fields: UnknownFieldsPolicy::Deny,
754 }));
755
756 let mut alias_properties = IndexMap::new();
757 alias_properties.insert(
758 "nickname".to_string(),
759 RecordFieldSchema {
760 schema: nickname_schema_id,
761 optional: false,
762 binding_style: None,
763 field_codegen: FieldCodegen::default(),
764 },
765 );
766 let alias_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
767 properties: alias_properties,
768 flatten: vec![],
769 unknown_fields: UnknownFieldsPolicy::Deny,
770 }));
771
772 let mut union_variants = IndexMap::new();
773 union_variants.insert("Person".to_string(), person_schema_id);
774 union_variants.insert("Alias".to_string(), alias_schema_id);
775 let union_schema_id = schema.create_node(SchemaNodeContent::Union(UnionSchema {
776 variants: union_variants,
777 unambiguous: IndexSet::new(),
778 interop: crate::interop::UnionInterop::default(),
779 deny_untagged: IndexSet::new(),
780 }));
781
782 let mut sibling_properties = IndexMap::new();
783 sibling_properties.insert(
784 "age".to_string(),
785 RecordFieldSchema {
786 schema: age_schema_id,
787 optional: false,
788 binding_style: None,
789 field_codegen: FieldCodegen::default(),
790 },
791 );
792 let sibling_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
793 properties: sibling_properties,
794 flatten: vec![],
795 unknown_fields: UnknownFieldsPolicy::Deny,
796 }));
797
798 let mut doc = EureDocument::new();
799 let root_id = doc.get_root_id();
800 let name_id = doc
801 .add_map_child(ObjectKey::String("name".to_string()), root_id)
802 .unwrap()
803 .node_id;
804 doc.node_mut(name_id).content =
805 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice".to_string())));
806 let age_id = doc
807 .add_map_child(ObjectKey::String("age".to_string()), root_id)
808 .unwrap()
809 .node_id;
810 doc.node_mut(age_id).content =
811 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
812
813 for flatten in [
814 vec![union_schema_id, sibling_schema_id],
815 vec![sibling_schema_id, union_schema_id],
816 ] {
817 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
818 properties: IndexMap::new(),
819 flatten,
820 unknown_fields: UnknownFieldsPolicy::Deny,
821 });
822
823 let result = validate(&doc, &schema);
824 assert!(
825 result.is_valid,
826 "Expected flattened union + sibling flatten target to validate, got errors: {:?}",
827 result.errors
828 );
829 }
830 }
831
832 #[test]
833 fn test_flattened_union_best_match_ignores_sibling_consumed_fields() {
834 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
835 let name_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
836 let nickname_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
837 let age_schema_id =
838 schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
839
840 let mut person_properties = IndexMap::new();
841 person_properties.insert(
842 "name".to_string(),
843 RecordFieldSchema {
844 schema: name_schema_id,
845 optional: false,
846 binding_style: None,
847 field_codegen: FieldCodegen::default(),
848 },
849 );
850 let person_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
851 properties: person_properties,
852 flatten: vec![],
853 unknown_fields: UnknownFieldsPolicy::Deny,
854 }));
855
856 let mut alias_properties = IndexMap::new();
857 alias_properties.insert(
858 "nickname".to_string(),
859 RecordFieldSchema {
860 schema: nickname_schema_id,
861 optional: false,
862 binding_style: None,
863 field_codegen: FieldCodegen::default(),
864 },
865 );
866 let alias_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
867 properties: alias_properties,
868 flatten: vec![],
869 unknown_fields: UnknownFieldsPolicy::Deny,
870 }));
871
872 let mut union_variants = IndexMap::new();
873 union_variants.insert("Person".to_string(), person_schema_id);
874 union_variants.insert("Alias".to_string(), alias_schema_id);
875 let union_schema_id = schema.create_node(SchemaNodeContent::Union(UnionSchema {
876 variants: union_variants,
877 unambiguous: IndexSet::new(),
878 interop: crate::interop::UnionInterop::default(),
879 deny_untagged: IndexSet::new(),
880 }));
881
882 let mut sibling_properties = IndexMap::new();
883 sibling_properties.insert(
884 "age".to_string(),
885 RecordFieldSchema {
886 schema: age_schema_id,
887 optional: false,
888 binding_style: None,
889 field_codegen: FieldCodegen::default(),
890 },
891 );
892 let sibling_schema_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
893 properties: sibling_properties,
894 flatten: vec![],
895 unknown_fields: UnknownFieldsPolicy::Deny,
896 }));
897
898 schema.node_mut(schema.root).content = SchemaNodeContent::Record(RecordSchema {
899 properties: IndexMap::new(),
900 flatten: vec![union_schema_id, sibling_schema_id],
901 unknown_fields: UnknownFieldsPolicy::Deny,
902 });
903
904 let mut doc = EureDocument::new();
905 let root_id = doc.get_root_id();
906 let age_id = doc
907 .add_map_child(ObjectKey::String("age".to_string()), root_id)
908 .unwrap()
909 .node_id;
910 doc.node_mut(age_id).content =
911 NodeValue::Primitive(PrimitiveValue::Integer(BigInt::from(42)));
912 let fax_id = doc
913 .add_map_child(ObjectKey::String("fax".to_string()), root_id)
914 .unwrap()
915 .node_id;
916 doc.node_mut(fax_id).content =
917 NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("123".to_string())));
918
919 let result = validate(&doc, &schema);
920 assert!(!result.is_valid);
921
922 let no_variant_error = result
923 .errors
924 .iter()
925 .find_map(|error| match error {
926 ValidationError::NoVariantMatched {
927 best_match: Some(best_match),
928 ..
929 } => Some(best_match),
930 _ => None,
931 })
932 .expect("expected flattened union best match");
933
934 assert!(
935 no_variant_error.all_errors.iter().any(|error| matches!(
936 error,
937 ValidationError::UnknownField { field, .. } if field == "fax"
938 )),
939 "expected best match to retain globally unknown field"
940 );
941 assert!(
942 !no_variant_error.all_errors.iter().any(|error| matches!(
943 error,
944 ValidationError::UnknownField { field, .. } if field == "age"
945 )),
946 "best match should not treat sibling-consumed field as unknown"
947 );
948 }
949
950 #[test]
951 fn test_validate_hole() {
952 let (schema, _) =
953 create_simple_schema(SchemaNodeContent::Integer(IntegerSchema::default()));
954
955 let mut doc = EureDocument::new();
956 let root_id = doc.get_root_id();
957 doc.node_mut(root_id).content = NodeValue::Hole(None);
958
959 let result = validate(&doc, &schema);
960 assert!(result.is_valid);
961 assert!(!result.is_complete);
962 }
963
964 fn create_literal_schema(
966 schema: &mut SchemaDocument,
967 literal_doc: EureDocument,
968 ) -> SchemaNodeId {
969 schema.create_node(SchemaNodeContent::Literal(literal_doc))
970 }
971
972 #[test]
973 fn test_validate_union_deny_untagged_without_tag() {
974 use eure_document::eure;
975
976 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
978
979 let literal_schema_id = create_literal_schema(&mut schema, eure!({ = "active" }));
981
982 let mut variants = IndexMap::new();
984 variants.insert("literal".to_string(), literal_schema_id);
985
986 let mut deny_untagged = IndexSet::new();
987 deny_untagged.insert("literal".to_string());
988
989 schema.node_mut(schema.root).content = SchemaNodeContent::Union(UnionSchema {
990 variants,
991 unambiguous: IndexSet::new(),
992 interop: crate::interop::UnionInterop::default(),
993 deny_untagged,
994 });
995
996 let doc = eure!({ = "active" });
998
999 let result = validate(&doc, &schema);
1001 assert!(!result.is_valid);
1002 assert!(result.errors.iter().any(|e| matches!(
1003 e,
1004 ValidationError::RequiresExplicitVariant { variant, .. } if variant == "literal"
1005 )));
1006 }
1007
1008 #[test]
1009 fn test_validate_union_deny_untagged_with_tag() {
1010 use eure_document::eure;
1011
1012 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
1014
1015 let literal_schema_id = create_literal_schema(&mut schema, eure!({ = "active" }));
1017
1018 let mut variants = IndexMap::new();
1020 variants.insert("literal".to_string(), literal_schema_id);
1021
1022 let mut deny_untagged = IndexSet::new();
1023 deny_untagged.insert("literal".to_string());
1024
1025 schema.node_mut(schema.root).content = SchemaNodeContent::Union(UnionSchema {
1026 variants,
1027 unambiguous: IndexSet::new(),
1028 interop: crate::interop::UnionInterop::default(),
1029 deny_untagged,
1030 });
1031
1032 let doc = eure!({
1034 = "active"
1035 %variant = "literal"
1036 });
1037
1038 let result = validate(&doc, &schema);
1040 assert!(
1041 result.is_valid,
1042 "Expected valid, got errors: {:?}",
1043 result.errors
1044 );
1045 }
1046
1047 #[test]
1048 fn test_validate_union_mixed_deny_untagged() {
1049 use eure_document::eure;
1050
1051 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
1053
1054 let literal_active_id = create_literal_schema(&mut schema, eure!({ = "active" }));
1056
1057 let text_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
1059
1060 let mut variants = IndexMap::new();
1062 variants.insert("literal".to_string(), literal_active_id);
1063 variants.insert("text".to_string(), text_schema_id);
1064
1065 let mut deny_untagged = IndexSet::new();
1066 deny_untagged.insert("literal".to_string());
1067
1068 schema.node_mut(schema.root).content = SchemaNodeContent::Union(UnionSchema {
1069 variants,
1070 unambiguous: IndexSet::new(),
1071 interop: crate::interop::UnionInterop::default(),
1072 deny_untagged,
1073 });
1074
1075 let doc = eure!({ = "active" });
1078
1079 let result = validate(&doc, &schema);
1080 assert!(!result.is_valid);
1081 assert!(result.errors.iter().any(|e| matches!(
1082 e,
1083 ValidationError::RequiresExplicitVariant { variant, .. } if variant == "literal"
1084 )));
1085
1086 let doc2 = eure!({ = "other text" });
1088
1089 let result2 = validate(&doc2, &schema);
1090 assert!(
1091 result2.is_valid,
1092 "Expected valid for text match, got errors: {:?}",
1093 result2.errors
1094 );
1095 }
1096
1097 #[test]
1098 fn test_validate_union_internal_interop_does_not_count_as_explicit_tag() {
1099 use eure_document::eure;
1100
1101 let (mut schema, _) = create_simple_schema(SchemaNodeContent::Any);
1102
1103 let type_schema_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
1104 let mut properties = IndexMap::new();
1105 properties.insert(
1106 "type".to_string(),
1107 RecordFieldSchema {
1108 schema: type_schema_id,
1109 optional: false,
1110 binding_style: None,
1111 field_codegen: FieldCodegen::default(),
1112 },
1113 );
1114 let success_record_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
1115 properties,
1116 flatten: vec![],
1117 unknown_fields: UnknownFieldsPolicy::Deny,
1118 }));
1119
1120 let mut variants = IndexMap::new();
1121 variants.insert("success".to_string(), success_record_id);
1122
1123 let mut deny_untagged = IndexSet::new();
1124 deny_untagged.insert("success".to_string());
1125
1126 schema.node_mut(schema.root).content = SchemaNodeContent::Union(UnionSchema {
1127 variants,
1128 unambiguous: IndexSet::new(),
1129 interop: crate::interop::UnionInterop {
1130 variant_repr: Some(crate::interop::VariantRepr::Internal {
1131 tag: "type".to_string(),
1132 }),
1133 },
1134 deny_untagged,
1135 });
1136
1137 let doc = eure!({ type = "success" });
1139 let result = validate(&doc, &schema);
1140 assert!(!result.is_valid);
1141 assert!(result.errors.iter().any(|e| matches!(
1142 e,
1143 ValidationError::RequiresExplicitVariant { variant, .. } if variant == "success"
1144 )));
1145
1146 let tagged_doc = eure!({
1148 type = "success"
1149 %variant = "success"
1150 });
1151 let tagged_result = validate(&tagged_doc, &schema);
1152 assert!(
1153 tagged_result.is_valid,
1154 "Expected valid with explicit $variant, got errors: {:?}",
1155 tagged_result.errors
1156 );
1157 }
1158
1159 #[test]
1160 fn test_validate_literal_with_inline_code() {
1161 use eure_document::eure;
1162
1163 let mut schema = SchemaDocument::new();
1165
1166 let literal_doc = eure!({ = @code("boolean") });
1168
1169 schema.node_mut(schema.root).content = SchemaNodeContent::Literal(literal_doc);
1170
1171 let doc = eure!({ = @code("boolean") });
1173
1174 let result = validate(&doc, &schema);
1176 assert!(
1177 result.is_valid,
1178 "Expected valid, got errors: {:?}",
1179 result.errors
1180 );
1181 }
1182
1183 #[test]
1184 fn test_validate_with_trace_covers_all_node_ids_and_is_deterministic() {
1185 use eure_document::eure;
1186
1187 let schema_doc = eure!({
1188 profile {
1189 name = @code("text")
1190 tags = [@code("text")]
1191 }
1192 active = @code("boolean")
1193 });
1194 let (schema, layout, _source_map) =
1195 document_to_schema_with_layout(&schema_doc).expect("schema conversion should succeed");
1196
1197 let input_doc = eure!({
1198 profile {
1199 name = "Alice"
1200 tags = ["core", "ops"]
1201 }
1202 active = true
1203 });
1204
1205 let first = validate_with_trace(&input_doc, &schema, &layout.schema_node_paths);
1206 let second = validate_with_trace(&input_doc, &schema, &layout.schema_node_paths);
1207
1208 assert_eq!(first.node_type_traces, second.node_type_traces);
1209 assert_eq!(first.node_type_traces.len(), input_doc.node_count());
1210
1211 for index in 0..input_doc.node_count() {
1212 assert!(
1213 first.node_type_traces.contains_key(&NodeId(index)),
1214 "missing trace for NodeId({index})"
1215 );
1216 }
1217
1218 assert!(
1219 first.node_type_traces.values().all(|trace| !matches!(
1220 trace,
1221 ResolvedTypeTrace::Unresolved(TypeTraceUnresolvedReason::NotVisited)
1222 )),
1223 "all reachable document nodes must be visited"
1224 );
1225 }
1226}