1use std::collections::{HashMap, HashSet};
6
7use crate::error::{ErrorKind, ValidationError, ValidationResult, Warning, WarningKind};
8
9#[must_use]
14pub fn validate(schema: &Schema, value: &Spanned<RonValue>) -> ValidationResult {
15 let mut errors = Vec::new();
16 let mut warnings = Vec::new();
17 validate_struct(&schema.root, value, "", &mut errors, &mut warnings, &schema.enums, &schema.aliases);
18 ValidationResult { errors, warnings }
19}
20use crate::ron::RonValue;
21use crate::schema::{EnumDef, Schema, SchemaType, StructDef};
22use crate::span::{Span, Spanned};
23
24fn describe(value: &RonValue) -> String {
26 match value {
27 RonValue::String(s) => {
28 if s.len() > 20 {
29 format!("String(\"{}...\")", &s[..20])
30 } else {
31 format!("String(\"{s}\")")
32 }
33 }
34 RonValue::Integer(n) => format!("Integer({n})"),
35 RonValue::Float(f) => format!("Float({f})"),
36 RonValue::Bool(b) => format!("Bool({b})"),
37 RonValue::Option(_) => "Option".to_string(),
38 RonValue::Identifier(s) => format!("Identifier({s})"),
39 RonValue::EnumVariant(name, _) => format!("{name}(...)"),
40 RonValue::List(_) => "List".to_string(),
41 RonValue::Map(_) => "Map".to_string(),
42 RonValue::Tuple(_) => "Tuple".to_string(),
43 RonValue::Struct(_) => "Struct".to_string(),
44 }
45}
46
47fn build_path(parent: &str, field: &str) -> String {
52 if parent.is_empty() {
53 field.to_string()
54 } else {
55 format!("{parent}.{field}")
56 }
57}
58
59#[allow(clippy::too_many_lines)]
65fn validate_type(
66 expected: &SchemaType,
67 actual: &Spanned<RonValue>,
68 path: &str,
69 errors: &mut Vec<ValidationError>,
70 warnings: &mut Vec<Warning>,
71 enums: &HashMap<String, EnumDef>,
72 aliases: &HashMap<String, Spanned<SchemaType>>,
73) {
74 match expected {
75 SchemaType::String => {
77 if !matches!(actual.value, RonValue::String(_)) {
78 errors.push(ValidationError {
79 path: path.to_string(),
80 span: actual.span,
81 kind: ErrorKind::TypeMismatch {
82 expected: "String".to_string(),
83 found: describe(&actual.value),
84 },
85 });
86 }
87 }
88 SchemaType::Integer => {
89 if !matches!(actual.value, RonValue::Integer(_)) {
90 errors.push(ValidationError {
91 path: path.to_string(),
92 span: actual.span,
93 kind: ErrorKind::TypeMismatch {
94 expected: "Integer".to_string(),
95 found: describe(&actual.value),
96 },
97 });
98 }
99 }
100 SchemaType::Float => {
101 if !matches!(actual.value, RonValue::Float(_)) {
102 errors.push(ValidationError {
103 path: path.to_string(),
104 span: actual.span,
105 kind: ErrorKind::TypeMismatch {
106 expected: "Float".to_string(),
107 found: describe(&actual.value),
108 },
109 });
110 }
111 }
112 SchemaType::Bool => {
113 if !matches!(actual.value, RonValue::Bool(_)) {
114 errors.push(ValidationError {
115 path: path.to_string(),
116 span: actual.span,
117 kind: ErrorKind::TypeMismatch {
118 expected: "Bool".to_string(),
119 found: describe(&actual.value),
120 },
121 });
122 }
123 }
124
125 SchemaType::Option(inner_type) => match &actual.value {
128 RonValue::Option(None) => {}
129 RonValue::Option(Some(inner_value)) => {
130 validate_type(inner_type, inner_value, path, errors, warnings, enums, aliases);
131 }
132 _ => {
133 errors.push(ValidationError {
134 path: path.to_string(),
135 span: actual.span,
136 kind: ErrorKind::ExpectedOption {
137 found: describe(&actual.value),
138 },
139 });
140 }
141 },
142
143 SchemaType::List(element_type) => {
146 if let RonValue::List(elements) = &actual.value {
147 for (index, element) in elements.iter().enumerate() {
148 let element_path = format!("{path}[{index}]");
149 validate_type(element_type, element, &element_path, errors, warnings, enums, aliases);
150 }
151 } else {
152 errors.push(ValidationError {
153 path: path.to_string(),
154 span: actual.span,
155 kind: ErrorKind::ExpectedList {
156 found: describe(&actual.value),
157 },
158 });
159 }
160 }
161
162 SchemaType::EnumRef(enum_name) => {
166 let enum_def = &enums[enum_name];
167 let variant_names: Vec<String> = enum_def.variants.keys().cloned().collect();
168
169 match &actual.value {
170 RonValue::Identifier(variant) => {
172 match enum_def.variants.get(variant) {
173 None => {
174 errors.push(ValidationError {
175 path: path.to_string(),
176 span: actual.span,
177 kind: ErrorKind::InvalidEnumVariant {
178 enum_name: enum_name.clone(),
179 variant: variant.clone(),
180 valid: variant_names,
181 },
182 });
183 }
184 Some(Some(_expected_data_type)) => {
185 errors.push(ValidationError {
187 path: path.to_string(),
188 span: actual.span,
189 kind: ErrorKind::InvalidVariantData {
190 enum_name: enum_name.clone(),
191 variant: variant.clone(),
192 expected: "data".to_string(),
193 found: "unit variant".to_string(),
194 },
195 });
196 }
197 Some(None) => {} }
199 }
200 RonValue::EnumVariant(variant, data) => {
202 match enum_def.variants.get(variant) {
203 None => {
204 errors.push(ValidationError {
205 path: path.to_string(),
206 span: actual.span,
207 kind: ErrorKind::InvalidEnumVariant {
208 enum_name: enum_name.clone(),
209 variant: variant.clone(),
210 valid: variant_names,
211 },
212 });
213 }
214 Some(None) => {
215 errors.push(ValidationError {
217 path: path.to_string(),
218 span: actual.span,
219 kind: ErrorKind::InvalidVariantData {
220 enum_name: enum_name.clone(),
221 variant: variant.clone(),
222 expected: "unit variant".to_string(),
223 found: describe(&data.value),
224 },
225 });
226 }
227 Some(Some(expected_data_type)) => {
228 validate_type(expected_data_type, data, path, errors, warnings, enums, aliases);
230 }
231 }
232 }
233 _ => {
235 errors.push(ValidationError {
236 path: path.to_string(),
237 span: actual.span,
238 kind: ErrorKind::TypeMismatch {
239 expected: enum_name.clone(),
240 found: describe(&actual.value),
241 },
242 });
243 }
244 }
245 }
246
247 SchemaType::Map(key_type, value_type) => {
249 if let RonValue::Map(entries) = &actual.value {
250 for (key, value) in entries {
251 let key_desc = describe(&key.value);
252 validate_type(key_type, key, path, errors, warnings, enums, aliases);
253 let entry_path = format!("{path}[{key_desc}]");
254 validate_type(value_type, value, &entry_path, errors, warnings, enums, aliases);
255 }
256 } else {
257 errors.push(ValidationError {
258 path: path.to_string(),
259 span: actual.span,
260 kind: ErrorKind::ExpectedMap {
261 found: describe(&actual.value),
262 },
263 });
264 }
265 }
266
267 SchemaType::Tuple(element_types) => {
269 if let RonValue::Tuple(elements) = &actual.value {
270 if elements.len() == element_types.len() {
271 for (index, (expected_type, element)) in element_types.iter().zip(elements).enumerate() {
272 let element_path = format!("{path}.{index}");
273 validate_type(expected_type, element, &element_path, errors, warnings, enums, aliases);
274 }
275 } else {
276 errors.push(ValidationError {
277 path: path.to_string(),
278 span: actual.span,
279 kind: ErrorKind::TupleLengthMismatch {
280 expected: element_types.len(),
281 found: elements.len(),
282 },
283 });
284 }
285 } else {
286 errors.push(ValidationError {
287 path: path.to_string(),
288 span: actual.span,
289 kind: ErrorKind::ExpectedTuple {
290 found: describe(&actual.value),
291 },
292 });
293 }
294 }
295
296 SchemaType::AliasRef(alias_name) => {
299 if let Some(resolved) = aliases.get(alias_name) {
300 validate_type(&resolved.value, actual, path, errors, warnings, enums, aliases);
301 }
302 }
304
305 SchemaType::Struct(struct_def) => {
307 validate_struct(struct_def, actual, path, errors, warnings, enums, aliases);
308 }
309 }
310}
311
312fn validate_struct(
319 struct_def: &StructDef,
320 actual: &Spanned<RonValue>,
321 path: &str,
322 errors: &mut Vec<ValidationError>,
323 warnings: &mut Vec<Warning>,
324 enums: &HashMap<String, EnumDef>,
325 aliases: &HashMap<String, Spanned<SchemaType>>,
326) {
327 let RonValue::Struct(data_struct) = &actual.value else {
329 errors.push(ValidationError {
330 path: path.to_string(),
331 span: actual.span,
332 kind: ErrorKind::ExpectedStruct {
333 found: describe(&actual.value),
334 },
335 });
336 return;
337 };
338
339 let data_map: HashMap<&str, &Spanned<RonValue>> = data_struct
341 .fields
342 .iter()
343 .map(|(name, value)| (name.value.as_str(), value))
344 .collect();
345
346 let schema_names: HashSet<&str> = struct_def
348 .fields
349 .iter()
350 .map(|f| f.name.value.as_str())
351 .collect();
352
353 for field_def in &struct_def.fields {
355 if !data_map.contains_key(field_def.name.value.as_str()) && field_def.default.is_none() {
356 errors.push(ValidationError {
357 path: build_path(path, &field_def.name.value),
358 span: data_struct.close_span,
359 kind: ErrorKind::MissingField {
360 field_name: field_def.name.value.clone(),
361 },
362 });
363 }
364 }
365
366 for (name, _value) in &data_struct.fields {
368 if !schema_names.contains(name.value.as_str()) {
369 errors.push(ValidationError {
370 path: build_path(path, &name.value),
371 span: name.span,
372 kind: ErrorKind::UnknownField {
373 field_name: name.value.clone(),
374 },
375 });
376 }
377 }
378
379 for field_def in &struct_def.fields {
381 if let Some(data_value) = data_map.get(field_def.name.value.as_str()) {
382 let field_path = build_path(path, &field_def.name.value);
383 validate_type(&field_def.type_.value, data_value, &field_path, errors, warnings, enums, aliases);
384 }
385 }
386
387 let schema_order: Vec<&str> = struct_def.fields.iter()
389 .map(|f| f.name.value.as_str())
390 .collect();
391 let data_fields: Vec<(&str, Span)> = data_struct.fields.iter()
392 .map(|(name, _)| (name.value.as_str(), name.span))
393 .collect();
394 let mut last_schema_index = 0;
395 for (data_name, data_span) in &data_fields {
396 if let Some(schema_index) = schema_order.iter().position(|&s| s == *data_name) {
397 if schema_index < last_schema_index {
398 let expected_after = schema_order[last_schema_index];
400 warnings.push(Warning {
401 path: build_path(path, data_name),
402 span: *data_span,
403 kind: WarningKind::FieldOrderMismatch {
404 field_name: data_name.to_string(),
405 expected_after: expected_after.to_string(),
406 },
407 });
408 } else {
409 last_schema_index = schema_index;
410 }
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use crate::schema::parser::parse_schema;
419 use crate::ron::parser::parse_ron;
420
421 fn validate_str(schema_src: &str, data_src: &str) -> Vec<ValidationError> {
423 validate_full(schema_src, data_src).errors
424 }
425
426 fn validate_full(schema_src: &str, data_src: &str) -> ValidationResult {
428 let schema = parse_schema(schema_src).expect("test schema should parse");
429 let data = parse_ron(data_src).expect("test data should parse");
430 validate(&schema, &data)
431 }
432
433 #[test]
439 fn describe_string() {
440 assert_eq!(describe(&RonValue::String("hi".to_string())), "String(\"hi\")");
441 }
442
443 #[test]
445 fn describe_string_truncated() {
446 let long = "a".repeat(30);
447 let desc = describe(&RonValue::String(long));
448 assert!(desc.contains("..."));
449 }
450
451 #[test]
453 fn describe_integer() {
454 assert_eq!(describe(&RonValue::Integer(42)), "Integer(42)");
455 }
456
457 #[test]
459 fn describe_float() {
460 assert_eq!(describe(&RonValue::Float(3.14)), "Float(3.14)");
461 }
462
463 #[test]
465 fn describe_bool() {
466 assert_eq!(describe(&RonValue::Bool(true)), "Bool(true)");
467 }
468
469 #[test]
471 fn describe_identifier() {
472 assert_eq!(describe(&RonValue::Identifier("Creature".to_string())), "Identifier(Creature)");
473 }
474
475 #[test]
481 fn build_path_root() {
482 assert_eq!(build_path("", "name"), "name");
483 }
484
485 #[test]
487 fn build_path_nested() {
488 assert_eq!(build_path("cost", "generic"), "cost.generic");
489 }
490
491 #[test]
493 fn build_path_deep() {
494 assert_eq!(build_path("a.b", "c"), "a.b.c");
495 }
496
497 #[test]
503 fn valid_single_string_field() {
504 let errs = validate_str("(\n name: String,\n)", "(name: \"hello\")");
505 assert!(errs.is_empty());
506 }
507
508 #[test]
510 fn valid_all_primitives() {
511 let schema = "(\n s: String,\n i: Integer,\n f: Float,\n b: Bool,\n)";
512 let data = "(s: \"hi\", i: 42, f: 3.14, b: true)";
513 let errs = validate_str(schema, data);
514 assert!(errs.is_empty());
515 }
516
517 #[test]
519 fn valid_option_none() {
520 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: None)");
521 assert!(errs.is_empty());
522 }
523
524 #[test]
526 fn valid_option_some() {
527 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(5))");
528 assert!(errs.is_empty());
529 }
530
531 #[test]
533 fn valid_list_empty() {
534 let errs = validate_str("(\n tags: [String],\n)", "(tags: [])");
535 assert!(errs.is_empty());
536 }
537
538 #[test]
540 fn valid_list_populated() {
541 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"a\", \"b\"])");
542 assert!(errs.is_empty());
543 }
544
545 #[test]
547 fn valid_enum_variant() {
548 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B, C }";
549 let data = "(kind: B)";
550 let errs = validate_str(schema, data);
551 assert!(errs.is_empty());
552 }
553
554 #[test]
556 fn valid_enum_list() {
557 let schema = "(\n types: [CardType],\n)\nenum CardType { Creature, Trap }";
558 let data = "(types: [Creature, Trap])";
559 let errs = validate_str(schema, data);
560 assert!(errs.is_empty());
561 }
562
563 #[test]
565 fn valid_nested_struct() {
566 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
567 let data = "(cost: (generic: 2, sigil: 1))";
568 let errs = validate_str(schema, data);
569 assert!(errs.is_empty());
570 }
571
572 #[test]
578 fn type_mismatch_string_got_integer() {
579 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
580 assert_eq!(errs.len(), 1);
581 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
582 }
583
584 #[test]
586 fn type_mismatch_integer_got_string() {
587 let errs = validate_str("(\n age: Integer,\n)", "(age: \"five\")");
588 assert_eq!(errs.len(), 1);
589 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
590 }
591
592 #[test]
594 fn type_mismatch_float_got_integer() {
595 let errs = validate_str("(\n rate: Float,\n)", "(rate: 5)");
596 assert_eq!(errs.len(), 1);
597 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
598 }
599
600 #[test]
602 fn type_mismatch_bool_got_string() {
603 let errs = validate_str("(\n flag: Bool,\n)", "(flag: \"yes\")");
604 assert_eq!(errs.len(), 1);
605 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Bool"));
606 }
607
608 #[test]
610 fn type_mismatch_has_correct_path() {
611 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
612 assert_eq!(errs[0].path, "name");
613 }
614
615 #[test]
621 fn missing_field_detected() {
622 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
623 assert_eq!(errs.len(), 1);
624 assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "age"));
625 }
626
627 #[test]
629 fn missing_field_has_correct_path() {
630 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
631 assert_eq!(errs[0].path, "age");
632 }
633
634 #[test]
636 fn missing_field_span_points_to_close_paren() {
637 let data = "(name: \"hi\")";
638 let errs = validate_str("(\n name: String,\n age: Integer,\n)", data);
639 assert_eq!(errs[0].span.start.offset, data.len() - 1);
641 }
642
643 #[test]
645 fn missing_fields_all_reported() {
646 let errs = validate_str("(\n a: String,\n b: Integer,\n c: Bool,\n)", "()");
647 assert_eq!(errs.len(), 3);
648 }
649
650 #[test]
656 fn field_order_correct_no_warning() {
657 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(a: \"hi\", b: 1)");
658 assert!(result.warnings.is_empty());
659 }
660
661 #[test]
663 fn field_order_swapped_produces_warning() {
664 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
665 assert_eq!(result.warnings.len(), 1);
666 }
667
668 #[test]
670 fn field_order_warning_has_correct_kind() {
671 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
672 assert!(matches!(&result.warnings[0].kind, WarningKind::FieldOrderMismatch { .. }));
673 }
674
675 #[test]
677 fn field_order_warning_identifies_field() {
678 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
679 if let WarningKind::FieldOrderMismatch { field_name, .. } = &result.warnings[0].kind {
680 assert_eq!(field_name, "a");
681 } else {
682 panic!("expected FieldOrderMismatch");
683 }
684 }
685
686 #[test]
688 fn field_order_warning_identifies_expected_after() {
689 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
690 if let WarningKind::FieldOrderMismatch { expected_after, .. } = &result.warnings[0].kind {
691 assert_eq!(expected_after, "b");
692 } else {
693 panic!("expected FieldOrderMismatch");
694 }
695 }
696
697 #[test]
699 fn field_order_warning_has_correct_path() {
700 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
701 assert_eq!(result.warnings[0].path, "a");
702 }
703
704 #[test]
706 fn field_order_warning_has_span() {
707 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
708 assert!(result.warnings[0].span.start.line > 0);
709 }
710
711 #[test]
713 fn field_order_three_fields_correct() {
714 let result = validate_full(
715 "(\n a: String,\n b: Integer,\n c: Bool,\n)",
716 "(a: \"hi\", b: 1, c: true)",
717 );
718 assert!(result.warnings.is_empty());
719 }
720
721 #[test]
723 fn field_order_middle_field_swapped() {
724 let result = validate_full(
725 "(\n a: String,\n b: Integer,\n c: Bool,\n)",
726 "(a: \"hi\", c: true, b: 1)",
727 );
728 assert_eq!(result.warnings.len(), 1);
729 if let WarningKind::FieldOrderMismatch { field_name, .. } = &result.warnings[0].kind {
730 assert_eq!(field_name, "b");
731 } else {
732 panic!("expected FieldOrderMismatch");
733 }
734 }
735
736 #[test]
738 fn field_order_warning_no_errors() {
739 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
740 assert!(result.errors.is_empty());
741 }
742
743 #[test]
745 fn field_order_with_unknown_field() {
746 let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, x: true, a: \"hi\")");
747 assert_eq!(result.warnings.len(), 1);
748 }
749
750 #[test]
756 fn default_field_not_required() {
757 let errs = validate_str("(\n name: String,\n label: String = \"none\",\n)", "(name: \"hi\")");
758 assert!(errs.is_empty());
759 }
760
761 #[test]
763 fn default_field_still_validates_type() {
764 let errs = validate_str("(\n name: String,\n label: String = \"none\",\n)", "(name: \"hi\", label: 42)");
765 assert_eq!(errs.len(), 1);
766 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
767 }
768
769 #[test]
771 fn default_field_accepts_correct_type() {
772 let errs = validate_str("(\n name: String,\n label: String = \"none\",\n)", "(name: \"hi\", label: \"custom\")");
773 assert!(errs.is_empty());
774 }
775
776 #[test]
778 fn non_default_field_still_required() {
779 let errs = validate_str("(\n name: String,\n label: String = \"none\",\n)", "(label: \"hi\")");
780 assert_eq!(errs.len(), 1);
781 assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "name"));
782 }
783
784 #[test]
786 fn multiple_default_fields_all_absent() {
787 let errs = validate_str(
788 "(\n name: String,\n a: Integer = 0,\n b: Bool = false,\n c: String = \"x\",\n)",
789 "(name: \"hi\")",
790 );
791 assert!(errs.is_empty());
792 }
793
794 #[test]
796 fn default_option_field_not_required() {
797 let errs = validate_str("(\n name: String,\n tag: Option(String) = None,\n)", "(name: \"hi\")");
798 assert!(errs.is_empty());
799 }
800
801 #[test]
803 fn default_list_field_not_required() {
804 let errs = validate_str("(\n name: String,\n tags: [String] = [],\n)", "(name: \"hi\")");
805 assert!(errs.is_empty());
806 }
807
808 #[test]
814 fn unknown_field_detected() {
815 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", colour: \"red\")");
816 assert_eq!(errs.len(), 1);
817 assert!(matches!(&errs[0].kind, ErrorKind::UnknownField { field_name } if field_name == "colour"));
818 }
819
820 #[test]
822 fn unknown_field_has_correct_path() {
823 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", extra: 5)");
824 assert_eq!(errs[0].path, "extra");
825 }
826
827 #[test]
833 fn invalid_enum_variant() {
834 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
835 let errs = validate_str(schema, "(kind: C)");
836 assert_eq!(errs.len(), 1);
837 assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { variant, .. } if variant == "C"));
838 }
839
840 #[test]
842 fn enum_rejects_string() {
843 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
844 let errs = validate_str(schema, "(kind: \"A\")");
845 assert_eq!(errs.len(), 1);
846 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
847 }
848
849 #[test]
855 fn expected_option_got_bare_value() {
856 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: 5)");
857 assert_eq!(errs.len(), 1);
858 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedOption { .. }));
859 }
860
861 #[test]
863 fn option_some_wrong_inner_type() {
864 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(\"five\"))");
865 assert_eq!(errs.len(), 1);
866 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
867 }
868
869 #[test]
875 fn expected_list_got_string() {
876 let errs = validate_str("(\n tags: [String],\n)", "(tags: \"hi\")");
877 assert_eq!(errs.len(), 1);
878 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedList { .. }));
879 }
880
881 #[test]
883 fn list_element_wrong_type() {
884 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
885 assert_eq!(errs.len(), 1);
886 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
887 }
888
889 #[test]
891 fn list_element_error_has_bracket_path() {
892 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
893 assert_eq!(errs[0].path, "tags[1]");
894 }
895
896 #[test]
902 fn expected_struct_got_integer() {
903 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
904 let errs = validate_str(schema, "(cost: 5)");
905 assert_eq!(errs.len(), 1);
906 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedStruct { .. }));
907 }
908
909 #[test]
915 fn nested_struct_type_mismatch_path() {
916 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
917 let errs = validate_str(schema, "(cost: (generic: \"two\"))");
918 assert_eq!(errs.len(), 1);
919 assert_eq!(errs[0].path, "cost.generic");
920 }
921
922 #[test]
924 fn nested_struct_missing_field_path() {
925 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
926 let errs = validate_str(schema, "(cost: (generic: 1))");
927 assert_eq!(errs.len(), 1);
928 assert_eq!(errs[0].path, "cost.sigil");
929 }
930
931 #[test]
937 fn multiple_errors_collected() {
938 let schema = "(\n name: String,\n age: Integer,\n active: Bool,\n)";
939 let data = "(name: 42, age: \"five\", active: \"yes\")";
940 let errs = validate_str(schema, data);
941 assert_eq!(errs.len(), 3);
942 }
943
944 #[test]
946 fn mixed_error_types_collected() {
947 let schema = "(\n name: String,\n age: Integer,\n)";
948 let data = "(name: \"hi\", age: \"five\", extra: true)";
949 let errs = validate_str(schema, data);
950 assert_eq!(errs.len(), 2);
952 }
953
954 #[test]
960 fn valid_card_data() {
961 let schema = r#"(
962 name: String,
963 card_types: [CardType],
964 legendary: Bool,
965 power: Option(Integer),
966 toughness: Option(Integer),
967 keywords: [String],
968 )
969 enum CardType { Creature, Trap, Artifact }"#;
970 let data = r#"(
971 name: "Ashborn Hound",
972 card_types: [Creature],
973 legendary: false,
974 power: Some(1),
975 toughness: Some(1),
976 keywords: [],
977 )"#;
978 let errs = validate_str(schema, data);
979 assert!(errs.is_empty());
980 }
981
982 #[test]
984 fn card_data_multiple_errors() {
985 let schema = r#"(
986 name: String,
987 card_types: [CardType],
988 legendary: Bool,
989 power: Option(Integer),
990 )
991 enum CardType { Creature, Trap }"#;
992 let data = r#"(
993 name: 42,
994 card_types: [Pirates],
995 legendary: false,
996 power: Some("five"),
997 )"#;
998 let errs = validate_str(schema, data);
999 assert_eq!(errs.len(), 3);
1001 }
1002
1003 #[test]
1009 fn alias_struct_valid() {
1010 let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
1011 let data = "(cost: (generic: 5))";
1012 let errs = validate_str(schema, data);
1013 assert!(errs.is_empty());
1014 }
1015
1016 #[test]
1018 fn alias_struct_type_mismatch() {
1019 let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
1020 let data = "(cost: (generic: \"five\"))";
1021 let errs = validate_str(schema, data);
1022 assert_eq!(errs.len(), 1);
1023 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
1024 }
1025
1026 #[test]
1028 fn alias_primitive_valid() {
1029 let schema = "(\n name: Name,\n)\ntype Name = String";
1030 let data = "(name: \"hello\")";
1031 let errs = validate_str(schema, data);
1032 assert!(errs.is_empty());
1033 }
1034
1035 #[test]
1037 fn alias_primitive_mismatch() {
1038 let schema = "(\n name: Name,\n)\ntype Name = String";
1039 let data = "(name: 42)";
1040 let errs = validate_str(schema, data);
1041 assert_eq!(errs.len(), 1);
1042 }
1043
1044 #[test]
1046 fn alias_in_list_valid() {
1047 let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
1048 let data = "(costs: [(generic: 1), (generic: 2)])";
1049 let errs = validate_str(schema, data);
1050 assert!(errs.is_empty());
1051 }
1052
1053 #[test]
1055 fn alias_in_list_element_error() {
1056 let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
1057 let data = "(costs: [(generic: 1), (generic: \"two\")])";
1058 let errs = validate_str(schema, data);
1059 assert_eq!(errs.len(), 1);
1060 assert_eq!(errs[0].path, "costs[1].generic");
1061 }
1062
1063 #[test]
1069 fn map_valid() {
1070 let schema = "(\n attrs: {String: Integer},\n)";
1071 let data = "(attrs: {\"str\": 5, \"dex\": 3})";
1072 let errs = validate_str(schema, data);
1073 assert!(errs.is_empty());
1074 }
1075
1076 #[test]
1078 fn map_empty_valid() {
1079 let schema = "(\n attrs: {String: Integer},\n)";
1080 let data = "(attrs: {})";
1081 let errs = validate_str(schema, data);
1082 assert!(errs.is_empty());
1083 }
1084
1085 #[test]
1087 fn map_expected_got_string() {
1088 let schema = "(\n attrs: {String: Integer},\n)";
1089 let data = "(attrs: \"not a map\")";
1090 let errs = validate_str(schema, data);
1091 assert_eq!(errs.len(), 1);
1092 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedMap { .. }));
1093 }
1094
1095 #[test]
1097 fn map_wrong_value_type() {
1098 let schema = "(\n attrs: {String: Integer},\n)";
1099 let data = "(attrs: {\"str\": \"five\"})";
1100 let errs = validate_str(schema, data);
1101 assert_eq!(errs.len(), 1);
1102 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
1103 }
1104
1105 #[test]
1107 fn map_wrong_key_type() {
1108 let schema = "(\n attrs: {String: Integer},\n)";
1109 let data = "(attrs: {42: 5})";
1110 let errs = validate_str(schema, data);
1111 assert_eq!(errs.len(), 1);
1112 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
1113 }
1114
1115 #[test]
1121 fn tuple_valid() {
1122 let schema = "(\n pos: (Float, Float),\n)";
1123 let data = "(pos: (1.0, 2.5))";
1124 let errs = validate_str(schema, data);
1125 assert!(errs.is_empty());
1126 }
1127
1128 #[test]
1130 fn tuple_expected_got_string() {
1131 let schema = "(\n pos: (Float, Float),\n)";
1132 let data = "(pos: \"not a tuple\")";
1133 let errs = validate_str(schema, data);
1134 assert_eq!(errs.len(), 1);
1135 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedTuple { .. }));
1136 }
1137
1138 #[test]
1140 fn tuple_wrong_length() {
1141 let schema = "(\n pos: (Float, Float),\n)";
1142 let data = "(pos: (1.0, 2.5, 3.0))";
1143 let errs = validate_str(schema, data);
1144 assert_eq!(errs.len(), 1);
1145 assert!(matches!(&errs[0].kind, ErrorKind::TupleLengthMismatch { expected: 2, found: 3 }));
1146 }
1147
1148 #[test]
1150 fn tuple_wrong_element_type() {
1151 let schema = "(\n pos: (Float, Float),\n)";
1152 let data = "(pos: (1.0, \"bad\"))";
1153 let errs = validate_str(schema, data);
1154 assert_eq!(errs.len(), 1);
1155 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
1156 }
1157
1158 #[test]
1160 fn tuple_element_error_path() {
1161 let schema = "(\n pos: (Float, Float),\n)";
1162 let data = "(pos: (1.0, \"bad\"))";
1163 let errs = validate_str(schema, data);
1164 assert_eq!(errs[0].path, "pos.1");
1165 }
1166
1167 #[test]
1173 fn enum_data_variant_valid() {
1174 let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1175 let data = "(effect: Damage(5))";
1176 let errs = validate_str(schema, data);
1177 assert!(errs.is_empty());
1178 }
1179
1180 #[test]
1182 fn enum_unit_variant_valid() {
1183 let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1184 let data = "(effect: Draw)";
1185 let errs = validate_str(schema, data);
1186 assert!(errs.is_empty());
1187 }
1188
1189 #[test]
1191 fn enum_data_variant_wrong_type() {
1192 let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1193 let data = "(effect: Damage(\"five\"))";
1194 let errs = validate_str(schema, data);
1195 assert_eq!(errs.len(), 1);
1196 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
1197 }
1198
1199 #[test]
1201 fn enum_data_variant_unknown() {
1202 let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1203 let data = "(effect: Explode(10))";
1204 let errs = validate_str(schema, data);
1205 assert_eq!(errs.len(), 1);
1206 assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { .. }));
1207 }
1208
1209 #[test]
1211 fn enum_missing_data() {
1212 let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1213 let data = "(effect: Damage)";
1214 let errs = validate_str(schema, data);
1215 assert_eq!(errs.len(), 1);
1216 assert!(matches!(&errs[0].kind, ErrorKind::InvalidVariantData { .. }));
1217 }
1218
1219 #[test]
1221 fn enum_unexpected_data() {
1222 let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1223 let data = "(effect: Draw(5))";
1224 let errs = validate_str(schema, data);
1225 assert_eq!(errs.len(), 1);
1226 assert!(matches!(&errs[0].kind, ErrorKind::InvalidVariantData { .. }));
1227 }
1228}