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::Struct(_) => "Struct".to_string(),
41 }
42}
43
44fn build_path(parent: &str, field: &str) -> String {
49 if parent.is_empty() {
50 field.to_string()
51 } else {
52 format!("{parent}.{field}")
53 }
54}
55
56#[allow(clippy::too_many_lines)]
62fn validate_type(
63 expected: &SchemaType,
64 actual: &Spanned<RonValue>,
65 path: &str,
66 errors: &mut Vec<ValidationError>,
67 enums: &HashMap<String, EnumDef>,
68 aliases: &HashMap<String, Spanned<SchemaType>>,
69) {
70 match expected {
71 SchemaType::String => {
73 if !matches!(actual.value, RonValue::String(_)) {
74 errors.push(ValidationError {
75 path: path.to_string(),
76 span: actual.span,
77 kind: ErrorKind::TypeMismatch {
78 expected: "String".to_string(),
79 found: describe(&actual.value),
80 },
81 });
82 }
83 }
84 SchemaType::Integer => {
85 if !matches!(actual.value, RonValue::Integer(_)) {
86 errors.push(ValidationError {
87 path: path.to_string(),
88 span: actual.span,
89 kind: ErrorKind::TypeMismatch {
90 expected: "Integer".to_string(),
91 found: describe(&actual.value),
92 },
93 });
94 }
95 }
96 SchemaType::Float => {
97 if !matches!(actual.value, RonValue::Float(_)) {
98 errors.push(ValidationError {
99 path: path.to_string(),
100 span: actual.span,
101 kind: ErrorKind::TypeMismatch {
102 expected: "Float".to_string(),
103 found: describe(&actual.value),
104 },
105 });
106 }
107 }
108 SchemaType::Bool => {
109 if !matches!(actual.value, RonValue::Bool(_)) {
110 errors.push(ValidationError {
111 path: path.to_string(),
112 span: actual.span,
113 kind: ErrorKind::TypeMismatch {
114 expected: "Bool".to_string(),
115 found: describe(&actual.value),
116 },
117 });
118 }
119 }
120
121 SchemaType::Option(inner_type) => match &actual.value {
124 RonValue::Option(None) => {}
125 RonValue::Option(Some(inner_value)) => {
126 validate_type(inner_type, inner_value, path, errors, enums, aliases);
127 }
128 _ => {
129 errors.push(ValidationError {
130 path: path.to_string(),
131 span: actual.span,
132 kind: ErrorKind::ExpectedOption {
133 found: describe(&actual.value),
134 },
135 });
136 }
137 },
138
139 SchemaType::List(element_type) => {
142 if let RonValue::List(elements) = &actual.value {
143 for (index, element) in elements.iter().enumerate() {
144 let element_path = format!("{path}[{index}]");
145 validate_type(element_type, element, &element_path, errors, enums, aliases);
146 }
147 } else {
148 errors.push(ValidationError {
149 path: path.to_string(),
150 span: actual.span,
151 kind: ErrorKind::ExpectedList {
152 found: describe(&actual.value),
153 },
154 });
155 }
156 }
157
158 SchemaType::EnumRef(enum_name) => {
161 let enum_def = &enums[enum_name];
162 if let RonValue::Identifier(variant) = &actual.value {
163 if !enum_def.variants.contains(variant) {
164 errors.push(ValidationError {
165 path: path.to_string(),
166 span: actual.span,
167 kind: ErrorKind::InvalidEnumVariant {
168 enum_name: enum_name.clone(),
169 variant: variant.clone(),
170 valid: enum_def.variants.iter().cloned().collect(),
171 },
172 });
173 }
174 } else {
175 errors.push(ValidationError {
176 path: path.to_string(),
177 span: actual.span,
178 kind: ErrorKind::TypeMismatch {
179 expected: enum_name.clone(),
180 found: describe(&actual.value),
181 },
182 });
183 }
184 }
185
186 SchemaType::Map(key_type, value_type) => {
188 if let RonValue::Map(entries) = &actual.value {
189 for (key, value) in entries {
190 let key_desc = describe(&key.value);
191 validate_type(key_type, key, path, errors, enums, aliases);
192 let entry_path = format!("{path}[{key_desc}]");
193 validate_type(value_type, value, &entry_path, errors, enums, aliases);
194 }
195 } else {
196 errors.push(ValidationError {
197 path: path.to_string(),
198 span: actual.span,
199 kind: ErrorKind::ExpectedMap {
200 found: describe(&actual.value),
201 },
202 });
203 }
204 }
205
206 SchemaType::AliasRef(alias_name) => {
209 if let Some(resolved) = aliases.get(alias_name) {
210 validate_type(&resolved.value, actual, path, errors, enums, aliases);
211 }
212 }
214
215 SchemaType::Struct(struct_def) => {
217 validate_struct(struct_def, actual, path, errors, enums, aliases);
218 }
219 }
220}
221
222fn validate_struct(
229 struct_def: &StructDef,
230 actual: &Spanned<RonValue>,
231 path: &str,
232 errors: &mut Vec<ValidationError>,
233 enums: &HashMap<String, EnumDef>,
234 aliases: &HashMap<String, Spanned<SchemaType>>,
235) {
236 let RonValue::Struct(data_struct) = &actual.value else {
238 errors.push(ValidationError {
239 path: path.to_string(),
240 span: actual.span,
241 kind: ErrorKind::ExpectedStruct {
242 found: describe(&actual.value),
243 },
244 });
245 return;
246 };
247
248 let data_map: HashMap<&str, &Spanned<RonValue>> = data_struct
250 .fields
251 .iter()
252 .map(|(name, value)| (name.value.as_str(), value))
253 .collect();
254
255 let schema_names: HashSet<&str> = struct_def
257 .fields
258 .iter()
259 .map(|f| f.name.value.as_str())
260 .collect();
261
262 for field_def in &struct_def.fields {
264 if !data_map.contains_key(field_def.name.value.as_str()) {
265 errors.push(ValidationError {
266 path: build_path(path, &field_def.name.value),
267 span: data_struct.close_span,
268 kind: ErrorKind::MissingField {
269 field_name: field_def.name.value.clone(),
270 },
271 });
272 }
273 }
274
275 for (name, _value) in &data_struct.fields {
277 if !schema_names.contains(name.value.as_str()) {
278 errors.push(ValidationError {
279 path: build_path(path, &name.value),
280 span: name.span,
281 kind: ErrorKind::UnknownField {
282 field_name: name.value.clone(),
283 },
284 });
285 }
286 }
287
288 for field_def in &struct_def.fields {
290 if let Some(data_value) = data_map.get(field_def.name.value.as_str()) {
291 let field_path = build_path(path, &field_def.name.value);
292 validate_type(&field_def.type_.value, data_value, &field_path, errors, enums, aliases);
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::schema::parser::parse_schema;
301 use crate::ron::parser::parse_ron;
302
303 fn validate_str(schema_src: &str, data_src: &str) -> Vec<ValidationError> {
305 let schema = parse_schema(schema_src).expect("test schema should parse");
306 let data = parse_ron(data_src).expect("test data should parse");
307 validate(&schema, &data)
308 }
309
310 #[test]
316 fn describe_string() {
317 assert_eq!(describe(&RonValue::String("hi".to_string())), "String(\"hi\")");
318 }
319
320 #[test]
322 fn describe_string_truncated() {
323 let long = "a".repeat(30);
324 let desc = describe(&RonValue::String(long));
325 assert!(desc.contains("..."));
326 }
327
328 #[test]
330 fn describe_integer() {
331 assert_eq!(describe(&RonValue::Integer(42)), "Integer(42)");
332 }
333
334 #[test]
336 fn describe_float() {
337 assert_eq!(describe(&RonValue::Float(3.14)), "Float(3.14)");
338 }
339
340 #[test]
342 fn describe_bool() {
343 assert_eq!(describe(&RonValue::Bool(true)), "Bool(true)");
344 }
345
346 #[test]
348 fn describe_identifier() {
349 assert_eq!(describe(&RonValue::Identifier("Creature".to_string())), "Identifier(Creature)");
350 }
351
352 #[test]
358 fn build_path_root() {
359 assert_eq!(build_path("", "name"), "name");
360 }
361
362 #[test]
364 fn build_path_nested() {
365 assert_eq!(build_path("cost", "generic"), "cost.generic");
366 }
367
368 #[test]
370 fn build_path_deep() {
371 assert_eq!(build_path("a.b", "c"), "a.b.c");
372 }
373
374 #[test]
380 fn valid_single_string_field() {
381 let errs = validate_str("(\n name: String,\n)", "(name: \"hello\")");
382 assert!(errs.is_empty());
383 }
384
385 #[test]
387 fn valid_all_primitives() {
388 let schema = "(\n s: String,\n i: Integer,\n f: Float,\n b: Bool,\n)";
389 let data = "(s: \"hi\", i: 42, f: 3.14, b: true)";
390 let errs = validate_str(schema, data);
391 assert!(errs.is_empty());
392 }
393
394 #[test]
396 fn valid_option_none() {
397 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: None)");
398 assert!(errs.is_empty());
399 }
400
401 #[test]
403 fn valid_option_some() {
404 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(5))");
405 assert!(errs.is_empty());
406 }
407
408 #[test]
410 fn valid_list_empty() {
411 let errs = validate_str("(\n tags: [String],\n)", "(tags: [])");
412 assert!(errs.is_empty());
413 }
414
415 #[test]
417 fn valid_list_populated() {
418 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"a\", \"b\"])");
419 assert!(errs.is_empty());
420 }
421
422 #[test]
424 fn valid_enum_variant() {
425 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B, C }";
426 let data = "(kind: B)";
427 let errs = validate_str(schema, data);
428 assert!(errs.is_empty());
429 }
430
431 #[test]
433 fn valid_enum_list() {
434 let schema = "(\n types: [CardType],\n)\nenum CardType { Creature, Trap }";
435 let data = "(types: [Creature, Trap])";
436 let errs = validate_str(schema, data);
437 assert!(errs.is_empty());
438 }
439
440 #[test]
442 fn valid_nested_struct() {
443 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
444 let data = "(cost: (generic: 2, sigil: 1))";
445 let errs = validate_str(schema, data);
446 assert!(errs.is_empty());
447 }
448
449 #[test]
455 fn type_mismatch_string_got_integer() {
456 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
457 assert_eq!(errs.len(), 1);
458 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
459 }
460
461 #[test]
463 fn type_mismatch_integer_got_string() {
464 let errs = validate_str("(\n age: Integer,\n)", "(age: \"five\")");
465 assert_eq!(errs.len(), 1);
466 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
467 }
468
469 #[test]
471 fn type_mismatch_float_got_integer() {
472 let errs = validate_str("(\n rate: Float,\n)", "(rate: 5)");
473 assert_eq!(errs.len(), 1);
474 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
475 }
476
477 #[test]
479 fn type_mismatch_bool_got_string() {
480 let errs = validate_str("(\n flag: Bool,\n)", "(flag: \"yes\")");
481 assert_eq!(errs.len(), 1);
482 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Bool"));
483 }
484
485 #[test]
487 fn type_mismatch_has_correct_path() {
488 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
489 assert_eq!(errs[0].path, "name");
490 }
491
492 #[test]
498 fn missing_field_detected() {
499 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
500 assert_eq!(errs.len(), 1);
501 assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "age"));
502 }
503
504 #[test]
506 fn missing_field_has_correct_path() {
507 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
508 assert_eq!(errs[0].path, "age");
509 }
510
511 #[test]
513 fn missing_field_span_points_to_close_paren() {
514 let data = "(name: \"hi\")";
515 let errs = validate_str("(\n name: String,\n age: Integer,\n)", data);
516 assert_eq!(errs[0].span.start.offset, data.len() - 1);
518 }
519
520 #[test]
522 fn missing_fields_all_reported() {
523 let errs = validate_str("(\n a: String,\n b: Integer,\n c: Bool,\n)", "()");
524 assert_eq!(errs.len(), 3);
525 }
526
527 #[test]
533 fn unknown_field_detected() {
534 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", colour: \"red\")");
535 assert_eq!(errs.len(), 1);
536 assert!(matches!(&errs[0].kind, ErrorKind::UnknownField { field_name } if field_name == "colour"));
537 }
538
539 #[test]
541 fn unknown_field_has_correct_path() {
542 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", extra: 5)");
543 assert_eq!(errs[0].path, "extra");
544 }
545
546 #[test]
552 fn invalid_enum_variant() {
553 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
554 let errs = validate_str(schema, "(kind: C)");
555 assert_eq!(errs.len(), 1);
556 assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { variant, .. } if variant == "C"));
557 }
558
559 #[test]
561 fn enum_rejects_string() {
562 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
563 let errs = validate_str(schema, "(kind: \"A\")");
564 assert_eq!(errs.len(), 1);
565 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
566 }
567
568 #[test]
574 fn expected_option_got_bare_value() {
575 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: 5)");
576 assert_eq!(errs.len(), 1);
577 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedOption { .. }));
578 }
579
580 #[test]
582 fn option_some_wrong_inner_type() {
583 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(\"five\"))");
584 assert_eq!(errs.len(), 1);
585 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
586 }
587
588 #[test]
594 fn expected_list_got_string() {
595 let errs = validate_str("(\n tags: [String],\n)", "(tags: \"hi\")");
596 assert_eq!(errs.len(), 1);
597 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedList { .. }));
598 }
599
600 #[test]
602 fn list_element_wrong_type() {
603 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
604 assert_eq!(errs.len(), 1);
605 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
606 }
607
608 #[test]
610 fn list_element_error_has_bracket_path() {
611 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
612 assert_eq!(errs[0].path, "tags[1]");
613 }
614
615 #[test]
621 fn expected_struct_got_integer() {
622 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
623 let errs = validate_str(schema, "(cost: 5)");
624 assert_eq!(errs.len(), 1);
625 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedStruct { .. }));
626 }
627
628 #[test]
634 fn nested_struct_type_mismatch_path() {
635 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
636 let errs = validate_str(schema, "(cost: (generic: \"two\"))");
637 assert_eq!(errs.len(), 1);
638 assert_eq!(errs[0].path, "cost.generic");
639 }
640
641 #[test]
643 fn nested_struct_missing_field_path() {
644 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
645 let errs = validate_str(schema, "(cost: (generic: 1))");
646 assert_eq!(errs.len(), 1);
647 assert_eq!(errs[0].path, "cost.sigil");
648 }
649
650 #[test]
656 fn multiple_errors_collected() {
657 let schema = "(\n name: String,\n age: Integer,\n active: Bool,\n)";
658 let data = "(name: 42, age: \"five\", active: \"yes\")";
659 let errs = validate_str(schema, data);
660 assert_eq!(errs.len(), 3);
661 }
662
663 #[test]
665 fn mixed_error_types_collected() {
666 let schema = "(\n name: String,\n age: Integer,\n)";
667 let data = "(name: \"hi\", age: \"five\", extra: true)";
668 let errs = validate_str(schema, data);
669 assert_eq!(errs.len(), 2);
671 }
672
673 #[test]
679 fn valid_card_data() {
680 let schema = r#"(
681 name: String,
682 card_types: [CardType],
683 legendary: Bool,
684 power: Option(Integer),
685 toughness: Option(Integer),
686 keywords: [String],
687 )
688 enum CardType { Creature, Trap, Artifact }"#;
689 let data = r#"(
690 name: "Ashborn Hound",
691 card_types: [Creature],
692 legendary: false,
693 power: Some(1),
694 toughness: Some(1),
695 keywords: [],
696 )"#;
697 let errs = validate_str(schema, data);
698 assert!(errs.is_empty());
699 }
700
701 #[test]
703 fn card_data_multiple_errors() {
704 let schema = r#"(
705 name: String,
706 card_types: [CardType],
707 legendary: Bool,
708 power: Option(Integer),
709 )
710 enum CardType { Creature, Trap }"#;
711 let data = r#"(
712 name: 42,
713 card_types: [Pirates],
714 legendary: false,
715 power: Some("five"),
716 )"#;
717 let errs = validate_str(schema, data);
718 assert_eq!(errs.len(), 3);
720 }
721
722 #[test]
728 fn alias_struct_valid() {
729 let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
730 let data = "(cost: (generic: 5))";
731 let errs = validate_str(schema, data);
732 assert!(errs.is_empty());
733 }
734
735 #[test]
737 fn alias_struct_type_mismatch() {
738 let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
739 let data = "(cost: (generic: \"five\"))";
740 let errs = validate_str(schema, data);
741 assert_eq!(errs.len(), 1);
742 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
743 }
744
745 #[test]
747 fn alias_primitive_valid() {
748 let schema = "(\n name: Name,\n)\ntype Name = String";
749 let data = "(name: \"hello\")";
750 let errs = validate_str(schema, data);
751 assert!(errs.is_empty());
752 }
753
754 #[test]
756 fn alias_primitive_mismatch() {
757 let schema = "(\n name: Name,\n)\ntype Name = String";
758 let data = "(name: 42)";
759 let errs = validate_str(schema, data);
760 assert_eq!(errs.len(), 1);
761 }
762
763 #[test]
765 fn alias_in_list_valid() {
766 let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
767 let data = "(costs: [(generic: 1), (generic: 2)])";
768 let errs = validate_str(schema, data);
769 assert!(errs.is_empty());
770 }
771
772 #[test]
774 fn alias_in_list_element_error() {
775 let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
776 let data = "(costs: [(generic: 1), (generic: \"two\")])";
777 let errs = validate_str(schema, data);
778 assert_eq!(errs.len(), 1);
779 assert_eq!(errs[0].path, "costs[1].generic");
780 }
781
782 #[test]
788 fn map_valid() {
789 let schema = "(\n attrs: {String: Integer},\n)";
790 let data = "(attrs: {\"str\": 5, \"dex\": 3})";
791 let errs = validate_str(schema, data);
792 assert!(errs.is_empty());
793 }
794
795 #[test]
797 fn map_empty_valid() {
798 let schema = "(\n attrs: {String: Integer},\n)";
799 let data = "(attrs: {})";
800 let errs = validate_str(schema, data);
801 assert!(errs.is_empty());
802 }
803
804 #[test]
806 fn map_expected_got_string() {
807 let schema = "(\n attrs: {String: Integer},\n)";
808 let data = "(attrs: \"not a map\")";
809 let errs = validate_str(schema, data);
810 assert_eq!(errs.len(), 1);
811 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedMap { .. }));
812 }
813
814 #[test]
816 fn map_wrong_value_type() {
817 let schema = "(\n attrs: {String: Integer},\n)";
818 let data = "(attrs: {\"str\": \"five\"})";
819 let errs = validate_str(schema, data);
820 assert_eq!(errs.len(), 1);
821 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
822 }
823
824 #[test]
826 fn map_wrong_key_type() {
827 let schema = "(\n attrs: {String: Integer},\n)";
828 let data = "(attrs: {42: 5})";
829 let errs = validate_str(schema, data);
830 assert_eq!(errs.len(), 1);
831 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
832 }
833}