1use std::collections::{HashMap, HashSet};
6
7use crate::error::{ErrorKind, ValidationError};
8
9#[must_use]
14pub fn validate(schema: &Schema, value: &Spanned<RonValue>) -> Vec<ValidationError> {
15 let mut errors = Vec::new();
16 validate_struct(&schema.root, value, "", &mut errors, &schema.enums, &schema.aliases);
17 errors
18}
19use crate::ron::RonValue;
20use crate::schema::{EnumDef, Schema, SchemaType, StructDef};
21use crate::span::Spanned;
22
23fn describe(value: &RonValue) -> String {
25 match value {
26 RonValue::String(s) => {
27 if s.len() > 20 {
28 format!("String(\"{}...\")", &s[..20])
29 } else {
30 format!("String(\"{s}\")")
31 }
32 }
33 RonValue::Integer(n) => format!("Integer({n})"),
34 RonValue::Float(f) => format!("Float({f})"),
35 RonValue::Bool(b) => format!("Bool({b})"),
36 RonValue::Option(_) => "Option".to_string(),
37 RonValue::Identifier(s) => format!("Identifier({s})"),
38 RonValue::List(_) => "List".to_string(),
39 RonValue::Map(_) => "Map".to_string(),
40 RonValue::Tuple(_) => "Tuple".to_string(),
41 RonValue::Struct(_) => "Struct".to_string(),
42 }
43}
44
45fn build_path(parent: &str, field: &str) -> String {
50 if parent.is_empty() {
51 field.to_string()
52 } else {
53 format!("{parent}.{field}")
54 }
55}
56
57#[allow(clippy::too_many_lines)]
63fn validate_type(
64 expected: &SchemaType,
65 actual: &Spanned<RonValue>,
66 path: &str,
67 errors: &mut Vec<ValidationError>,
68 enums: &HashMap<String, EnumDef>,
69 aliases: &HashMap<String, Spanned<SchemaType>>,
70) {
71 match expected {
72 SchemaType::String => {
74 if !matches!(actual.value, RonValue::String(_)) {
75 errors.push(ValidationError {
76 path: path.to_string(),
77 span: actual.span,
78 kind: ErrorKind::TypeMismatch {
79 expected: "String".to_string(),
80 found: describe(&actual.value),
81 },
82 });
83 }
84 }
85 SchemaType::Integer => {
86 if !matches!(actual.value, RonValue::Integer(_)) {
87 errors.push(ValidationError {
88 path: path.to_string(),
89 span: actual.span,
90 kind: ErrorKind::TypeMismatch {
91 expected: "Integer".to_string(),
92 found: describe(&actual.value),
93 },
94 });
95 }
96 }
97 SchemaType::Float => {
98 if !matches!(actual.value, RonValue::Float(_)) {
99 errors.push(ValidationError {
100 path: path.to_string(),
101 span: actual.span,
102 kind: ErrorKind::TypeMismatch {
103 expected: "Float".to_string(),
104 found: describe(&actual.value),
105 },
106 });
107 }
108 }
109 SchemaType::Bool => {
110 if !matches!(actual.value, RonValue::Bool(_)) {
111 errors.push(ValidationError {
112 path: path.to_string(),
113 span: actual.span,
114 kind: ErrorKind::TypeMismatch {
115 expected: "Bool".to_string(),
116 found: describe(&actual.value),
117 },
118 });
119 }
120 }
121
122 SchemaType::Option(inner_type) => match &actual.value {
125 RonValue::Option(None) => {}
126 RonValue::Option(Some(inner_value)) => {
127 validate_type(inner_type, inner_value, path, errors, enums, aliases);
128 }
129 _ => {
130 errors.push(ValidationError {
131 path: path.to_string(),
132 span: actual.span,
133 kind: ErrorKind::ExpectedOption {
134 found: describe(&actual.value),
135 },
136 });
137 }
138 },
139
140 SchemaType::List(element_type) => {
143 if let RonValue::List(elements) = &actual.value {
144 for (index, element) in elements.iter().enumerate() {
145 let element_path = format!("{path}[{index}]");
146 validate_type(element_type, element, &element_path, errors, enums, aliases);
147 }
148 } else {
149 errors.push(ValidationError {
150 path: path.to_string(),
151 span: actual.span,
152 kind: ErrorKind::ExpectedList {
153 found: describe(&actual.value),
154 },
155 });
156 }
157 }
158
159 SchemaType::EnumRef(enum_name) => {
162 let enum_def = &enums[enum_name];
163 if let RonValue::Identifier(variant) = &actual.value {
164 if !enum_def.variants.contains(variant) {
165 errors.push(ValidationError {
166 path: path.to_string(),
167 span: actual.span,
168 kind: ErrorKind::InvalidEnumVariant {
169 enum_name: enum_name.clone(),
170 variant: variant.clone(),
171 valid: enum_def.variants.iter().cloned().collect(),
172 },
173 });
174 }
175 } else {
176 errors.push(ValidationError {
177 path: path.to_string(),
178 span: actual.span,
179 kind: ErrorKind::TypeMismatch {
180 expected: enum_name.clone(),
181 found: describe(&actual.value),
182 },
183 });
184 }
185 }
186
187 SchemaType::Map(key_type, value_type) => {
189 if let RonValue::Map(entries) = &actual.value {
190 for (key, value) in entries {
191 let key_desc = describe(&key.value);
192 validate_type(key_type, key, path, errors, enums, aliases);
193 let entry_path = format!("{path}[{key_desc}]");
194 validate_type(value_type, value, &entry_path, errors, enums, aliases);
195 }
196 } else {
197 errors.push(ValidationError {
198 path: path.to_string(),
199 span: actual.span,
200 kind: ErrorKind::ExpectedMap {
201 found: describe(&actual.value),
202 },
203 });
204 }
205 }
206
207 SchemaType::Tuple(element_types) => {
209 if let RonValue::Tuple(elements) = &actual.value {
210 if elements.len() == element_types.len() {
211 for (index, (expected_type, element)) in element_types.iter().zip(elements).enumerate() {
212 let element_path = format!("{path}.{index}");
213 validate_type(expected_type, element, &element_path, errors, enums, aliases);
214 }
215 } else {
216 errors.push(ValidationError {
217 path: path.to_string(),
218 span: actual.span,
219 kind: ErrorKind::TupleLengthMismatch {
220 expected: element_types.len(),
221 found: elements.len(),
222 },
223 });
224 }
225 } else {
226 errors.push(ValidationError {
227 path: path.to_string(),
228 span: actual.span,
229 kind: ErrorKind::ExpectedTuple {
230 found: describe(&actual.value),
231 },
232 });
233 }
234 }
235
236 SchemaType::AliasRef(alias_name) => {
239 if let Some(resolved) = aliases.get(alias_name) {
240 validate_type(&resolved.value, actual, path, errors, enums, aliases);
241 }
242 }
244
245 SchemaType::Struct(struct_def) => {
247 validate_struct(struct_def, actual, path, errors, enums, aliases);
248 }
249 }
250}
251
252fn validate_struct(
259 struct_def: &StructDef,
260 actual: &Spanned<RonValue>,
261 path: &str,
262 errors: &mut Vec<ValidationError>,
263 enums: &HashMap<String, EnumDef>,
264 aliases: &HashMap<String, Spanned<SchemaType>>,
265) {
266 let RonValue::Struct(data_struct) = &actual.value else {
268 errors.push(ValidationError {
269 path: path.to_string(),
270 span: actual.span,
271 kind: ErrorKind::ExpectedStruct {
272 found: describe(&actual.value),
273 },
274 });
275 return;
276 };
277
278 let data_map: HashMap<&str, &Spanned<RonValue>> = data_struct
280 .fields
281 .iter()
282 .map(|(name, value)| (name.value.as_str(), value))
283 .collect();
284
285 let schema_names: HashSet<&str> = struct_def
287 .fields
288 .iter()
289 .map(|f| f.name.value.as_str())
290 .collect();
291
292 for field_def in &struct_def.fields {
294 if !data_map.contains_key(field_def.name.value.as_str()) {
295 errors.push(ValidationError {
296 path: build_path(path, &field_def.name.value),
297 span: data_struct.close_span,
298 kind: ErrorKind::MissingField {
299 field_name: field_def.name.value.clone(),
300 },
301 });
302 }
303 }
304
305 for (name, _value) in &data_struct.fields {
307 if !schema_names.contains(name.value.as_str()) {
308 errors.push(ValidationError {
309 path: build_path(path, &name.value),
310 span: name.span,
311 kind: ErrorKind::UnknownField {
312 field_name: name.value.clone(),
313 },
314 });
315 }
316 }
317
318 for field_def in &struct_def.fields {
320 if let Some(data_value) = data_map.get(field_def.name.value.as_str()) {
321 let field_path = build_path(path, &field_def.name.value);
322 validate_type(&field_def.type_.value, data_value, &field_path, errors, enums, aliases);
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::schema::parser::parse_schema;
331 use crate::ron::parser::parse_ron;
332
333 fn validate_str(schema_src: &str, data_src: &str) -> Vec<ValidationError> {
335 let schema = parse_schema(schema_src).expect("test schema should parse");
336 let data = parse_ron(data_src).expect("test data should parse");
337 validate(&schema, &data)
338 }
339
340 #[test]
346 fn describe_string() {
347 assert_eq!(describe(&RonValue::String("hi".to_string())), "String(\"hi\")");
348 }
349
350 #[test]
352 fn describe_string_truncated() {
353 let long = "a".repeat(30);
354 let desc = describe(&RonValue::String(long));
355 assert!(desc.contains("..."));
356 }
357
358 #[test]
360 fn describe_integer() {
361 assert_eq!(describe(&RonValue::Integer(42)), "Integer(42)");
362 }
363
364 #[test]
366 fn describe_float() {
367 assert_eq!(describe(&RonValue::Float(3.14)), "Float(3.14)");
368 }
369
370 #[test]
372 fn describe_bool() {
373 assert_eq!(describe(&RonValue::Bool(true)), "Bool(true)");
374 }
375
376 #[test]
378 fn describe_identifier() {
379 assert_eq!(describe(&RonValue::Identifier("Creature".to_string())), "Identifier(Creature)");
380 }
381
382 #[test]
388 fn build_path_root() {
389 assert_eq!(build_path("", "name"), "name");
390 }
391
392 #[test]
394 fn build_path_nested() {
395 assert_eq!(build_path("cost", "generic"), "cost.generic");
396 }
397
398 #[test]
400 fn build_path_deep() {
401 assert_eq!(build_path("a.b", "c"), "a.b.c");
402 }
403
404 #[test]
410 fn valid_single_string_field() {
411 let errs = validate_str("(\n name: String,\n)", "(name: \"hello\")");
412 assert!(errs.is_empty());
413 }
414
415 #[test]
417 fn valid_all_primitives() {
418 let schema = "(\n s: String,\n i: Integer,\n f: Float,\n b: Bool,\n)";
419 let data = "(s: \"hi\", i: 42, f: 3.14, b: true)";
420 let errs = validate_str(schema, data);
421 assert!(errs.is_empty());
422 }
423
424 #[test]
426 fn valid_option_none() {
427 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: None)");
428 assert!(errs.is_empty());
429 }
430
431 #[test]
433 fn valid_option_some() {
434 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(5))");
435 assert!(errs.is_empty());
436 }
437
438 #[test]
440 fn valid_list_empty() {
441 let errs = validate_str("(\n tags: [String],\n)", "(tags: [])");
442 assert!(errs.is_empty());
443 }
444
445 #[test]
447 fn valid_list_populated() {
448 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"a\", \"b\"])");
449 assert!(errs.is_empty());
450 }
451
452 #[test]
454 fn valid_enum_variant() {
455 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B, C }";
456 let data = "(kind: B)";
457 let errs = validate_str(schema, data);
458 assert!(errs.is_empty());
459 }
460
461 #[test]
463 fn valid_enum_list() {
464 let schema = "(\n types: [CardType],\n)\nenum CardType { Creature, Trap }";
465 let data = "(types: [Creature, Trap])";
466 let errs = validate_str(schema, data);
467 assert!(errs.is_empty());
468 }
469
470 #[test]
472 fn valid_nested_struct() {
473 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
474 let data = "(cost: (generic: 2, sigil: 1))";
475 let errs = validate_str(schema, data);
476 assert!(errs.is_empty());
477 }
478
479 #[test]
485 fn type_mismatch_string_got_integer() {
486 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
487 assert_eq!(errs.len(), 1);
488 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
489 }
490
491 #[test]
493 fn type_mismatch_integer_got_string() {
494 let errs = validate_str("(\n age: Integer,\n)", "(age: \"five\")");
495 assert_eq!(errs.len(), 1);
496 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
497 }
498
499 #[test]
501 fn type_mismatch_float_got_integer() {
502 let errs = validate_str("(\n rate: Float,\n)", "(rate: 5)");
503 assert_eq!(errs.len(), 1);
504 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
505 }
506
507 #[test]
509 fn type_mismatch_bool_got_string() {
510 let errs = validate_str("(\n flag: Bool,\n)", "(flag: \"yes\")");
511 assert_eq!(errs.len(), 1);
512 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Bool"));
513 }
514
515 #[test]
517 fn type_mismatch_has_correct_path() {
518 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
519 assert_eq!(errs[0].path, "name");
520 }
521
522 #[test]
528 fn missing_field_detected() {
529 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
530 assert_eq!(errs.len(), 1);
531 assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "age"));
532 }
533
534 #[test]
536 fn missing_field_has_correct_path() {
537 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
538 assert_eq!(errs[0].path, "age");
539 }
540
541 #[test]
543 fn missing_field_span_points_to_close_paren() {
544 let data = "(name: \"hi\")";
545 let errs = validate_str("(\n name: String,\n age: Integer,\n)", data);
546 assert_eq!(errs[0].span.start.offset, data.len() - 1);
548 }
549
550 #[test]
552 fn missing_fields_all_reported() {
553 let errs = validate_str("(\n a: String,\n b: Integer,\n c: Bool,\n)", "()");
554 assert_eq!(errs.len(), 3);
555 }
556
557 #[test]
563 fn unknown_field_detected() {
564 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", colour: \"red\")");
565 assert_eq!(errs.len(), 1);
566 assert!(matches!(&errs[0].kind, ErrorKind::UnknownField { field_name } if field_name == "colour"));
567 }
568
569 #[test]
571 fn unknown_field_has_correct_path() {
572 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", extra: 5)");
573 assert_eq!(errs[0].path, "extra");
574 }
575
576 #[test]
582 fn invalid_enum_variant() {
583 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
584 let errs = validate_str(schema, "(kind: C)");
585 assert_eq!(errs.len(), 1);
586 assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { variant, .. } if variant == "C"));
587 }
588
589 #[test]
591 fn enum_rejects_string() {
592 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
593 let errs = validate_str(schema, "(kind: \"A\")");
594 assert_eq!(errs.len(), 1);
595 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
596 }
597
598 #[test]
604 fn expected_option_got_bare_value() {
605 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: 5)");
606 assert_eq!(errs.len(), 1);
607 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedOption { .. }));
608 }
609
610 #[test]
612 fn option_some_wrong_inner_type() {
613 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(\"five\"))");
614 assert_eq!(errs.len(), 1);
615 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
616 }
617
618 #[test]
624 fn expected_list_got_string() {
625 let errs = validate_str("(\n tags: [String],\n)", "(tags: \"hi\")");
626 assert_eq!(errs.len(), 1);
627 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedList { .. }));
628 }
629
630 #[test]
632 fn list_element_wrong_type() {
633 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
634 assert_eq!(errs.len(), 1);
635 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
636 }
637
638 #[test]
640 fn list_element_error_has_bracket_path() {
641 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
642 assert_eq!(errs[0].path, "tags[1]");
643 }
644
645 #[test]
651 fn expected_struct_got_integer() {
652 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
653 let errs = validate_str(schema, "(cost: 5)");
654 assert_eq!(errs.len(), 1);
655 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedStruct { .. }));
656 }
657
658 #[test]
664 fn nested_struct_type_mismatch_path() {
665 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
666 let errs = validate_str(schema, "(cost: (generic: \"two\"))");
667 assert_eq!(errs.len(), 1);
668 assert_eq!(errs[0].path, "cost.generic");
669 }
670
671 #[test]
673 fn nested_struct_missing_field_path() {
674 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
675 let errs = validate_str(schema, "(cost: (generic: 1))");
676 assert_eq!(errs.len(), 1);
677 assert_eq!(errs[0].path, "cost.sigil");
678 }
679
680 #[test]
686 fn multiple_errors_collected() {
687 let schema = "(\n name: String,\n age: Integer,\n active: Bool,\n)";
688 let data = "(name: 42, age: \"five\", active: \"yes\")";
689 let errs = validate_str(schema, data);
690 assert_eq!(errs.len(), 3);
691 }
692
693 #[test]
695 fn mixed_error_types_collected() {
696 let schema = "(\n name: String,\n age: Integer,\n)";
697 let data = "(name: \"hi\", age: \"five\", extra: true)";
698 let errs = validate_str(schema, data);
699 assert_eq!(errs.len(), 2);
701 }
702
703 #[test]
709 fn valid_card_data() {
710 let schema = r#"(
711 name: String,
712 card_types: [CardType],
713 legendary: Bool,
714 power: Option(Integer),
715 toughness: Option(Integer),
716 keywords: [String],
717 )
718 enum CardType { Creature, Trap, Artifact }"#;
719 let data = r#"(
720 name: "Ashborn Hound",
721 card_types: [Creature],
722 legendary: false,
723 power: Some(1),
724 toughness: Some(1),
725 keywords: [],
726 )"#;
727 let errs = validate_str(schema, data);
728 assert!(errs.is_empty());
729 }
730
731 #[test]
733 fn card_data_multiple_errors() {
734 let schema = r#"(
735 name: String,
736 card_types: [CardType],
737 legendary: Bool,
738 power: Option(Integer),
739 )
740 enum CardType { Creature, Trap }"#;
741 let data = r#"(
742 name: 42,
743 card_types: [Pirates],
744 legendary: false,
745 power: Some("five"),
746 )"#;
747 let errs = validate_str(schema, data);
748 assert_eq!(errs.len(), 3);
750 }
751
752 #[test]
758 fn alias_struct_valid() {
759 let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
760 let data = "(cost: (generic: 5))";
761 let errs = validate_str(schema, data);
762 assert!(errs.is_empty());
763 }
764
765 #[test]
767 fn alias_struct_type_mismatch() {
768 let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
769 let data = "(cost: (generic: \"five\"))";
770 let errs = validate_str(schema, data);
771 assert_eq!(errs.len(), 1);
772 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
773 }
774
775 #[test]
777 fn alias_primitive_valid() {
778 let schema = "(\n name: Name,\n)\ntype Name = String";
779 let data = "(name: \"hello\")";
780 let errs = validate_str(schema, data);
781 assert!(errs.is_empty());
782 }
783
784 #[test]
786 fn alias_primitive_mismatch() {
787 let schema = "(\n name: Name,\n)\ntype Name = String";
788 let data = "(name: 42)";
789 let errs = validate_str(schema, data);
790 assert_eq!(errs.len(), 1);
791 }
792
793 #[test]
795 fn alias_in_list_valid() {
796 let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
797 let data = "(costs: [(generic: 1), (generic: 2)])";
798 let errs = validate_str(schema, data);
799 assert!(errs.is_empty());
800 }
801
802 #[test]
804 fn alias_in_list_element_error() {
805 let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
806 let data = "(costs: [(generic: 1), (generic: \"two\")])";
807 let errs = validate_str(schema, data);
808 assert_eq!(errs.len(), 1);
809 assert_eq!(errs[0].path, "costs[1].generic");
810 }
811
812 #[test]
818 fn map_valid() {
819 let schema = "(\n attrs: {String: Integer},\n)";
820 let data = "(attrs: {\"str\": 5, \"dex\": 3})";
821 let errs = validate_str(schema, data);
822 assert!(errs.is_empty());
823 }
824
825 #[test]
827 fn map_empty_valid() {
828 let schema = "(\n attrs: {String: Integer},\n)";
829 let data = "(attrs: {})";
830 let errs = validate_str(schema, data);
831 assert!(errs.is_empty());
832 }
833
834 #[test]
836 fn map_expected_got_string() {
837 let schema = "(\n attrs: {String: Integer},\n)";
838 let data = "(attrs: \"not a map\")";
839 let errs = validate_str(schema, data);
840 assert_eq!(errs.len(), 1);
841 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedMap { .. }));
842 }
843
844 #[test]
846 fn map_wrong_value_type() {
847 let schema = "(\n attrs: {String: Integer},\n)";
848 let data = "(attrs: {\"str\": \"five\"})";
849 let errs = validate_str(schema, data);
850 assert_eq!(errs.len(), 1);
851 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
852 }
853
854 #[test]
856 fn map_wrong_key_type() {
857 let schema = "(\n attrs: {String: Integer},\n)";
858 let data = "(attrs: {42: 5})";
859 let errs = validate_str(schema, data);
860 assert_eq!(errs.len(), 1);
861 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
862 }
863
864 #[test]
870 fn tuple_valid() {
871 let schema = "(\n pos: (Float, Float),\n)";
872 let data = "(pos: (1.0, 2.5))";
873 let errs = validate_str(schema, data);
874 assert!(errs.is_empty());
875 }
876
877 #[test]
879 fn tuple_expected_got_string() {
880 let schema = "(\n pos: (Float, Float),\n)";
881 let data = "(pos: \"not a tuple\")";
882 let errs = validate_str(schema, data);
883 assert_eq!(errs.len(), 1);
884 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedTuple { .. }));
885 }
886
887 #[test]
889 fn tuple_wrong_length() {
890 let schema = "(\n pos: (Float, Float),\n)";
891 let data = "(pos: (1.0, 2.5, 3.0))";
892 let errs = validate_str(schema, data);
893 assert_eq!(errs.len(), 1);
894 assert!(matches!(&errs[0].kind, ErrorKind::TupleLengthMismatch { expected: 2, found: 3 }));
895 }
896
897 #[test]
899 fn tuple_wrong_element_type() {
900 let schema = "(\n pos: (Float, Float),\n)";
901 let data = "(pos: (1.0, \"bad\"))";
902 let errs = validate_str(schema, data);
903 assert_eq!(errs.len(), 1);
904 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
905 }
906
907 #[test]
909 fn tuple_element_error_path() {
910 let schema = "(\n pos: (Float, Float),\n)";
911 let data = "(pos: (1.0, \"bad\"))";
912 let errs = validate_str(schema, data);
913 assert_eq!(errs[0].path, "pos.1");
914 }
915}