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