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