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