Skip to main content

ron_schema/
validate.rs

1/*************************
2 * Author: Bradley Hunter
3 */
4
5use std::collections::{HashMap, HashSet};
6
7use crate::error::{ErrorKind, ValidationError};
8
9/// Validates a parsed RON value against a schema.
10///
11/// Returns all validation errors found — does not stop at the first error.
12/// An empty vec means the data is valid.
13#[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
23/// Produces a human-readable description of a RON value for error messages.
24fn 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::EnumVariant(name, _) => format!("{name}(...)"),
39        RonValue::List(_) => "List".to_string(),
40        RonValue::Map(_) => "Map".to_string(),
41        RonValue::Tuple(_) => "Tuple".to_string(),
42        RonValue::Struct(_) => "Struct".to_string(),
43    }
44}
45
46/// Builds a dot-separated field path for error messages.
47///
48/// An empty parent means we're at the root, so just return the field name.
49/// Otherwise, join with a dot: `"cost"` + `"generic"` → `"cost.generic"`.
50fn build_path(parent: &str, field: &str) -> String {
51    if parent.is_empty() {
52        field.to_string()
53    } else {
54        format!("{parent}.{field}")
55    }
56}
57
58/// Validates a single RON value against an expected schema type.
59///
60/// Matches on the expected type and checks that the actual value conforms.
61/// For composite types (Option, List, Struct), recurses into the inner values.
62/// Errors are collected into the `errors` vec — validation does not stop at the first error.
63#[allow(clippy::too_many_lines)]
64fn validate_type(
65    expected: &SchemaType,
66    actual: &Spanned<RonValue>,
67    path: &str,
68    errors: &mut Vec<ValidationError>,
69    enums: &HashMap<String, EnumDef>,
70    aliases: &HashMap<String, Spanned<SchemaType>>,
71) {
72    match expected {
73        // Primitives: check that the value variant matches the schema type.
74        SchemaType::String => {
75            if !matches!(actual.value, RonValue::String(_)) {
76                errors.push(ValidationError {
77                    path: path.to_string(),
78                    span: actual.span,
79                    kind: ErrorKind::TypeMismatch {
80                        expected: "String".to_string(),
81                        found: describe(&actual.value),
82                    },
83                });
84            }
85        }
86        SchemaType::Integer => {
87            if !matches!(actual.value, RonValue::Integer(_)) {
88                errors.push(ValidationError {
89                    path: path.to_string(),
90                    span: actual.span,
91                    kind: ErrorKind::TypeMismatch {
92                        expected: "Integer".to_string(),
93                        found: describe(&actual.value),
94                    },
95                });
96            }
97        }
98        SchemaType::Float => {
99            if !matches!(actual.value, RonValue::Float(_)) {
100                errors.push(ValidationError {
101                    path: path.to_string(),
102                    span: actual.span,
103                    kind: ErrorKind::TypeMismatch {
104                        expected: "Float".to_string(),
105                        found: describe(&actual.value),
106                    },
107                });
108            }
109        }
110        SchemaType::Bool => {
111            if !matches!(actual.value, RonValue::Bool(_)) {
112                errors.push(ValidationError {
113                    path: path.to_string(),
114                    span: actual.span,
115                    kind: ErrorKind::TypeMismatch {
116                        expected: "Bool".to_string(),
117                        found: describe(&actual.value),
118                    },
119                });
120            }
121        }
122
123        // Option: None is always valid. Some(inner) recurses into the inner value.
124        // Anything else (bare integer, string, etc.) is an error — must be Some(...) or None.
125        SchemaType::Option(inner_type) => match &actual.value {
126            RonValue::Option(None) => {}
127            RonValue::Option(Some(inner_value)) => {
128                validate_type(inner_type, inner_value, path, errors, enums, aliases);
129            }
130            _ => {
131                errors.push(ValidationError {
132                    path: path.to_string(),
133                    span: actual.span,
134                    kind: ErrorKind::ExpectedOption {
135                        found: describe(&actual.value),
136                    },
137                });
138            }
139        },
140
141        // List: check value is a list, then validate each element against the element type.
142        // Path gets bracket notation: "card_types[0]", "card_types[1]", etc.
143        SchemaType::List(element_type) => {
144            if let RonValue::List(elements) = &actual.value {
145                for (index, element) in elements.iter().enumerate() {
146                    let element_path = format!("{path}[{index}]");
147                    validate_type(element_type, element, &element_path, errors, enums, aliases);
148                }
149            } else {
150                errors.push(ValidationError {
151                    path: path.to_string(),
152                    span: actual.span,
153                    kind: ErrorKind::ExpectedList {
154                        found: describe(&actual.value),
155                    },
156                });
157            }
158        }
159
160        // EnumRef: value must be a known variant. Unit variants are bare identifiers,
161        // data variants are EnumVariant(name, data). The schema defines which variants
162        // exist and whether they carry data.
163        SchemaType::EnumRef(enum_name) => {
164            let enum_def = &enums[enum_name];
165            let variant_names: Vec<String> = enum_def.variants.keys().cloned().collect();
166
167            match &actual.value {
168                // Bare identifier — must be a known unit variant
169                RonValue::Identifier(variant) => {
170                    match enum_def.variants.get(variant) {
171                        None => {
172                            errors.push(ValidationError {
173                                path: path.to_string(),
174                                span: actual.span,
175                                kind: ErrorKind::InvalidEnumVariant {
176                                    enum_name: enum_name.clone(),
177                                    variant: variant.clone(),
178                                    valid: variant_names,
179                                },
180                            });
181                        }
182                        Some(Some(_expected_data_type)) => {
183                            // Variant exists but expects data — bare identifier is wrong
184                            errors.push(ValidationError {
185                                path: path.to_string(),
186                                span: actual.span,
187                                kind: ErrorKind::InvalidVariantData {
188                                    enum_name: enum_name.clone(),
189                                    variant: variant.clone(),
190                                    expected: "data".to_string(),
191                                    found: "unit variant".to_string(),
192                                },
193                            });
194                        }
195                        Some(None) => {} // Unit variant, matches
196                    }
197                }
198                // Enum variant with data — must be a known data variant, and data must match
199                RonValue::EnumVariant(variant, data) => {
200                    match enum_def.variants.get(variant) {
201                        None => {
202                            errors.push(ValidationError {
203                                path: path.to_string(),
204                                span: actual.span,
205                                kind: ErrorKind::InvalidEnumVariant {
206                                    enum_name: enum_name.clone(),
207                                    variant: variant.clone(),
208                                    valid: variant_names,
209                                },
210                            });
211                        }
212                        Some(None) => {
213                            // Variant exists but is a unit variant — data is unexpected
214                            errors.push(ValidationError {
215                                path: path.to_string(),
216                                span: actual.span,
217                                kind: ErrorKind::InvalidVariantData {
218                                    enum_name: enum_name.clone(),
219                                    variant: variant.clone(),
220                                    expected: "unit variant".to_string(),
221                                    found: describe(&data.value),
222                                },
223                            });
224                        }
225                        Some(Some(expected_data_type)) => {
226                            // Validate the associated data
227                            validate_type(expected_data_type, data, path, errors, enums, aliases);
228                        }
229                    }
230                }
231                // Wrong value type entirely
232                _ => {
233                    errors.push(ValidationError {
234                        path: path.to_string(),
235                        span: actual.span,
236                        kind: ErrorKind::TypeMismatch {
237                            expected: enum_name.clone(),
238                            found: describe(&actual.value),
239                        },
240                    });
241                }
242            }
243        }
244
245        // Map: check value is a map, then validate each key and value.
246        SchemaType::Map(key_type, value_type) => {
247            if let RonValue::Map(entries) = &actual.value {
248                for (key, value) in entries {
249                    let key_desc = describe(&key.value);
250                    validate_type(key_type, key, path, errors, enums, aliases);
251                    let entry_path = format!("{path}[{key_desc}]");
252                    validate_type(value_type, value, &entry_path, errors, enums, aliases);
253                }
254            } else {
255                errors.push(ValidationError {
256                    path: path.to_string(),
257                    span: actual.span,
258                    kind: ErrorKind::ExpectedMap {
259                        found: describe(&actual.value),
260                    },
261                });
262            }
263        }
264
265        // Tuple: check value is a tuple, check element count, validate each element.
266        SchemaType::Tuple(element_types) => {
267            if let RonValue::Tuple(elements) = &actual.value {
268                if elements.len() == element_types.len() {
269                    for (index, (expected_type, element)) in element_types.iter().zip(elements).enumerate() {
270                        let element_path = format!("{path}.{index}");
271                        validate_type(expected_type, element, &element_path, errors, enums, aliases);
272                    }
273                } else {
274                    errors.push(ValidationError {
275                        path: path.to_string(),
276                        span: actual.span,
277                        kind: ErrorKind::TupleLengthMismatch {
278                            expected: element_types.len(),
279                            found: elements.len(),
280                        },
281                    });
282                }
283            } else {
284                errors.push(ValidationError {
285                    path: path.to_string(),
286                    span: actual.span,
287                    kind: ErrorKind::ExpectedTuple {
288                        found: describe(&actual.value),
289                    },
290                });
291            }
292        }
293
294        // AliasRef: look up the alias and validate against the resolved type.
295        // Error messages use the alias name (e.g., "expected Cost") not the expanded type.
296        SchemaType::AliasRef(alias_name) => {
297            if let Some(resolved) = aliases.get(alias_name) {
298                validate_type(&resolved.value, actual, path, errors, enums, aliases);
299            }
300            // If alias doesn't exist, the parser already caught it — unreachable in practice.
301        }
302
303        // Nested struct: recurse into validate_struct.
304        SchemaType::Struct(struct_def) => {
305            validate_struct(struct_def, actual, path, errors, enums, aliases);
306        }
307    }
308}
309
310/// Validates a RON struct against a schema struct definition.
311///
312/// Three checks:
313/// 1. Missing fields — in schema but not in data (points to closing paren)
314/// 2. Unknown fields — in data but not in schema (points to field name)
315/// 3. Matching fields — present in both, recurse into `validate_type`
316fn validate_struct(
317    struct_def: &StructDef,
318    actual: &Spanned<RonValue>,
319    path: &str,
320    errors: &mut Vec<ValidationError>,
321    enums: &HashMap<String, EnumDef>,
322    aliases: &HashMap<String, Spanned<SchemaType>>,
323) {
324    // Value must be a struct — if not, report and bail (can't check fields of a non-struct)
325    let RonValue::Struct(data_struct) = &actual.value else {
326        errors.push(ValidationError {
327            path: path.to_string(),
328            span: actual.span,
329            kind: ErrorKind::ExpectedStruct {
330                found: describe(&actual.value),
331            },
332        });
333        return;
334    };
335
336    // Build a lookup map from data fields for O(1) access by name
337    let data_map: HashMap<&str, &Spanned<RonValue>> = data_struct
338        .fields
339        .iter()
340        .map(|(name, value)| (name.value.as_str(), value))
341        .collect();
342
343    // Build a set of schema field names for unknown-field detection
344    let schema_names: HashSet<&str> = struct_def
345        .fields
346        .iter()
347        .map(|f| f.name.value.as_str())
348        .collect();
349
350    // 1. Missing fields: in schema but not in data
351    for field_def in &struct_def.fields {
352        if !data_map.contains_key(field_def.name.value.as_str()) {
353            errors.push(ValidationError {
354                path: build_path(path, &field_def.name.value),
355                span: data_struct.close_span,
356                kind: ErrorKind::MissingField {
357                    field_name: field_def.name.value.clone(),
358                },
359            });
360        }
361    }
362
363    // 2. Unknown fields: in data but not in schema
364    for (name, _value) in &data_struct.fields {
365        if !schema_names.contains(name.value.as_str()) {
366            errors.push(ValidationError {
367                path: build_path(path, &name.value),
368                span: name.span,
369                kind: ErrorKind::UnknownField {
370                    field_name: name.value.clone(),
371                },
372            });
373        }
374    }
375
376    // 3. Matching fields: validate each against its expected type
377    for field_def in &struct_def.fields {
378        if let Some(data_value) = data_map.get(field_def.name.value.as_str()) {
379            let field_path = build_path(path, &field_def.name.value);
380            validate_type(&field_def.type_.value, data_value, &field_path, errors, enums, aliases);
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crate::schema::parser::parse_schema;
389    use crate::ron::parser::parse_ron;
390
391    /// Parses both a schema and data string, runs validation, returns errors.
392    fn validate_str(schema_src: &str, data_src: &str) -> Vec<ValidationError> {
393        let schema = parse_schema(schema_src).expect("test schema should parse");
394        let data = parse_ron(data_src).expect("test data should parse");
395        validate(&schema, &data)
396    }
397
398    // ========================================================
399    // describe() tests
400    // ========================================================
401
402    // Describes a string value.
403    #[test]
404    fn describe_string() {
405        assert_eq!(describe(&RonValue::String("hi".to_string())), "String(\"hi\")");
406    }
407
408    // Truncates long strings at 20 characters.
409    #[test]
410    fn describe_string_truncated() {
411        let long = "a".repeat(30);
412        let desc = describe(&RonValue::String(long));
413        assert!(desc.contains("..."));
414    }
415
416    // Describes an integer.
417    #[test]
418    fn describe_integer() {
419        assert_eq!(describe(&RonValue::Integer(42)), "Integer(42)");
420    }
421
422    // Describes a float.
423    #[test]
424    fn describe_float() {
425        assert_eq!(describe(&RonValue::Float(3.14)), "Float(3.14)");
426    }
427
428    // Describes a bool.
429    #[test]
430    fn describe_bool() {
431        assert_eq!(describe(&RonValue::Bool(true)), "Bool(true)");
432    }
433
434    // Describes an identifier.
435    #[test]
436    fn describe_identifier() {
437        assert_eq!(describe(&RonValue::Identifier("Creature".to_string())), "Identifier(Creature)");
438    }
439
440    // ========================================================
441    // build_path() tests
442    // ========================================================
443
444    // Root-level field has no dot prefix.
445    #[test]
446    fn build_path_root() {
447        assert_eq!(build_path("", "name"), "name");
448    }
449
450    // Nested field gets dot notation.
451    #[test]
452    fn build_path_nested() {
453        assert_eq!(build_path("cost", "generic"), "cost.generic");
454    }
455
456    // Deeply nested path.
457    #[test]
458    fn build_path_deep() {
459        assert_eq!(build_path("a.b", "c"), "a.b.c");
460    }
461
462    // ========================================================
463    // Valid data — no errors
464    // ========================================================
465
466    // Valid data with a single string field.
467    #[test]
468    fn valid_single_string_field() {
469        let errs = validate_str("(\n  name: String,\n)", "(name: \"hello\")");
470        assert!(errs.is_empty());
471    }
472
473    // Valid data with all primitive types.
474    #[test]
475    fn valid_all_primitives() {
476        let schema = "(\n  s: String,\n  i: Integer,\n  f: Float,\n  b: Bool,\n)";
477        let data = "(s: \"hi\", i: 42, f: 3.14, b: true)";
478        let errs = validate_str(schema, data);
479        assert!(errs.is_empty());
480    }
481
482    // Valid data with None option.
483    #[test]
484    fn valid_option_none() {
485        let errs = validate_str("(\n  power: Option(Integer),\n)", "(power: None)");
486        assert!(errs.is_empty());
487    }
488
489    // Valid data with Some option.
490    #[test]
491    fn valid_option_some() {
492        let errs = validate_str("(\n  power: Option(Integer),\n)", "(power: Some(5))");
493        assert!(errs.is_empty());
494    }
495
496    // Valid data with empty list.
497    #[test]
498    fn valid_list_empty() {
499        let errs = validate_str("(\n  tags: [String],\n)", "(tags: [])");
500        assert!(errs.is_empty());
501    }
502
503    // Valid data with populated list.
504    #[test]
505    fn valid_list_populated() {
506        let errs = validate_str("(\n  tags: [String],\n)", "(tags: [\"a\", \"b\"])");
507        assert!(errs.is_empty());
508    }
509
510    // Valid data with enum variant.
511    #[test]
512    fn valid_enum_variant() {
513        let schema = "(\n  kind: Kind,\n)\nenum Kind { A, B, C }";
514        let data = "(kind: B)";
515        let errs = validate_str(schema, data);
516        assert!(errs.is_empty());
517    }
518
519    // Valid data with list of enum variants.
520    #[test]
521    fn valid_enum_list() {
522        let schema = "(\n  types: [CardType],\n)\nenum CardType { Creature, Trap }";
523        let data = "(types: [Creature, Trap])";
524        let errs = validate_str(schema, data);
525        assert!(errs.is_empty());
526    }
527
528    // Valid data with nested struct.
529    #[test]
530    fn valid_nested_struct() {
531        let schema = "(\n  cost: (\n    generic: Integer,\n    sigil: Integer,\n  ),\n)";
532        let data = "(cost: (generic: 2, sigil: 1))";
533        let errs = validate_str(schema, data);
534        assert!(errs.is_empty());
535    }
536
537    // ========================================================
538    // TypeMismatch errors
539    // ========================================================
540
541    // String field rejects integer value.
542    #[test]
543    fn type_mismatch_string_got_integer() {
544        let errs = validate_str("(\n  name: String,\n)", "(name: 42)");
545        assert_eq!(errs.len(), 1);
546        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
547    }
548
549    // Integer field rejects string value.
550    #[test]
551    fn type_mismatch_integer_got_string() {
552        let errs = validate_str("(\n  age: Integer,\n)", "(age: \"five\")");
553        assert_eq!(errs.len(), 1);
554        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
555    }
556
557    // Float field rejects integer value.
558    #[test]
559    fn type_mismatch_float_got_integer() {
560        let errs = validate_str("(\n  rate: Float,\n)", "(rate: 5)");
561        assert_eq!(errs.len(), 1);
562        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
563    }
564
565    // Bool field rejects string value.
566    #[test]
567    fn type_mismatch_bool_got_string() {
568        let errs = validate_str("(\n  flag: Bool,\n)", "(flag: \"yes\")");
569        assert_eq!(errs.len(), 1);
570        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Bool"));
571    }
572
573    // Error path is correct for type mismatch.
574    #[test]
575    fn type_mismatch_has_correct_path() {
576        let errs = validate_str("(\n  name: String,\n)", "(name: 42)");
577        assert_eq!(errs[0].path, "name");
578    }
579
580    // ========================================================
581    // MissingField errors
582    // ========================================================
583
584    // Missing field is detected.
585    #[test]
586    fn missing_field_detected() {
587        let errs = validate_str("(\n  name: String,\n  age: Integer,\n)", "(name: \"hi\")");
588        assert_eq!(errs.len(), 1);
589        assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "age"));
590    }
591
592    // Missing field path is correct.
593    #[test]
594    fn missing_field_has_correct_path() {
595        let errs = validate_str("(\n  name: String,\n  age: Integer,\n)", "(name: \"hi\")");
596        assert_eq!(errs[0].path, "age");
597    }
598
599    // Missing field span points to close paren.
600    #[test]
601    fn missing_field_span_points_to_close_paren() {
602        let data = "(name: \"hi\")";
603        let errs = validate_str("(\n  name: String,\n  age: Integer,\n)", data);
604        // close paren is the last character
605        assert_eq!(errs[0].span.start.offset, data.len() - 1);
606    }
607
608    // Multiple missing fields are all reported.
609    #[test]
610    fn missing_fields_all_reported() {
611        let errs = validate_str("(\n  a: String,\n  b: Integer,\n  c: Bool,\n)", "()");
612        assert_eq!(errs.len(), 3);
613    }
614
615    // ========================================================
616    // UnknownField errors
617    // ========================================================
618
619    // Unknown field is detected.
620    #[test]
621    fn unknown_field_detected() {
622        let errs = validate_str("(\n  name: String,\n)", "(name: \"hi\", colour: \"red\")");
623        assert_eq!(errs.len(), 1);
624        assert!(matches!(&errs[0].kind, ErrorKind::UnknownField { field_name } if field_name == "colour"));
625    }
626
627    // Unknown field path is correct.
628    #[test]
629    fn unknown_field_has_correct_path() {
630        let errs = validate_str("(\n  name: String,\n)", "(name: \"hi\", extra: 5)");
631        assert_eq!(errs[0].path, "extra");
632    }
633
634    // ========================================================
635    // InvalidEnumVariant errors
636    // ========================================================
637
638    // Invalid enum variant is detected.
639    #[test]
640    fn invalid_enum_variant() {
641        let schema = "(\n  kind: Kind,\n)\nenum Kind { A, B }";
642        let errs = validate_str(schema, "(kind: C)");
643        assert_eq!(errs.len(), 1);
644        assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { variant, .. } if variant == "C"));
645    }
646
647    // Enum field rejects string value (should be bare identifier).
648    #[test]
649    fn enum_rejects_string() {
650        let schema = "(\n  kind: Kind,\n)\nenum Kind { A, B }";
651        let errs = validate_str(schema, "(kind: \"A\")");
652        assert_eq!(errs.len(), 1);
653        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
654    }
655
656    // ========================================================
657    // ExpectedOption errors
658    // ========================================================
659
660    // Option field rejects bare integer (not wrapped in Some).
661    #[test]
662    fn expected_option_got_bare_value() {
663        let errs = validate_str("(\n  power: Option(Integer),\n)", "(power: 5)");
664        assert_eq!(errs.len(), 1);
665        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedOption { .. }));
666    }
667
668    // Some wrapping wrong type is an error.
669    #[test]
670    fn option_some_wrong_inner_type() {
671        let errs = validate_str("(\n  power: Option(Integer),\n)", "(power: Some(\"five\"))");
672        assert_eq!(errs.len(), 1);
673        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
674    }
675
676    // ========================================================
677    // ExpectedList errors
678    // ========================================================
679
680    // List field rejects non-list value.
681    #[test]
682    fn expected_list_got_string() {
683        let errs = validate_str("(\n  tags: [String],\n)", "(tags: \"hi\")");
684        assert_eq!(errs.len(), 1);
685        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedList { .. }));
686    }
687
688    // List element with wrong type is an error.
689    #[test]
690    fn list_element_wrong_type() {
691        let errs = validate_str("(\n  tags: [String],\n)", "(tags: [\"ok\", 42])");
692        assert_eq!(errs.len(), 1);
693        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
694    }
695
696    // List element error has bracket path.
697    #[test]
698    fn list_element_error_has_bracket_path() {
699        let errs = validate_str("(\n  tags: [String],\n)", "(tags: [\"ok\", 42])");
700        assert_eq!(errs[0].path, "tags[1]");
701    }
702
703    // ========================================================
704    // ExpectedStruct errors
705    // ========================================================
706
707    // Struct field rejects non-struct value.
708    #[test]
709    fn expected_struct_got_integer() {
710        let schema = "(\n  cost: (\n    generic: Integer,\n  ),\n)";
711        let errs = validate_str(schema, "(cost: 5)");
712        assert_eq!(errs.len(), 1);
713        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedStruct { .. }));
714    }
715
716    // ========================================================
717    // Nested validation
718    // ========================================================
719
720    // Type mismatch in nested struct has correct path.
721    #[test]
722    fn nested_struct_type_mismatch_path() {
723        let schema = "(\n  cost: (\n    generic: Integer,\n  ),\n)";
724        let errs = validate_str(schema, "(cost: (generic: \"two\"))");
725        assert_eq!(errs.len(), 1);
726        assert_eq!(errs[0].path, "cost.generic");
727    }
728
729    // Missing field in nested struct has correct path.
730    #[test]
731    fn nested_struct_missing_field_path() {
732        let schema = "(\n  cost: (\n    generic: Integer,\n    sigil: Integer,\n  ),\n)";
733        let errs = validate_str(schema, "(cost: (generic: 1))");
734        assert_eq!(errs.len(), 1);
735        assert_eq!(errs[0].path, "cost.sigil");
736    }
737
738    // ========================================================
739    // Multiple errors collected
740    // ========================================================
741
742    // Multiple errors in one struct are all reported.
743    #[test]
744    fn multiple_errors_collected() {
745        let schema = "(\n  name: String,\n  age: Integer,\n  active: Bool,\n)";
746        let data = "(name: 42, age: \"five\", active: \"yes\")";
747        let errs = validate_str(schema, data);
748        assert_eq!(errs.len(), 3);
749    }
750
751    // Mixed error types are all collected.
752    #[test]
753    fn mixed_error_types_collected() {
754        let schema = "(\n  name: String,\n  age: Integer,\n)";
755        let data = "(name: \"hi\", age: \"five\", extra: true)";
756        let errs = validate_str(schema, data);
757        // age is TypeMismatch, extra is UnknownField
758        assert_eq!(errs.len(), 2);
759    }
760
761    // ========================================================
762    // Integration: card-like schema
763    // ========================================================
764
765    // Valid card data produces no errors.
766    #[test]
767    fn valid_card_data() {
768        let schema = r#"(
769            name: String,
770            card_types: [CardType],
771            legendary: Bool,
772            power: Option(Integer),
773            toughness: Option(Integer),
774            keywords: [String],
775        )
776        enum CardType { Creature, Trap, Artifact }"#;
777        let data = r#"(
778            name: "Ashborn Hound",
779            card_types: [Creature],
780            legendary: false,
781            power: Some(1),
782            toughness: Some(1),
783            keywords: [],
784        )"#;
785        let errs = validate_str(schema, data);
786        assert!(errs.is_empty());
787    }
788
789    // Card data with multiple errors reports all of them.
790    #[test]
791    fn card_data_multiple_errors() {
792        let schema = r#"(
793            name: String,
794            card_types: [CardType],
795            legendary: Bool,
796            power: Option(Integer),
797        )
798        enum CardType { Creature, Trap }"#;
799        let data = r#"(
800            name: 42,
801            card_types: [Pirates],
802            legendary: false,
803            power: Some("five"),
804        )"#;
805        let errs = validate_str(schema, data);
806        // name: TypeMismatch, card_types[0]: InvalidEnumVariant, power: TypeMismatch
807        assert_eq!(errs.len(), 3);
808    }
809
810    // ========================================================
811    // Type alias validation
812    // ========================================================
813
814    // Alias to a struct type validates correctly.
815    #[test]
816    fn alias_struct_valid() {
817        let schema = "(\n  cost: Cost,\n)\ntype Cost = (generic: Integer,)";
818        let data = "(cost: (generic: 5))";
819        let errs = validate_str(schema, data);
820        assert!(errs.is_empty());
821    }
822
823    // Alias to a struct type catches type mismatch inside.
824    #[test]
825    fn alias_struct_type_mismatch() {
826        let schema = "(\n  cost: Cost,\n)\ntype Cost = (generic: Integer,)";
827        let data = "(cost: (generic: \"five\"))";
828        let errs = validate_str(schema, data);
829        assert_eq!(errs.len(), 1);
830        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
831    }
832
833    // Alias to a primitive type validates correctly.
834    #[test]
835    fn alias_primitive_valid() {
836        let schema = "(\n  name: Name,\n)\ntype Name = String";
837        let data = "(name: \"hello\")";
838        let errs = validate_str(schema, data);
839        assert!(errs.is_empty());
840    }
841
842    // Alias to a primitive type catches mismatch.
843    #[test]
844    fn alias_primitive_mismatch() {
845        let schema = "(\n  name: Name,\n)\ntype Name = String";
846        let data = "(name: 42)";
847        let errs = validate_str(schema, data);
848        assert_eq!(errs.len(), 1);
849    }
850
851    // Alias used inside a list validates each element.
852    #[test]
853    fn alias_in_list_valid() {
854        let schema = "(\n  costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
855        let data = "(costs: [(generic: 1), (generic: 2)])";
856        let errs = validate_str(schema, data);
857        assert!(errs.is_empty());
858    }
859
860    // Alias used inside a list catches element errors.
861    #[test]
862    fn alias_in_list_element_error() {
863        let schema = "(\n  costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
864        let data = "(costs: [(generic: 1), (generic: \"two\")])";
865        let errs = validate_str(schema, data);
866        assert_eq!(errs.len(), 1);
867        assert_eq!(errs[0].path, "costs[1].generic");
868    }
869
870    // ========================================================
871    // Map validation
872    // ========================================================
873
874    // Valid map with string keys and integer values.
875    #[test]
876    fn map_valid() {
877        let schema = "(\n  attrs: {String: Integer},\n)";
878        let data = "(attrs: {\"str\": 5, \"dex\": 3})";
879        let errs = validate_str(schema, data);
880        assert!(errs.is_empty());
881    }
882
883    // Empty map is always valid.
884    #[test]
885    fn map_empty_valid() {
886        let schema = "(\n  attrs: {String: Integer},\n)";
887        let data = "(attrs: {})";
888        let errs = validate_str(schema, data);
889        assert!(errs.is_empty());
890    }
891
892    // Non-map value where map expected.
893    #[test]
894    fn map_expected_got_string() {
895        let schema = "(\n  attrs: {String: Integer},\n)";
896        let data = "(attrs: \"not a map\")";
897        let errs = validate_str(schema, data);
898        assert_eq!(errs.len(), 1);
899        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedMap { .. }));
900    }
901
902    // Map value with wrong type.
903    #[test]
904    fn map_wrong_value_type() {
905        let schema = "(\n  attrs: {String: Integer},\n)";
906        let data = "(attrs: {\"str\": \"five\"})";
907        let errs = validate_str(schema, data);
908        assert_eq!(errs.len(), 1);
909        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
910    }
911
912    // Map key with wrong type.
913    #[test]
914    fn map_wrong_key_type() {
915        let schema = "(\n  attrs: {String: Integer},\n)";
916        let data = "(attrs: {42: 5})";
917        let errs = validate_str(schema, data);
918        assert_eq!(errs.len(), 1);
919        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
920    }
921
922    // ========================================================
923    // Tuple validation
924    // ========================================================
925
926    // Valid tuple.
927    #[test]
928    fn tuple_valid() {
929        let schema = "(\n  pos: (Float, Float),\n)";
930        let data = "(pos: (1.0, 2.5))";
931        let errs = validate_str(schema, data);
932        assert!(errs.is_empty());
933    }
934
935    // Non-tuple value where tuple expected.
936    #[test]
937    fn tuple_expected_got_string() {
938        let schema = "(\n  pos: (Float, Float),\n)";
939        let data = "(pos: \"not a tuple\")";
940        let errs = validate_str(schema, data);
941        assert_eq!(errs.len(), 1);
942        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedTuple { .. }));
943    }
944
945    // Tuple with wrong element count.
946    #[test]
947    fn tuple_wrong_length() {
948        let schema = "(\n  pos: (Float, Float),\n)";
949        let data = "(pos: (1.0, 2.5, 3.0))";
950        let errs = validate_str(schema, data);
951        assert_eq!(errs.len(), 1);
952        assert!(matches!(&errs[0].kind, ErrorKind::TupleLengthMismatch { expected: 2, found: 3 }));
953    }
954
955    // Tuple with wrong element type.
956    #[test]
957    fn tuple_wrong_element_type() {
958        let schema = "(\n  pos: (Float, Float),\n)";
959        let data = "(pos: (1.0, \"bad\"))";
960        let errs = validate_str(schema, data);
961        assert_eq!(errs.len(), 1);
962        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
963    }
964
965    // Tuple element error has correct path.
966    #[test]
967    fn tuple_element_error_path() {
968        let schema = "(\n  pos: (Float, Float),\n)";
969        let data = "(pos: (1.0, \"bad\"))";
970        let errs = validate_str(schema, data);
971        assert_eq!(errs[0].path, "pos.1");
972    }
973
974    // ========================================================
975    // Enum variant with data — validation
976    // ========================================================
977
978    // Valid data variant.
979    #[test]
980    fn enum_data_variant_valid() {
981        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
982        let data = "(effect: Damage(5))";
983        let errs = validate_str(schema, data);
984        assert!(errs.is_empty());
985    }
986
987    // Valid unit variant alongside data variants.
988    #[test]
989    fn enum_unit_variant_valid() {
990        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
991        let data = "(effect: Draw)";
992        let errs = validate_str(schema, data);
993        assert!(errs.is_empty());
994    }
995
996    // Data variant with wrong inner type.
997    #[test]
998    fn enum_data_variant_wrong_type() {
999        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1000        let data = "(effect: Damage(\"five\"))";
1001        let errs = validate_str(schema, data);
1002        assert_eq!(errs.len(), 1);
1003        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
1004    }
1005
1006    // Unknown variant name with data.
1007    #[test]
1008    fn enum_data_variant_unknown() {
1009        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1010        let data = "(effect: Explode(10))";
1011        let errs = validate_str(schema, data);
1012        assert_eq!(errs.len(), 1);
1013        assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { .. }));
1014    }
1015
1016    // Bare identifier for a variant that expects data.
1017    #[test]
1018    fn enum_missing_data() {
1019        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1020        let data = "(effect: Damage)";
1021        let errs = validate_str(schema, data);
1022        assert_eq!(errs.len(), 1);
1023        assert!(matches!(&errs[0].kind, ErrorKind::InvalidVariantData { .. }));
1024    }
1025
1026    // Data provided for a unit variant.
1027    #[test]
1028    fn enum_unexpected_data() {
1029        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1030        let data = "(effect: Draw(5))";
1031        let errs = validate_str(schema, data);
1032        assert_eq!(errs.len(), 1);
1033        assert!(matches!(&errs[0].kind, ErrorKind::InvalidVariantData { .. }));
1034    }
1035}