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::Struct(_) => "Struct".to_string(),
40 }
41}
42
43fn build_path(parent: &str, field: &str) -> String {
48 if parent.is_empty() {
49 field.to_string()
50 } else {
51 format!("{parent}.{field}")
52 }
53}
54
55#[allow(clippy::too_many_lines)]
61fn validate_type(
62 expected: &SchemaType,
63 actual: &Spanned<RonValue>,
64 path: &str,
65 errors: &mut Vec<ValidationError>,
66 enums: &HashMap<String, EnumDef>,
67 aliases: &HashMap<String, Spanned<SchemaType>>,
68) {
69 match expected {
70 SchemaType::String => {
72 if !matches!(actual.value, RonValue::String(_)) {
73 errors.push(ValidationError {
74 path: path.to_string(),
75 span: actual.span,
76 kind: ErrorKind::TypeMismatch {
77 expected: "String".to_string(),
78 found: describe(&actual.value),
79 },
80 });
81 }
82 }
83 SchemaType::Integer => {
84 if !matches!(actual.value, RonValue::Integer(_)) {
85 errors.push(ValidationError {
86 path: path.to_string(),
87 span: actual.span,
88 kind: ErrorKind::TypeMismatch {
89 expected: "Integer".to_string(),
90 found: describe(&actual.value),
91 },
92 });
93 }
94 }
95 SchemaType::Float => {
96 if !matches!(actual.value, RonValue::Float(_)) {
97 errors.push(ValidationError {
98 path: path.to_string(),
99 span: actual.span,
100 kind: ErrorKind::TypeMismatch {
101 expected: "Float".to_string(),
102 found: describe(&actual.value),
103 },
104 });
105 }
106 }
107 SchemaType::Bool => {
108 if !matches!(actual.value, RonValue::Bool(_)) {
109 errors.push(ValidationError {
110 path: path.to_string(),
111 span: actual.span,
112 kind: ErrorKind::TypeMismatch {
113 expected: "Bool".to_string(),
114 found: describe(&actual.value),
115 },
116 });
117 }
118 }
119
120 SchemaType::Option(inner_type) => match &actual.value {
123 RonValue::Option(None) => {}
124 RonValue::Option(Some(inner_value)) => {
125 validate_type(inner_type, inner_value, path, errors, enums, aliases);
126 }
127 _ => {
128 errors.push(ValidationError {
129 path: path.to_string(),
130 span: actual.span,
131 kind: ErrorKind::ExpectedOption {
132 found: describe(&actual.value),
133 },
134 });
135 }
136 },
137
138 SchemaType::List(element_type) => {
141 if let RonValue::List(elements) = &actual.value {
142 for (index, element) in elements.iter().enumerate() {
143 let element_path = format!("{path}[{index}]");
144 validate_type(element_type, element, &element_path, errors, enums, aliases);
145 }
146 } else {
147 errors.push(ValidationError {
148 path: path.to_string(),
149 span: actual.span,
150 kind: ErrorKind::ExpectedList {
151 found: describe(&actual.value),
152 },
153 });
154 }
155 }
156
157 SchemaType::EnumRef(enum_name) => {
160 let enum_def = &enums[enum_name];
161 if let RonValue::Identifier(variant) = &actual.value {
162 if !enum_def.variants.contains(variant) {
163 errors.push(ValidationError {
164 path: path.to_string(),
165 span: actual.span,
166 kind: ErrorKind::InvalidEnumVariant {
167 enum_name: enum_name.clone(),
168 variant: variant.clone(),
169 valid: enum_def.variants.iter().cloned().collect(),
170 },
171 });
172 }
173 } else {
174 errors.push(ValidationError {
175 path: path.to_string(),
176 span: actual.span,
177 kind: ErrorKind::TypeMismatch {
178 expected: enum_name.clone(),
179 found: describe(&actual.value),
180 },
181 });
182 }
183 }
184
185 SchemaType::AliasRef(alias_name) => {
188 if let Some(resolved) = aliases.get(alias_name) {
189 validate_type(&resolved.value, actual, path, errors, enums, aliases);
190 }
191 }
193
194 SchemaType::Struct(struct_def) => {
196 validate_struct(struct_def, actual, path, errors, enums, aliases);
197 }
198 }
199}
200
201fn validate_struct(
208 struct_def: &StructDef,
209 actual: &Spanned<RonValue>,
210 path: &str,
211 errors: &mut Vec<ValidationError>,
212 enums: &HashMap<String, EnumDef>,
213 aliases: &HashMap<String, Spanned<SchemaType>>,
214) {
215 let RonValue::Struct(data_struct) = &actual.value else {
217 errors.push(ValidationError {
218 path: path.to_string(),
219 span: actual.span,
220 kind: ErrorKind::ExpectedStruct {
221 found: describe(&actual.value),
222 },
223 });
224 return;
225 };
226
227 let data_map: HashMap<&str, &Spanned<RonValue>> = data_struct
229 .fields
230 .iter()
231 .map(|(name, value)| (name.value.as_str(), value))
232 .collect();
233
234 let schema_names: HashSet<&str> = struct_def
236 .fields
237 .iter()
238 .map(|f| f.name.value.as_str())
239 .collect();
240
241 for field_def in &struct_def.fields {
243 if !data_map.contains_key(field_def.name.value.as_str()) {
244 errors.push(ValidationError {
245 path: build_path(path, &field_def.name.value),
246 span: data_struct.close_span,
247 kind: ErrorKind::MissingField {
248 field_name: field_def.name.value.clone(),
249 },
250 });
251 }
252 }
253
254 for (name, _value) in &data_struct.fields {
256 if !schema_names.contains(name.value.as_str()) {
257 errors.push(ValidationError {
258 path: build_path(path, &name.value),
259 span: name.span,
260 kind: ErrorKind::UnknownField {
261 field_name: name.value.clone(),
262 },
263 });
264 }
265 }
266
267 for field_def in &struct_def.fields {
269 if let Some(data_value) = data_map.get(field_def.name.value.as_str()) {
270 let field_path = build_path(path, &field_def.name.value);
271 validate_type(&field_def.type_.value, data_value, &field_path, errors, enums, aliases);
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::schema::parser::parse_schema;
280 use crate::ron::parser::parse_ron;
281
282 fn validate_str(schema_src: &str, data_src: &str) -> Vec<ValidationError> {
284 let schema = parse_schema(schema_src).expect("test schema should parse");
285 let data = parse_ron(data_src).expect("test data should parse");
286 validate(&schema, &data)
287 }
288
289 #[test]
295 fn describe_string() {
296 assert_eq!(describe(&RonValue::String("hi".to_string())), "String(\"hi\")");
297 }
298
299 #[test]
301 fn describe_string_truncated() {
302 let long = "a".repeat(30);
303 let desc = describe(&RonValue::String(long));
304 assert!(desc.contains("..."));
305 }
306
307 #[test]
309 fn describe_integer() {
310 assert_eq!(describe(&RonValue::Integer(42)), "Integer(42)");
311 }
312
313 #[test]
315 fn describe_float() {
316 assert_eq!(describe(&RonValue::Float(3.14)), "Float(3.14)");
317 }
318
319 #[test]
321 fn describe_bool() {
322 assert_eq!(describe(&RonValue::Bool(true)), "Bool(true)");
323 }
324
325 #[test]
327 fn describe_identifier() {
328 assert_eq!(describe(&RonValue::Identifier("Creature".to_string())), "Identifier(Creature)");
329 }
330
331 #[test]
337 fn build_path_root() {
338 assert_eq!(build_path("", "name"), "name");
339 }
340
341 #[test]
343 fn build_path_nested() {
344 assert_eq!(build_path("cost", "generic"), "cost.generic");
345 }
346
347 #[test]
349 fn build_path_deep() {
350 assert_eq!(build_path("a.b", "c"), "a.b.c");
351 }
352
353 #[test]
359 fn valid_single_string_field() {
360 let errs = validate_str("(\n name: String,\n)", "(name: \"hello\")");
361 assert!(errs.is_empty());
362 }
363
364 #[test]
366 fn valid_all_primitives() {
367 let schema = "(\n s: String,\n i: Integer,\n f: Float,\n b: Bool,\n)";
368 let data = "(s: \"hi\", i: 42, f: 3.14, b: true)";
369 let errs = validate_str(schema, data);
370 assert!(errs.is_empty());
371 }
372
373 #[test]
375 fn valid_option_none() {
376 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: None)");
377 assert!(errs.is_empty());
378 }
379
380 #[test]
382 fn valid_option_some() {
383 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(5))");
384 assert!(errs.is_empty());
385 }
386
387 #[test]
389 fn valid_list_empty() {
390 let errs = validate_str("(\n tags: [String],\n)", "(tags: [])");
391 assert!(errs.is_empty());
392 }
393
394 #[test]
396 fn valid_list_populated() {
397 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"a\", \"b\"])");
398 assert!(errs.is_empty());
399 }
400
401 #[test]
403 fn valid_enum_variant() {
404 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B, C }";
405 let data = "(kind: B)";
406 let errs = validate_str(schema, data);
407 assert!(errs.is_empty());
408 }
409
410 #[test]
412 fn valid_enum_list() {
413 let schema = "(\n types: [CardType],\n)\nenum CardType { Creature, Trap }";
414 let data = "(types: [Creature, Trap])";
415 let errs = validate_str(schema, data);
416 assert!(errs.is_empty());
417 }
418
419 #[test]
421 fn valid_nested_struct() {
422 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
423 let data = "(cost: (generic: 2, sigil: 1))";
424 let errs = validate_str(schema, data);
425 assert!(errs.is_empty());
426 }
427
428 #[test]
434 fn type_mismatch_string_got_integer() {
435 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
436 assert_eq!(errs.len(), 1);
437 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
438 }
439
440 #[test]
442 fn type_mismatch_integer_got_string() {
443 let errs = validate_str("(\n age: Integer,\n)", "(age: \"five\")");
444 assert_eq!(errs.len(), 1);
445 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
446 }
447
448 #[test]
450 fn type_mismatch_float_got_integer() {
451 let errs = validate_str("(\n rate: Float,\n)", "(rate: 5)");
452 assert_eq!(errs.len(), 1);
453 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
454 }
455
456 #[test]
458 fn type_mismatch_bool_got_string() {
459 let errs = validate_str("(\n flag: Bool,\n)", "(flag: \"yes\")");
460 assert_eq!(errs.len(), 1);
461 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Bool"));
462 }
463
464 #[test]
466 fn type_mismatch_has_correct_path() {
467 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
468 assert_eq!(errs[0].path, "name");
469 }
470
471 #[test]
477 fn missing_field_detected() {
478 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
479 assert_eq!(errs.len(), 1);
480 assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "age"));
481 }
482
483 #[test]
485 fn missing_field_has_correct_path() {
486 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
487 assert_eq!(errs[0].path, "age");
488 }
489
490 #[test]
492 fn missing_field_span_points_to_close_paren() {
493 let data = "(name: \"hi\")";
494 let errs = validate_str("(\n name: String,\n age: Integer,\n)", data);
495 assert_eq!(errs[0].span.start.offset, data.len() - 1);
497 }
498
499 #[test]
501 fn missing_fields_all_reported() {
502 let errs = validate_str("(\n a: String,\n b: Integer,\n c: Bool,\n)", "()");
503 assert_eq!(errs.len(), 3);
504 }
505
506 #[test]
512 fn unknown_field_detected() {
513 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", colour: \"red\")");
514 assert_eq!(errs.len(), 1);
515 assert!(matches!(&errs[0].kind, ErrorKind::UnknownField { field_name } if field_name == "colour"));
516 }
517
518 #[test]
520 fn unknown_field_has_correct_path() {
521 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", extra: 5)");
522 assert_eq!(errs[0].path, "extra");
523 }
524
525 #[test]
531 fn invalid_enum_variant() {
532 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
533 let errs = validate_str(schema, "(kind: C)");
534 assert_eq!(errs.len(), 1);
535 assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { variant, .. } if variant == "C"));
536 }
537
538 #[test]
540 fn enum_rejects_string() {
541 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
542 let errs = validate_str(schema, "(kind: \"A\")");
543 assert_eq!(errs.len(), 1);
544 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
545 }
546
547 #[test]
553 fn expected_option_got_bare_value() {
554 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: 5)");
555 assert_eq!(errs.len(), 1);
556 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedOption { .. }));
557 }
558
559 #[test]
561 fn option_some_wrong_inner_type() {
562 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(\"five\"))");
563 assert_eq!(errs.len(), 1);
564 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
565 }
566
567 #[test]
573 fn expected_list_got_string() {
574 let errs = validate_str("(\n tags: [String],\n)", "(tags: \"hi\")");
575 assert_eq!(errs.len(), 1);
576 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedList { .. }));
577 }
578
579 #[test]
581 fn list_element_wrong_type() {
582 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
583 assert_eq!(errs.len(), 1);
584 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
585 }
586
587 #[test]
589 fn list_element_error_has_bracket_path() {
590 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
591 assert_eq!(errs[0].path, "tags[1]");
592 }
593
594 #[test]
600 fn expected_struct_got_integer() {
601 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
602 let errs = validate_str(schema, "(cost: 5)");
603 assert_eq!(errs.len(), 1);
604 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedStruct { .. }));
605 }
606
607 #[test]
613 fn nested_struct_type_mismatch_path() {
614 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
615 let errs = validate_str(schema, "(cost: (generic: \"two\"))");
616 assert_eq!(errs.len(), 1);
617 assert_eq!(errs[0].path, "cost.generic");
618 }
619
620 #[test]
622 fn nested_struct_missing_field_path() {
623 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
624 let errs = validate_str(schema, "(cost: (generic: 1))");
625 assert_eq!(errs.len(), 1);
626 assert_eq!(errs[0].path, "cost.sigil");
627 }
628
629 #[test]
635 fn multiple_errors_collected() {
636 let schema = "(\n name: String,\n age: Integer,\n active: Bool,\n)";
637 let data = "(name: 42, age: \"five\", active: \"yes\")";
638 let errs = validate_str(schema, data);
639 assert_eq!(errs.len(), 3);
640 }
641
642 #[test]
644 fn mixed_error_types_collected() {
645 let schema = "(\n name: String,\n age: Integer,\n)";
646 let data = "(name: \"hi\", age: \"five\", extra: true)";
647 let errs = validate_str(schema, data);
648 assert_eq!(errs.len(), 2);
650 }
651
652 #[test]
658 fn valid_card_data() {
659 let schema = r#"(
660 name: String,
661 card_types: [CardType],
662 legendary: Bool,
663 power: Option(Integer),
664 toughness: Option(Integer),
665 keywords: [String],
666 )
667 enum CardType { Creature, Trap, Artifact }"#;
668 let data = r#"(
669 name: "Ashborn Hound",
670 card_types: [Creature],
671 legendary: false,
672 power: Some(1),
673 toughness: Some(1),
674 keywords: [],
675 )"#;
676 let errs = validate_str(schema, data);
677 assert!(errs.is_empty());
678 }
679
680 #[test]
682 fn card_data_multiple_errors() {
683 let schema = r#"(
684 name: String,
685 card_types: [CardType],
686 legendary: Bool,
687 power: Option(Integer),
688 )
689 enum CardType { Creature, Trap }"#;
690 let data = r#"(
691 name: 42,
692 card_types: [Pirates],
693 legendary: false,
694 power: Some("five"),
695 )"#;
696 let errs = validate_str(schema, data);
697 assert_eq!(errs.len(), 3);
699 }
700
701 #[test]
707 fn alias_struct_valid() {
708 let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
709 let data = "(cost: (generic: 5))";
710 let errs = validate_str(schema, data);
711 assert!(errs.is_empty());
712 }
713
714 #[test]
716 fn alias_struct_type_mismatch() {
717 let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
718 let data = "(cost: (generic: \"five\"))";
719 let errs = validate_str(schema, data);
720 assert_eq!(errs.len(), 1);
721 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
722 }
723
724 #[test]
726 fn alias_primitive_valid() {
727 let schema = "(\n name: Name,\n)\ntype Name = String";
728 let data = "(name: \"hello\")";
729 let errs = validate_str(schema, data);
730 assert!(errs.is_empty());
731 }
732
733 #[test]
735 fn alias_primitive_mismatch() {
736 let schema = "(\n name: Name,\n)\ntype Name = String";
737 let data = "(name: 42)";
738 let errs = validate_str(schema, data);
739 assert_eq!(errs.len(), 1);
740 }
741
742 #[test]
744 fn alias_in_list_valid() {
745 let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
746 let data = "(costs: [(generic: 1), (generic: 2)])";
747 let errs = validate_str(schema, data);
748 assert!(errs.is_empty());
749 }
750
751 #[test]
753 fn alias_in_list_element_error() {
754 let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
755 let data = "(costs: [(generic: 1), (generic: \"two\")])";
756 let errs = validate_str(schema, data);
757 assert_eq!(errs.len(), 1);
758 assert_eq!(errs[0].path, "costs[1].generic");
759 }
760}