use std::collections::{HashMap, HashSet};
use crate::error::{ErrorKind, ValidationError, ValidationResult, Warning, WarningKind};
#[must_use]
pub fn validate(schema: &Schema, value: &Spanned<RonValue>) -> ValidationResult {
let mut errors = Vec::new();
let mut warnings = Vec::new();
validate_struct(&schema.root, value, "", &mut errors, &mut warnings, &schema.enums, &schema.aliases);
ValidationResult { errors, warnings }
}
use crate::ron::RonValue;
use crate::schema::{EnumDef, Schema, SchemaType, StructDef};
use crate::span::{Span, Spanned};
fn describe(value: &RonValue) -> String {
match value {
RonValue::String(s) => {
if s.len() > 20 {
format!("String(\"{}...\")", &s[..20])
} else {
format!("String(\"{s}\")")
}
}
RonValue::Integer(n) => format!("Integer({n})"),
RonValue::Float(f) => format!("Float({f})"),
RonValue::Bool(b) => format!("Bool({b})"),
RonValue::Option(_) => "Option".to_string(),
RonValue::Identifier(s) => format!("Identifier({s})"),
RonValue::EnumVariant(name, _) => format!("{name}(...)"),
RonValue::List(_) => "List".to_string(),
RonValue::Map(_) => "Map".to_string(),
RonValue::Tuple(_) => "Tuple".to_string(),
RonValue::Struct(_) => "Struct".to_string(),
}
}
fn build_path(parent: &str, field: &str) -> String {
if parent.is_empty() {
field.to_string()
} else {
format!("{parent}.{field}")
}
}
#[allow(clippy::too_many_lines)]
fn validate_type(
expected: &SchemaType,
actual: &Spanned<RonValue>,
path: &str,
errors: &mut Vec<ValidationError>,
warnings: &mut Vec<Warning>,
enums: &HashMap<String, EnumDef>,
aliases: &HashMap<String, Spanned<SchemaType>>,
) {
match expected {
SchemaType::String => {
if !matches!(actual.value, RonValue::String(_)) {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::TypeMismatch {
expected: "String".to_string(),
found: describe(&actual.value),
},
});
}
}
SchemaType::Integer => {
if !matches!(actual.value, RonValue::Integer(_)) {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::TypeMismatch {
expected: "Integer".to_string(),
found: describe(&actual.value),
},
});
}
}
SchemaType::Float => {
if !matches!(actual.value, RonValue::Float(_)) {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::TypeMismatch {
expected: "Float".to_string(),
found: describe(&actual.value),
},
});
}
}
SchemaType::Bool => {
if !matches!(actual.value, RonValue::Bool(_)) {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::TypeMismatch {
expected: "Bool".to_string(),
found: describe(&actual.value),
},
});
}
}
SchemaType::Option(inner_type) => match &actual.value {
RonValue::Option(None) => {}
RonValue::Option(Some(inner_value)) => {
validate_type(inner_type, inner_value, path, errors, warnings, enums, aliases);
}
_ => {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::ExpectedOption {
found: describe(&actual.value),
},
});
}
},
SchemaType::List(element_type) => {
if let RonValue::List(elements) = &actual.value {
for (index, element) in elements.iter().enumerate() {
let element_path = format!("{path}[{index}]");
validate_type(element_type, element, &element_path, errors, warnings, enums, aliases);
}
} else {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::ExpectedList {
found: describe(&actual.value),
},
});
}
}
SchemaType::EnumRef(enum_name) => {
let enum_def = &enums[enum_name];
let variant_names: Vec<String> = enum_def.variants.keys().cloned().collect();
match &actual.value {
RonValue::Identifier(variant) => {
match enum_def.variants.get(variant) {
None => {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::InvalidEnumVariant {
enum_name: enum_name.clone(),
variant: variant.clone(),
valid: variant_names,
},
});
}
Some(Some(_expected_data_type)) => {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::InvalidVariantData {
enum_name: enum_name.clone(),
variant: variant.clone(),
expected: "data".to_string(),
found: "unit variant".to_string(),
},
});
}
Some(None) => {} }
}
RonValue::EnumVariant(variant, data) => {
match enum_def.variants.get(variant) {
None => {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::InvalidEnumVariant {
enum_name: enum_name.clone(),
variant: variant.clone(),
valid: variant_names,
},
});
}
Some(None) => {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::InvalidVariantData {
enum_name: enum_name.clone(),
variant: variant.clone(),
expected: "unit variant".to_string(),
found: describe(&data.value),
},
});
}
Some(Some(expected_data_type)) => {
validate_type(expected_data_type, data, path, errors, warnings, enums, aliases);
}
}
}
_ => {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::TypeMismatch {
expected: enum_name.clone(),
found: describe(&actual.value),
},
});
}
}
}
SchemaType::Map(key_type, value_type) => {
if let RonValue::Map(entries) = &actual.value {
for (key, value) in entries {
let key_desc = describe(&key.value);
validate_type(key_type, key, path, errors, warnings, enums, aliases);
let entry_path = format!("{path}[{key_desc}]");
validate_type(value_type, value, &entry_path, errors, warnings, enums, aliases);
}
} else {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::ExpectedMap {
found: describe(&actual.value),
},
});
}
}
SchemaType::Tuple(element_types) => {
if let RonValue::Tuple(elements) = &actual.value {
if elements.len() == element_types.len() {
for (index, (expected_type, element)) in element_types.iter().zip(elements).enumerate() {
let element_path = format!("{path}.{index}");
validate_type(expected_type, element, &element_path, errors, warnings, enums, aliases);
}
} else {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::TupleLengthMismatch {
expected: element_types.len(),
found: elements.len(),
},
});
}
} else {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::ExpectedTuple {
found: describe(&actual.value),
},
});
}
}
SchemaType::AliasRef(alias_name) => {
if let Some(resolved) = aliases.get(alias_name) {
validate_type(&resolved.value, actual, path, errors, warnings, enums, aliases);
}
}
SchemaType::Struct(struct_def) => {
validate_struct(struct_def, actual, path, errors, warnings, enums, aliases);
}
}
}
fn validate_struct(
struct_def: &StructDef,
actual: &Spanned<RonValue>,
path: &str,
errors: &mut Vec<ValidationError>,
warnings: &mut Vec<Warning>,
enums: &HashMap<String, EnumDef>,
aliases: &HashMap<String, Spanned<SchemaType>>,
) {
let RonValue::Struct(data_struct) = &actual.value else {
errors.push(ValidationError {
path: path.to_string(),
span: actual.span,
kind: ErrorKind::ExpectedStruct {
found: describe(&actual.value),
},
});
return;
};
let data_map: HashMap<&str, &Spanned<RonValue>> = data_struct
.fields
.iter()
.map(|(name, value)| (name.value.as_str(), value))
.collect();
let schema_names: HashSet<&str> = struct_def
.fields
.iter()
.map(|f| f.name.value.as_str())
.collect();
for field_def in &struct_def.fields {
if !data_map.contains_key(field_def.name.value.as_str()) && field_def.default.is_none() {
errors.push(ValidationError {
path: build_path(path, &field_def.name.value),
span: data_struct.close_span,
kind: ErrorKind::MissingField {
field_name: field_def.name.value.clone(),
},
});
}
}
for (name, _value) in &data_struct.fields {
if !schema_names.contains(name.value.as_str()) {
errors.push(ValidationError {
path: build_path(path, &name.value),
span: name.span,
kind: ErrorKind::UnknownField {
field_name: name.value.clone(),
},
});
}
}
for field_def in &struct_def.fields {
if let Some(data_value) = data_map.get(field_def.name.value.as_str()) {
let field_path = build_path(path, &field_def.name.value);
validate_type(&field_def.type_.value, data_value, &field_path, errors, warnings, enums, aliases);
}
}
let schema_order: Vec<&str> = struct_def.fields.iter()
.map(|f| f.name.value.as_str())
.collect();
let data_fields: Vec<(&str, Span)> = data_struct.fields.iter()
.map(|(name, _)| (name.value.as_str(), name.span))
.collect();
let mut last_schema_index = 0;
for (data_name, data_span) in &data_fields {
if let Some(schema_index) = schema_order.iter().position(|&s| s == *data_name) {
if schema_index < last_schema_index {
let expected_after = schema_order[last_schema_index];
warnings.push(Warning {
path: build_path(path, data_name),
span: *data_span,
kind: WarningKind::FieldOrderMismatch {
field_name: data_name.to_string(),
expected_after: expected_after.to_string(),
},
});
} else {
last_schema_index = schema_index;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::parser::parse_schema;
use crate::ron::parser::parse_ron;
fn validate_str(schema_src: &str, data_src: &str) -> Vec<ValidationError> {
validate_full(schema_src, data_src).errors
}
fn validate_full(schema_src: &str, data_src: &str) -> ValidationResult {
let schema = parse_schema(schema_src).expect("test schema should parse");
let data = parse_ron(data_src).expect("test data should parse");
validate(&schema, &data)
}
#[test]
fn describe_string() {
assert_eq!(describe(&RonValue::String("hi".to_string())), "String(\"hi\")");
}
#[test]
fn describe_string_truncated() {
let long = "a".repeat(30);
let desc = describe(&RonValue::String(long));
assert!(desc.contains("..."));
}
#[test]
fn describe_integer() {
assert_eq!(describe(&RonValue::Integer(42)), "Integer(42)");
}
#[test]
fn describe_float() {
assert_eq!(describe(&RonValue::Float(3.14)), "Float(3.14)");
}
#[test]
fn describe_bool() {
assert_eq!(describe(&RonValue::Bool(true)), "Bool(true)");
}
#[test]
fn describe_identifier() {
assert_eq!(describe(&RonValue::Identifier("Creature".to_string())), "Identifier(Creature)");
}
#[test]
fn build_path_root() {
assert_eq!(build_path("", "name"), "name");
}
#[test]
fn build_path_nested() {
assert_eq!(build_path("cost", "generic"), "cost.generic");
}
#[test]
fn build_path_deep() {
assert_eq!(build_path("a.b", "c"), "a.b.c");
}
#[test]
fn valid_single_string_field() {
let errs = validate_str("(\n name: String,\n)", "(name: \"hello\")");
assert!(errs.is_empty());
}
#[test]
fn valid_all_primitives() {
let schema = "(\n s: String,\n i: Integer,\n f: Float,\n b: Bool,\n)";
let data = "(s: \"hi\", i: 42, f: 3.14, b: true)";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn valid_option_none() {
let errs = validate_str("(\n power: Option(Integer),\n)", "(power: None)");
assert!(errs.is_empty());
}
#[test]
fn valid_option_some() {
let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(5))");
assert!(errs.is_empty());
}
#[test]
fn valid_list_empty() {
let errs = validate_str("(\n tags: [String],\n)", "(tags: [])");
assert!(errs.is_empty());
}
#[test]
fn valid_list_populated() {
let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"a\", \"b\"])");
assert!(errs.is_empty());
}
#[test]
fn valid_enum_variant() {
let schema = "(\n kind: Kind,\n)\nenum Kind { A, B, C }";
let data = "(kind: B)";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn valid_enum_list() {
let schema = "(\n types: [CardType],\n)\nenum CardType { Creature, Trap }";
let data = "(types: [Creature, Trap])";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn valid_nested_struct() {
let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
let data = "(cost: (generic: 2, sigil: 1))";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn type_mismatch_string_got_integer() {
let errs = validate_str("(\n name: String,\n)", "(name: 42)");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
}
#[test]
fn type_mismatch_integer_got_string() {
let errs = validate_str("(\n age: Integer,\n)", "(age: \"five\")");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
}
#[test]
fn type_mismatch_float_got_integer() {
let errs = validate_str("(\n rate: Float,\n)", "(rate: 5)");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
}
#[test]
fn type_mismatch_bool_got_string() {
let errs = validate_str("(\n flag: Bool,\n)", "(flag: \"yes\")");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Bool"));
}
#[test]
fn type_mismatch_has_correct_path() {
let errs = validate_str("(\n name: String,\n)", "(name: 42)");
assert_eq!(errs[0].path, "name");
}
#[test]
fn missing_field_detected() {
let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "age"));
}
#[test]
fn missing_field_has_correct_path() {
let errs = validate_str("(\n name: String,\n age: Integer,\n)", "(name: \"hi\")");
assert_eq!(errs[0].path, "age");
}
#[test]
fn missing_field_span_points_to_close_paren() {
let data = "(name: \"hi\")";
let errs = validate_str("(\n name: String,\n age: Integer,\n)", data);
assert_eq!(errs[0].span.start.offset, data.len() - 1);
}
#[test]
fn missing_fields_all_reported() {
let errs = validate_str("(\n a: String,\n b: Integer,\n c: Bool,\n)", "()");
assert_eq!(errs.len(), 3);
}
#[test]
fn field_order_correct_no_warning() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(a: \"hi\", b: 1)");
assert!(result.warnings.is_empty());
}
#[test]
fn field_order_swapped_produces_warning() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
assert_eq!(result.warnings.len(), 1);
}
#[test]
fn field_order_warning_has_correct_kind() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
assert!(matches!(&result.warnings[0].kind, WarningKind::FieldOrderMismatch { .. }));
}
#[test]
fn field_order_warning_identifies_field() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
if let WarningKind::FieldOrderMismatch { field_name, .. } = &result.warnings[0].kind {
assert_eq!(field_name, "a");
} else {
panic!("expected FieldOrderMismatch");
}
}
#[test]
fn field_order_warning_identifies_expected_after() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
if let WarningKind::FieldOrderMismatch { expected_after, .. } = &result.warnings[0].kind {
assert_eq!(expected_after, "b");
} else {
panic!("expected FieldOrderMismatch");
}
}
#[test]
fn field_order_warning_has_correct_path() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
assert_eq!(result.warnings[0].path, "a");
}
#[test]
fn field_order_warning_has_span() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
assert!(result.warnings[0].span.start.line > 0);
}
#[test]
fn field_order_three_fields_correct() {
let result = validate_full(
"(\n a: String,\n b: Integer,\n c: Bool,\n)",
"(a: \"hi\", b: 1, c: true)",
);
assert!(result.warnings.is_empty());
}
#[test]
fn field_order_middle_field_swapped() {
let result = validate_full(
"(\n a: String,\n b: Integer,\n c: Bool,\n)",
"(a: \"hi\", c: true, b: 1)",
);
assert_eq!(result.warnings.len(), 1);
if let WarningKind::FieldOrderMismatch { field_name, .. } = &result.warnings[0].kind {
assert_eq!(field_name, "b");
} else {
panic!("expected FieldOrderMismatch");
}
}
#[test]
fn field_order_warning_no_errors() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, a: \"hi\")");
assert!(result.errors.is_empty());
}
#[test]
fn field_order_with_unknown_field() {
let result = validate_full("(\n a: String,\n b: Integer,\n)", "(b: 1, x: true, a: \"hi\")");
assert_eq!(result.warnings.len(), 1);
}
#[test]
fn default_field_not_required() {
let errs = validate_str("(\n name: String,\n label: String = \"none\",\n)", "(name: \"hi\")");
assert!(errs.is_empty());
}
#[test]
fn default_field_still_validates_type() {
let errs = validate_str("(\n name: String,\n label: String = \"none\",\n)", "(name: \"hi\", label: 42)");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
}
#[test]
fn default_field_accepts_correct_type() {
let errs = validate_str("(\n name: String,\n label: String = \"none\",\n)", "(name: \"hi\", label: \"custom\")");
assert!(errs.is_empty());
}
#[test]
fn non_default_field_still_required() {
let errs = validate_str("(\n name: String,\n label: String = \"none\",\n)", "(label: \"hi\")");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::MissingField { field_name } if field_name == "name"));
}
#[test]
fn multiple_default_fields_all_absent() {
let errs = validate_str(
"(\n name: String,\n a: Integer = 0,\n b: Bool = false,\n c: String = \"x\",\n)",
"(name: \"hi\")",
);
assert!(errs.is_empty());
}
#[test]
fn default_option_field_not_required() {
let errs = validate_str("(\n name: String,\n tag: Option(String) = None,\n)", "(name: \"hi\")");
assert!(errs.is_empty());
}
#[test]
fn default_list_field_not_required() {
let errs = validate_str("(\n name: String,\n tags: [String] = [],\n)", "(name: \"hi\")");
assert!(errs.is_empty());
}
#[test]
fn unknown_field_detected() {
let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", colour: \"red\")");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::UnknownField { field_name } if field_name == "colour"));
}
#[test]
fn unknown_field_has_correct_path() {
let errs = validate_str("(\n name: String,\n)", "(name: \"hi\", extra: 5)");
assert_eq!(errs[0].path, "extra");
}
#[test]
fn invalid_enum_variant() {
let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
let errs = validate_str(schema, "(kind: C)");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { variant, .. } if variant == "C"));
}
#[test]
fn enum_rejects_string() {
let schema = "(\n kind: Kind,\n)\nenum Kind { A, B }";
let errs = validate_str(schema, "(kind: \"A\")");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { .. }));
}
#[test]
fn expected_option_got_bare_value() {
let errs = validate_str("(\n power: Option(Integer),\n)", "(power: 5)");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::ExpectedOption { .. }));
}
#[test]
fn option_some_wrong_inner_type() {
let errs = validate_str("(\n power: Option(Integer),\n)", "(power: Some(\"five\"))");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
}
#[test]
fn expected_list_got_string() {
let errs = validate_str("(\n tags: [String],\n)", "(tags: \"hi\")");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::ExpectedList { .. }));
}
#[test]
fn list_element_wrong_type() {
let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
}
#[test]
fn list_element_error_has_bracket_path() {
let errs = validate_str("(\n tags: [String],\n)", "(tags: [\"ok\", 42])");
assert_eq!(errs[0].path, "tags[1]");
}
#[test]
fn expected_struct_got_integer() {
let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
let errs = validate_str(schema, "(cost: 5)");
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::ExpectedStruct { .. }));
}
#[test]
fn nested_struct_type_mismatch_path() {
let schema = "(\n cost: (\n generic: Integer,\n ),\n)";
let errs = validate_str(schema, "(cost: (generic: \"two\"))");
assert_eq!(errs.len(), 1);
assert_eq!(errs[0].path, "cost.generic");
}
#[test]
fn nested_struct_missing_field_path() {
let schema = "(\n cost: (\n generic: Integer,\n sigil: Integer,\n ),\n)";
let errs = validate_str(schema, "(cost: (generic: 1))");
assert_eq!(errs.len(), 1);
assert_eq!(errs[0].path, "cost.sigil");
}
#[test]
fn multiple_errors_collected() {
let schema = "(\n name: String,\n age: Integer,\n active: Bool,\n)";
let data = "(name: 42, age: \"five\", active: \"yes\")";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 3);
}
#[test]
fn mixed_error_types_collected() {
let schema = "(\n name: String,\n age: Integer,\n)";
let data = "(name: \"hi\", age: \"five\", extra: true)";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 2);
}
#[test]
fn valid_card_data() {
let schema = r#"(
name: String,
card_types: [CardType],
legendary: Bool,
power: Option(Integer),
toughness: Option(Integer),
keywords: [String],
)
enum CardType { Creature, Trap, Artifact }"#;
let data = r#"(
name: "Ashborn Hound",
card_types: [Creature],
legendary: false,
power: Some(1),
toughness: Some(1),
keywords: [],
)"#;
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn card_data_multiple_errors() {
let schema = r#"(
name: String,
card_types: [CardType],
legendary: Bool,
power: Option(Integer),
)
enum CardType { Creature, Trap }"#;
let data = r#"(
name: 42,
card_types: [Pirates],
legendary: false,
power: Some("five"),
)"#;
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 3);
}
#[test]
fn alias_struct_valid() {
let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
let data = "(cost: (generic: 5))";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn alias_struct_type_mismatch() {
let schema = "(\n cost: Cost,\n)\ntype Cost = (generic: Integer,)";
let data = "(cost: (generic: \"five\"))";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
}
#[test]
fn alias_primitive_valid() {
let schema = "(\n name: Name,\n)\ntype Name = String";
let data = "(name: \"hello\")";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn alias_primitive_mismatch() {
let schema = "(\n name: Name,\n)\ntype Name = String";
let data = "(name: 42)";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
}
#[test]
fn alias_in_list_valid() {
let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
let data = "(costs: [(generic: 1), (generic: 2)])";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn alias_in_list_element_error() {
let schema = "(\n costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
let data = "(costs: [(generic: 1), (generic: \"two\")])";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert_eq!(errs[0].path, "costs[1].generic");
}
#[test]
fn map_valid() {
let schema = "(\n attrs: {String: Integer},\n)";
let data = "(attrs: {\"str\": 5, \"dex\": 3})";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn map_empty_valid() {
let schema = "(\n attrs: {String: Integer},\n)";
let data = "(attrs: {})";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn map_expected_got_string() {
let schema = "(\n attrs: {String: Integer},\n)";
let data = "(attrs: \"not a map\")";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::ExpectedMap { .. }));
}
#[test]
fn map_wrong_value_type() {
let schema = "(\n attrs: {String: Integer},\n)";
let data = "(attrs: {\"str\": \"five\"})";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
}
#[test]
fn map_wrong_key_type() {
let schema = "(\n attrs: {String: Integer},\n)";
let data = "(attrs: {42: 5})";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "String"));
}
#[test]
fn tuple_valid() {
let schema = "(\n pos: (Float, Float),\n)";
let data = "(pos: (1.0, 2.5))";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn tuple_expected_got_string() {
let schema = "(\n pos: (Float, Float),\n)";
let data = "(pos: \"not a tuple\")";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::ExpectedTuple { .. }));
}
#[test]
fn tuple_wrong_length() {
let schema = "(\n pos: (Float, Float),\n)";
let data = "(pos: (1.0, 2.5, 3.0))";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TupleLengthMismatch { expected: 2, found: 3 }));
}
#[test]
fn tuple_wrong_element_type() {
let schema = "(\n pos: (Float, Float),\n)";
let data = "(pos: (1.0, \"bad\"))";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Float"));
}
#[test]
fn tuple_element_error_path() {
let schema = "(\n pos: (Float, Float),\n)";
let data = "(pos: (1.0, \"bad\"))";
let errs = validate_str(schema, data);
assert_eq!(errs[0].path, "pos.1");
}
#[test]
fn enum_data_variant_valid() {
let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
let data = "(effect: Damage(5))";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn enum_unit_variant_valid() {
let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
let data = "(effect: Draw)";
let errs = validate_str(schema, data);
assert!(errs.is_empty());
}
#[test]
fn enum_data_variant_wrong_type() {
let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
let data = "(effect: Damage(\"five\"))";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::TypeMismatch { expected, .. } if expected == "Integer"));
}
#[test]
fn enum_data_variant_unknown() {
let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
let data = "(effect: Explode(10))";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::InvalidEnumVariant { .. }));
}
#[test]
fn enum_missing_data() {
let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
let data = "(effect: Damage)";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::InvalidVariantData { .. }));
}
#[test]
fn enum_unexpected_data() {
let schema = "(\n effect: Effect,\n)\nenum Effect { Damage(Integer), Draw }";
let data = "(effect: Draw(5))";
let errs = validate_str(schema, data);
assert_eq!(errs.len(), 1);
assert!(matches!(&errs[0].kind, ErrorKind::InvalidVariantData { .. }));
}
}