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 (skip fields with defaults)
351    for field_def in &struct_def.fields {
352        if !data_map.contains_key(field_def.name.value.as_str()) && field_def.default.is_none() {
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    // Default values — fields with defaults are optional
617    // ========================================================
618
619    // Field with default does not produce MissingField when absent.
620    #[test]
621    fn default_field_not_required() {
622        let errs = validate_str("(\n  name: String,\n  label: String = \"none\",\n)", "(name: \"hi\")");
623        assert!(errs.is_empty());
624    }
625
626    // Field with default still validates type when present.
627    #[test]
628    fn default_field_still_validates_type() {
629        let errs = validate_str("(\n  name: String,\n  label: String = \"none\",\n)", "(name: \"hi\", label: 42)");
630        assert_eq!(errs.len(), 1);
631        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
632    }
633
634    // Field with default accepts correct type when present.
635    #[test]
636    fn default_field_accepts_correct_type() {
637        let errs = validate_str("(\n  name: String,\n  label: String = \"none\",\n)", "(name: \"hi\", label: \"custom\")");
638        assert!(errs.is_empty());
639    }
640
641    // Field without default still produces MissingField.
642    #[test]
643    fn non_default_field_still_required() {
644        let errs = validate_str("(\n  name: String,\n  label: String = \"none\",\n)", "(label: \"hi\")");
645        assert_eq!(errs.len(), 1);
646        assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "name"));
647    }
648
649    // Multiple fields with defaults can all be absent.
650    #[test]
651    fn multiple_default_fields_all_absent() {
652        let errs = validate_str(
653            "(\n  name: String,\n  a: Integer = 0,\n  b: Bool = false,\n  c: String = \"x\",\n)",
654            "(name: \"hi\")",
655        );
656        assert!(errs.is_empty());
657    }
658
659    // Default on Option field allows absence.
660    #[test]
661    fn default_option_field_not_required() {
662        let errs = validate_str("(\n  name: String,\n  tag: Option(String) = None,\n)", "(name: \"hi\")");
663        assert!(errs.is_empty());
664    }
665
666    // Default on list field allows absence.
667    #[test]
668    fn default_list_field_not_required() {
669        let errs = validate_str("(\n  name: String,\n  tags: [String] = [],\n)", "(name: \"hi\")");
670        assert!(errs.is_empty());
671    }
672
673    // ========================================================
674    // UnknownField errors
675    // ========================================================
676
677    // Unknown field is detected.
678    #[test]
679    fn unknown_field_detected() {
680        let errs = validate_str("(\n  name: String,\n)", "(name: \"hi\", colour: \"red\")");
681        assert_eq!(errs.len(), 1);
682        assert!(matches!(&errs[0].kind, ErrorKind::UnknownField { field_name } if field_name == "colour"));
683    }
684
685    // Unknown field path is correct.
686    #[test]
687    fn unknown_field_has_correct_path() {
688        let errs = validate_str("(\n  name: String,\n)", "(name: \"hi\", extra: 5)");
689        assert_eq!(errs[0].path, "extra");
690    }
691
692    // ========================================================
693    // InvalidEnumVariant errors
694    // ========================================================
695
696    // Invalid enum variant is detected.
697    #[test]
698    fn invalid_enum_variant() {
699        let schema = "(\n  kind: Kind,\n)\nenum Kind { A, B }";
700        let errs = validate_str(schema, "(kind: C)");
701        assert_eq!(errs.len(), 1);
702        assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { variant, .. } if variant == "C"));
703    }
704
705    // Enum field rejects string value (should be bare identifier).
706    #[test]
707    fn enum_rejects_string() {
708        let schema = "(\n  kind: Kind,\n)\nenum Kind { A, B }";
709        let errs = validate_str(schema, "(kind: \"A\")");
710        assert_eq!(errs.len(), 1);
711        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
712    }
713
714    // ========================================================
715    // ExpectedOption errors
716    // ========================================================
717
718    // Option field rejects bare integer (not wrapped in Some).
719    #[test]
720    fn expected_option_got_bare_value() {
721        let errs = validate_str("(\n  power: Option(Integer),\n)", "(power: 5)");
722        assert_eq!(errs.len(), 1);
723        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedOption { .. }));
724    }
725
726    // Some wrapping wrong type is an error.
727    #[test]
728    fn option_some_wrong_inner_type() {
729        let errs = validate_str("(\n  power: Option(Integer),\n)", "(power: Some(\"five\"))");
730        assert_eq!(errs.len(), 1);
731        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
732    }
733
734    // ========================================================
735    // ExpectedList errors
736    // ========================================================
737
738    // List field rejects non-list value.
739    #[test]
740    fn expected_list_got_string() {
741        let errs = validate_str("(\n  tags: [String],\n)", "(tags: \"hi\")");
742        assert_eq!(errs.len(), 1);
743        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedList { .. }));
744    }
745
746    // List element with wrong type is an error.
747    #[test]
748    fn list_element_wrong_type() {
749        let errs = validate_str("(\n  tags: [String],\n)", "(tags: [\"ok\", 42])");
750        assert_eq!(errs.len(), 1);
751        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
752    }
753
754    // List element error has bracket path.
755    #[test]
756    fn list_element_error_has_bracket_path() {
757        let errs = validate_str("(\n  tags: [String],\n)", "(tags: [\"ok\", 42])");
758        assert_eq!(errs[0].path, "tags[1]");
759    }
760
761    // ========================================================
762    // ExpectedStruct errors
763    // ========================================================
764
765    // Struct field rejects non-struct value.
766    #[test]
767    fn expected_struct_got_integer() {
768        let schema = "(\n  cost: (\n    generic: Integer,\n  ),\n)";
769        let errs = validate_str(schema, "(cost: 5)");
770        assert_eq!(errs.len(), 1);
771        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedStruct { .. }));
772    }
773
774    // ========================================================
775    // Nested validation
776    // ========================================================
777
778    // Type mismatch in nested struct has correct path.
779    #[test]
780    fn nested_struct_type_mismatch_path() {
781        let schema = "(\n  cost: (\n    generic: Integer,\n  ),\n)";
782        let errs = validate_str(schema, "(cost: (generic: \"two\"))");
783        assert_eq!(errs.len(), 1);
784        assert_eq!(errs[0].path, "cost.generic");
785    }
786
787    // Missing field in nested struct has correct path.
788    #[test]
789    fn nested_struct_missing_field_path() {
790        let schema = "(\n  cost: (\n    generic: Integer,\n    sigil: Integer,\n  ),\n)";
791        let errs = validate_str(schema, "(cost: (generic: 1))");
792        assert_eq!(errs.len(), 1);
793        assert_eq!(errs[0].path, "cost.sigil");
794    }
795
796    // ========================================================
797    // Multiple errors collected
798    // ========================================================
799
800    // Multiple errors in one struct are all reported.
801    #[test]
802    fn multiple_errors_collected() {
803        let schema = "(\n  name: String,\n  age: Integer,\n  active: Bool,\n)";
804        let data = "(name: 42, age: \"five\", active: \"yes\")";
805        let errs = validate_str(schema, data);
806        assert_eq!(errs.len(), 3);
807    }
808
809    // Mixed error types are all collected.
810    #[test]
811    fn mixed_error_types_collected() {
812        let schema = "(\n  name: String,\n  age: Integer,\n)";
813        let data = "(name: \"hi\", age: \"five\", extra: true)";
814        let errs = validate_str(schema, data);
815        // age is TypeMismatch, extra is UnknownField
816        assert_eq!(errs.len(), 2);
817    }
818
819    // ========================================================
820    // Integration: card-like schema
821    // ========================================================
822
823    // Valid card data produces no errors.
824    #[test]
825    fn valid_card_data() {
826        let schema = r#"(
827            name: String,
828            card_types: [CardType],
829            legendary: Bool,
830            power: Option(Integer),
831            toughness: Option(Integer),
832            keywords: [String],
833        )
834        enum CardType { Creature, Trap, Artifact }"#;
835        let data = r#"(
836            name: "Ashborn Hound",
837            card_types: [Creature],
838            legendary: false,
839            power: Some(1),
840            toughness: Some(1),
841            keywords: [],
842        )"#;
843        let errs = validate_str(schema, data);
844        assert!(errs.is_empty());
845    }
846
847    // Card data with multiple errors reports all of them.
848    #[test]
849    fn card_data_multiple_errors() {
850        let schema = r#"(
851            name: String,
852            card_types: [CardType],
853            legendary: Bool,
854            power: Option(Integer),
855        )
856        enum CardType { Creature, Trap }"#;
857        let data = r#"(
858            name: 42,
859            card_types: [Pirates],
860            legendary: false,
861            power: Some("five"),
862        )"#;
863        let errs = validate_str(schema, data);
864        // name: TypeMismatch, card_types[0]: InvalidEnumVariant, power: TypeMismatch
865        assert_eq!(errs.len(), 3);
866    }
867
868    // ========================================================
869    // Type alias validation
870    // ========================================================
871
872    // Alias to a struct type validates correctly.
873    #[test]
874    fn alias_struct_valid() {
875        let schema = "(\n  cost: Cost,\n)\ntype Cost = (generic: Integer,)";
876        let data = "(cost: (generic: 5))";
877        let errs = validate_str(schema, data);
878        assert!(errs.is_empty());
879    }
880
881    // Alias to a struct type catches type mismatch inside.
882    #[test]
883    fn alias_struct_type_mismatch() {
884        let schema = "(\n  cost: Cost,\n)\ntype Cost = (generic: Integer,)";
885        let data = "(cost: (generic: \"five\"))";
886        let errs = validate_str(schema, data);
887        assert_eq!(errs.len(), 1);
888        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
889    }
890
891    // Alias to a primitive type validates correctly.
892    #[test]
893    fn alias_primitive_valid() {
894        let schema = "(\n  name: Name,\n)\ntype Name = String";
895        let data = "(name: \"hello\")";
896        let errs = validate_str(schema, data);
897        assert!(errs.is_empty());
898    }
899
900    // Alias to a primitive type catches mismatch.
901    #[test]
902    fn alias_primitive_mismatch() {
903        let schema = "(\n  name: Name,\n)\ntype Name = String";
904        let data = "(name: 42)";
905        let errs = validate_str(schema, data);
906        assert_eq!(errs.len(), 1);
907    }
908
909    // Alias used inside a list validates each element.
910    #[test]
911    fn alias_in_list_valid() {
912        let schema = "(\n  costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
913        let data = "(costs: [(generic: 1), (generic: 2)])";
914        let errs = validate_str(schema, data);
915        assert!(errs.is_empty());
916    }
917
918    // Alias used inside a list catches element errors.
919    #[test]
920    fn alias_in_list_element_error() {
921        let schema = "(\n  costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
922        let data = "(costs: [(generic: 1), (generic: \"two\")])";
923        let errs = validate_str(schema, data);
924        assert_eq!(errs.len(), 1);
925        assert_eq!(errs[0].path, "costs[1].generic");
926    }
927
928    // ========================================================
929    // Map validation
930    // ========================================================
931
932    // Valid map with string keys and integer values.
933    #[test]
934    fn map_valid() {
935        let schema = "(\n  attrs: {String: Integer},\n)";
936        let data = "(attrs: {\"str\": 5, \"dex\": 3})";
937        let errs = validate_str(schema, data);
938        assert!(errs.is_empty());
939    }
940
941    // Empty map is always valid.
942    #[test]
943    fn map_empty_valid() {
944        let schema = "(\n  attrs: {String: Integer},\n)";
945        let data = "(attrs: {})";
946        let errs = validate_str(schema, data);
947        assert!(errs.is_empty());
948    }
949
950    // Non-map value where map expected.
951    #[test]
952    fn map_expected_got_string() {
953        let schema = "(\n  attrs: {String: Integer},\n)";
954        let data = "(attrs: \"not a map\")";
955        let errs = validate_str(schema, data);
956        assert_eq!(errs.len(), 1);
957        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedMap { .. }));
958    }
959
960    // Map value with wrong type.
961    #[test]
962    fn map_wrong_value_type() {
963        let schema = "(\n  attrs: {String: Integer},\n)";
964        let data = "(attrs: {\"str\": \"five\"})";
965        let errs = validate_str(schema, data);
966        assert_eq!(errs.len(), 1);
967        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
968    }
969
970    // Map key with wrong type.
971    #[test]
972    fn map_wrong_key_type() {
973        let schema = "(\n  attrs: {String: Integer},\n)";
974        let data = "(attrs: {42: 5})";
975        let errs = validate_str(schema, data);
976        assert_eq!(errs.len(), 1);
977        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
978    }
979
980    // ========================================================
981    // Tuple validation
982    // ========================================================
983
984    // Valid tuple.
985    #[test]
986    fn tuple_valid() {
987        let schema = "(\n  pos: (Float, Float),\n)";
988        let data = "(pos: (1.0, 2.5))";
989        let errs = validate_str(schema, data);
990        assert!(errs.is_empty());
991    }
992
993    // Non-tuple value where tuple expected.
994    #[test]
995    fn tuple_expected_got_string() {
996        let schema = "(\n  pos: (Float, Float),\n)";
997        let data = "(pos: \"not a tuple\")";
998        let errs = validate_str(schema, data);
999        assert_eq!(errs.len(), 1);
1000        assert!(matches!(&errs[0].kind, ErrorKind::ExpectedTuple { .. }));
1001    }
1002
1003    // Tuple with wrong element count.
1004    #[test]
1005    fn tuple_wrong_length() {
1006        let schema = "(\n  pos: (Float, Float),\n)";
1007        let data = "(pos: (1.0, 2.5, 3.0))";
1008        let errs = validate_str(schema, data);
1009        assert_eq!(errs.len(), 1);
1010        assert!(matches!(&errs[0].kind, ErrorKind::TupleLengthMismatch { expected: 2, found: 3 }));
1011    }
1012
1013    // Tuple with wrong element type.
1014    #[test]
1015    fn tuple_wrong_element_type() {
1016        let schema = "(\n  pos: (Float, Float),\n)";
1017        let data = "(pos: (1.0, \"bad\"))";
1018        let errs = validate_str(schema, data);
1019        assert_eq!(errs.len(), 1);
1020        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
1021    }
1022
1023    // Tuple element error has correct path.
1024    #[test]
1025    fn tuple_element_error_path() {
1026        let schema = "(\n  pos: (Float, Float),\n)";
1027        let data = "(pos: (1.0, \"bad\"))";
1028        let errs = validate_str(schema, data);
1029        assert_eq!(errs[0].path, "pos.1");
1030    }
1031
1032    // ========================================================
1033    // Enum variant with data — validation
1034    // ========================================================
1035
1036    // Valid data variant.
1037    #[test]
1038    fn enum_data_variant_valid() {
1039        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1040        let data = "(effect: Damage(5))";
1041        let errs = validate_str(schema, data);
1042        assert!(errs.is_empty());
1043    }
1044
1045    // Valid unit variant alongside data variants.
1046    #[test]
1047    fn enum_unit_variant_valid() {
1048        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1049        let data = "(effect: Draw)";
1050        let errs = validate_str(schema, data);
1051        assert!(errs.is_empty());
1052    }
1053
1054    // Data variant with wrong inner type.
1055    #[test]
1056    fn enum_data_variant_wrong_type() {
1057        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1058        let data = "(effect: Damage(\"five\"))";
1059        let errs = validate_str(schema, data);
1060        assert_eq!(errs.len(), 1);
1061        assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
1062    }
1063
1064    // Unknown variant name with data.
1065    #[test]
1066    fn enum_data_variant_unknown() {
1067        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1068        let data = "(effect: Explode(10))";
1069        let errs = validate_str(schema, data);
1070        assert_eq!(errs.len(), 1);
1071        assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { .. }));
1072    }
1073
1074    // Bare identifier for a variant that expects data.
1075    #[test]
1076    fn enum_missing_data() {
1077        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1078        let data = "(effect: Damage)";
1079        let errs = validate_str(schema, data);
1080        assert_eq!(errs.len(), 1);
1081        assert!(matches!(&errs[0].kind, ErrorKind::InvalidVariantData { .. }));
1082    }
1083
1084    // Data provided for a unit variant.
1085    #[test]
1086    fn enum_unexpected_data() {
1087        let schema = "(\n  effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
1088        let data = "(effect: Draw(5))";
1089        let errs = validate_str(schema, data);
1090        assert_eq!(errs.len(), 1);
1091        assert!(matches!(&errs[0].kind, ErrorKind::InvalidVariantData { .. }));
1092    }
1093}