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