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);
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) {
68 match expected {
69 SchemaType::String => {
71 if !matches!(actual.value, RonValue::String(_)) {
72 errors.push(ValidationError {
73 path: path.to_string(),
74 span: actual.span,
75 kind: ErrorKind::TypeMismatch {
76 expected: "String".to_string(),
77 found: describe(&actual.value),
78 },
79 });
80 }
81 }
82 SchemaType::Integer => {
83 if !matches!(actual.value, RonValue::Integer(_)) {
84 errors.push(ValidationError {
85 path: path.to_string(),
86 span: actual.span,
87 kind: ErrorKind::TypeMismatch {
88 expected: "Integer".to_string(),
89 found: describe(&actual.value),
90 },
91 });
92 }
93 }
94 SchemaType::Float => {
95 if !matches!(actual.value, RonValue::Float(_)) {
96 errors.push(ValidationError {
97 path: path.to_string(),
98 span: actual.span,
99 kind: ErrorKind::TypeMismatch {
100 expected: "Float".to_string(),
101 found: describe(&actual.value),
102 },
103 });
104 }
105 }
106 SchemaType::Bool => {
107 if !matches!(actual.value, RonValue::Bool(_)) {
108 errors.push(ValidationError {
109 path: path.to_string(),
110 span: actual.span,
111 kind: ErrorKind::TypeMismatch {
112 expected: "Bool".to_string(),
113 found: describe(&actual.value),
114 },
115 });
116 }
117 }
118
119 SchemaType::Option(inner_type) => match &actual.value {
122 RonValue::Option(None) => {}
123 RonValue::Option(Some(inner_value)) => {
124 validate_type(inner_type, inner_value, path, errors, enums);
125 }
126 _ => {
127 errors.push(ValidationError {
128 path: path.to_string(),
129 span: actual.span,
130 kind: ErrorKind::ExpectedOption {
131 found: describe(&actual.value),
132 },
133 });
134 }
135 },
136
137 SchemaType::List(element_type) => {
140 if let RonValue::List(elements) = &actual.value {
141 for (index, element) in elements.iter().enumerate() {
142 let element_path = format!("{path}[{index}]");
143 validate_type(element_type, element, &element_path, errors, enums);
144 }
145 } else {
146 errors.push(ValidationError {
147 path: path.to_string(),
148 span: actual.span,
149 kind: ErrorKind::ExpectedList {
150 found: describe(&actual.value),
151 },
152 });
153 }
154 }
155
156 SchemaType::EnumRef(enum_name) => {
159 let enum_def = &enums[enum_name];
160 if let RonValue::Identifier(variant) = &actual.value {
161 if !enum_def.variants.contains(variant) {
162 errors.push(ValidationError {
163 path: path.to_string(),
164 span: actual.span,
165 kind: ErrorKind::InvalidEnumVariant {
166 enum_name: enum_name.clone(),
167 variant: variant.clone(),
168 valid: enum_def.variants.iter().cloned().collect(),
169 },
170 });
171 }
172 } else {
173 errors.push(ValidationError {
174 path: path.to_string(),
175 span: actual.span,
176 kind: ErrorKind::TypeMismatch {
177 expected: enum_name.clone(),
178 found: describe(&actual.value),
179 },
180 });
181 }
182 }
183
184 SchemaType::Struct(struct_def) => {
186 validate_struct(struct_def, actual, path, errors, enums);
187 }
188 }
189}
190
191fn validate_struct(
198 struct_def: &StructDef,
199 actual: &Spanned<RonValue>,
200 path: &str,
201 errors: &mut Vec<ValidationError>,
202 enums: &HashMap<String, EnumDef>,
203) {
204 let RonValue::Struct(data_struct) = &actual.value else {
206 errors.push(ValidationError {
207 path: path.to_string(),
208 span: actual.span,
209 kind: ErrorKind::ExpectedStruct {
210 found: describe(&actual.value),
211 },
212 });
213 return;
214 };
215
216 let data_map: HashMap<&str, &Spanned<RonValue>> = data_struct
218 .fields
219 .iter()
220 .map(|(name, value)| (name.value.as_str(), value))
221 .collect();
222
223 let schema_names: HashSet<&str> = struct_def
225 .fields
226 .iter()
227 .map(|f| f.name.value.as_str())
228 .collect();
229
230 for field_def in &struct_def.fields {
232 if !data_map.contains_key(field_def.name.value.as_str()) {
233 errors.push(ValidationError {
234 path: build_path(path, &field_def.name.value),
235 span: data_struct.close_span,
236 kind: ErrorKind::MissingField {
237 field_name: field_def.name.value.clone(),
238 },
239 });
240 }
241 }
242
243 for (name, _value) in &data_struct.fields {
245 if !schema_names.contains(name.value.as_str()) {
246 errors.push(ValidationError {
247 path: build_path(path, &name.value),
248 span: name.span,
249 kind: ErrorKind::UnknownField {
250 field_name: name.value.clone(),
251 },
252 });
253 }
254 }
255
256 for field_def in &struct_def.fields {
258 if let Some(data_value) = data_map.get(field_def.name.value.as_str()) {
259 let field_path = build_path(path, &field_def.name.value);
260 validate_type(&field_def.type_.value, data_value, &field_path, errors, enums);
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::schema::parser::parse_schema;
269 use crate::ron::parser::parse_ron;
270
271 fn validate_str(schema_src: &str, data_src: &str) -> Vec<ValidationError> {
273 let schema = parse_schema(schema_src).expect("test schema should parse");
274 let data = parse_ron(data_src).expect("test data should parse");
275 validate(&schema, &data)
276 }
277
278 #[test]
284 fn describe_string() {
285 assert_eq!(describe(&RonValue::String("hi".to_string())), "String(\"hi\")");
286 }
287
288 #[test]
290 fn describe_string_truncated() {
291 let long = "a".repeat(30);
292 let desc = describe(&RonValue::String(long));
293 assert!(desc.contains("..."));
294 }
295
296 #[test]
298 fn describe_integer() {
299 assert_eq!(describe(&RonValue::Integer(42)), "Integer(42)");
300 }
301
302 #[test]
304 fn describe_float() {
305 assert_eq!(describe(&RonValue::Float(3.14)), "Float(3.14)");
306 }
307
308 #[test]
310 fn describe_bool() {
311 assert_eq!(describe(&RonValue::Bool(true)), "Bool(true)");
312 }
313
314 #[test]
316 fn describe_identifier() {
317 assert_eq!(describe(&RonValue::Identifier("Creature".to_string())), "Identifier(Creature)");
318 }
319
320 #[test]
326 fn build_path_root() {
327 assert_eq!(build_path("", "name"), "name");
328 }
329
330 #[test]
332 fn build_path_nested() {
333 assert_eq!(build_path("cost", "generic"), "cost.generic");
334 }
335
336 #[test]
338 fn build_path_deep() {
339 assert_eq!(build_path("a.b", "c"), "a.b.c");
340 }
341
342 #[test]
348 fn valid_single_string_field() {
349 let errs = validate_str("(\n name: String,\n)", "(name: \"hello\")");
350 assert!(errs.is_empty());
351 }
352
353 #[test]
355 fn valid_all_primitives() {
356 let schema = "(\n s: String,\n i: Integer,\n f: Float,\n b: Bool,\n)";
357 let data = "(s: \"hi\", i: 42, f: 3.14, b: true)";
358 let errs = validate_str(schema, data);
359 assert!(errs.is_empty());
360 }
361
362 #[test]
364 fn valid_option_none() {
365 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: None)");
366 assert!(errs.is_empty());
367 }
368
369 #[test]
371 fn valid_option_some() {
372 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(5))");
373 assert!(errs.is_empty());
374 }
375
376 #[test]
378 fn valid_list_empty() {
379 let errs = validate_str("(\n tags: [String],\n)", "(tags: [])");
380 assert!(errs.is_empty());
381 }
382
383 #[test]
385 fn valid_list_populated() {
386 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"a\", \"b\"])");
387 assert!(errs.is_empty());
388 }
389
390 #[test]
392 fn valid_enum_variant() {
393 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B, C }";
394 let data = "(kind: B)";
395 let errs = validate_str(schema, data);
396 assert!(errs.is_empty());
397 }
398
399 #[test]
401 fn valid_enum_list() {
402 let schema = "(\n types: [CardType],\n)\nenum CardType { Creature, Trap }";
403 let data = "(types: [Creature, Trap])";
404 let errs = validate_str(schema, data);
405 assert!(errs.is_empty());
406 }
407
408 #[test]
410 fn valid_nested_struct() {
411 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
412 let data = "(cost: (generic: 2, sigil: 1))";
413 let errs = validate_str(schema, data);
414 assert!(errs.is_empty());
415 }
416
417 #[test]
423 fn type_mismatch_string_got_integer() {
424 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
425 assert_eq!(errs.len(), 1);
426 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
427 }
428
429 #[test]
431 fn type_mismatch_integer_got_string() {
432 let errs = validate_str("(\n age: Integer,\n)", "(age: \"five\")");
433 assert_eq!(errs.len(), 1);
434 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
435 }
436
437 #[test]
439 fn type_mismatch_float_got_integer() {
440 let errs = validate_str("(\n rate: Float,\n)", "(rate: 5)");
441 assert_eq!(errs.len(), 1);
442 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
443 }
444
445 #[test]
447 fn type_mismatch_bool_got_string() {
448 let errs = validate_str("(\n flag: Bool,\n)", "(flag: \"yes\")");
449 assert_eq!(errs.len(), 1);
450 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Bool"));
451 }
452
453 #[test]
455 fn type_mismatch_has_correct_path() {
456 let errs = validate_str("(\n name: String,\n)", "(name: 42)");
457 assert_eq!(errs[0].path, "name");
458 }
459
460 #[test]
466 fn missing_field_detected() {
467 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
468 assert_eq!(errs.len(), 1);
469 assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "age"));
470 }
471
472 #[test]
474 fn missing_field_has_correct_path() {
475 let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
476 assert_eq!(errs[0].path, "age");
477 }
478
479 #[test]
481 fn missing_field_span_points_to_close_paren() {
482 let data = "(name: \"hi\")";
483 let errs = validate_str("(\n name: String,\n age: Integer,\n)", data);
484 assert_eq!(errs[0].span.start.offset, data.len() - 1);
486 }
487
488 #[test]
490 fn missing_fields_all_reported() {
491 let errs = validate_str("(\n a: String,\n b: Integer,\n c: Bool,\n)", "()");
492 assert_eq!(errs.len(), 3);
493 }
494
495 #[test]
501 fn unknown_field_detected() {
502 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", colour: \"red\")");
503 assert_eq!(errs.len(), 1);
504 assert!(matches!(&errs[0].kind, ErrorKind::UnknownField { field_name } if field_name == "colour"));
505 }
506
507 #[test]
509 fn unknown_field_has_correct_path() {
510 let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", extra: 5)");
511 assert_eq!(errs[0].path, "extra");
512 }
513
514 #[test]
520 fn invalid_enum_variant() {
521 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
522 let errs = validate_str(schema, "(kind: C)");
523 assert_eq!(errs.len(), 1);
524 assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { variant, .. } if variant == "C"));
525 }
526
527 #[test]
529 fn enum_rejects_string() {
530 let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
531 let errs = validate_str(schema, "(kind: \"A\")");
532 assert_eq!(errs.len(), 1);
533 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
534 }
535
536 #[test]
542 fn expected_option_got_bare_value() {
543 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: 5)");
544 assert_eq!(errs.len(), 1);
545 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedOption { .. }));
546 }
547
548 #[test]
550 fn option_some_wrong_inner_type() {
551 let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(\"five\"))");
552 assert_eq!(errs.len(), 1);
553 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
554 }
555
556 #[test]
562 fn expected_list_got_string() {
563 let errs = validate_str("(\n tags: [String],\n)", "(tags: \"hi\")");
564 assert_eq!(errs.len(), 1);
565 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedList { .. }));
566 }
567
568 #[test]
570 fn list_element_wrong_type() {
571 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
572 assert_eq!(errs.len(), 1);
573 assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
574 }
575
576 #[test]
578 fn list_element_error_has_bracket_path() {
579 let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
580 assert_eq!(errs[0].path, "tags[1]");
581 }
582
583 #[test]
589 fn expected_struct_got_integer() {
590 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
591 let errs = validate_str(schema, "(cost: 5)");
592 assert_eq!(errs.len(), 1);
593 assert!(matches!(&errs[0].kind, ErrorKind::ExpectedStruct { .. }));
594 }
595
596 #[test]
602 fn nested_struct_type_mismatch_path() {
603 let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
604 let errs = validate_str(schema, "(cost: (generic: \"two\"))");
605 assert_eq!(errs.len(), 1);
606 assert_eq!(errs[0].path, "cost.generic");
607 }
608
609 #[test]
611 fn nested_struct_missing_field_path() {
612 let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
613 let errs = validate_str(schema, "(cost: (generic: 1))");
614 assert_eq!(errs.len(), 1);
615 assert_eq!(errs[0].path, "cost.sigil");
616 }
617
618 #[test]
624 fn multiple_errors_collected() {
625 let schema = "(\n name: String,\n age: Integer,\n active: Bool,\n)";
626 let data = "(name: 42, age: \"five\", active: \"yes\")";
627 let errs = validate_str(schema, data);
628 assert_eq!(errs.len(), 3);
629 }
630
631 #[test]
633 fn mixed_error_types_collected() {
634 let schema = "(\n name: String,\n age: Integer,\n)";
635 let data = "(name: \"hi\", age: \"five\", extra: true)";
636 let errs = validate_str(schema, data);
637 assert_eq!(errs.len(), 2);
639 }
640
641 #[test]
647 fn valid_card_data() {
648 let schema = r#"(
649 name: String,
650 card_types: [CardType],
651 legendary: Bool,
652 power: Option(Integer),
653 toughness: Option(Integer),
654 keywords: [String],
655 )
656 enum CardType { Creature, Trap, Artifact }"#;
657 let data = r#"(
658 name: "Ashborn Hound",
659 card_types: [Creature],
660 legendary: false,
661 power: Some(1),
662 toughness: Some(1),
663 keywords: [],
664 )"#;
665 let errs = validate_str(schema, data);
666 assert!(errs.is_empty());
667 }
668
669 #[test]
671 fn card_data_multiple_errors() {
672 let schema = r#"(
673 name: String,
674 card_types: [CardType],
675 legendary: Bool,
676 power: Option(Integer),
677 )
678 enum CardType { Creature, Trap }"#;
679 let data = r#"(
680 name: 42,
681 card_types: [Pirates],
682 legendary: false,
683 power: Some("five"),
684 )"#;
685 let errs = validate_str(schema, data);
686 assert_eq!(errs.len(), 3);
688 }
689}