#[cfg(feature = "derive")]
use std::path::{Path, PathBuf};
use ahash::{HashSet, HashSetExt};
use super::{
Field, Schema, TypeKind, Variant, VariantKind,
error::{Result, SchemaError, ValidationError, ValidationResult},
};
use crate::{
Value,
ast::{Expr, StructBody},
error::Span,
};
pub trait SchemaResolver {
fn resolve(&self, type_path: &str) -> Option<Schema>;
}
pub struct AcceptAllResolver;
impl SchemaResolver for AcceptAllResolver {
fn resolve(&self, _type_path: &str) -> Option<Schema> {
None
}
}
#[cfg(feature = "derive")]
pub struct StorageResolver {
search_dir: Option<PathBuf>,
}
#[cfg(feature = "derive")]
impl StorageResolver {
#[must_use]
pub fn new() -> Self {
Self { search_dir: None }
}
#[must_use]
pub fn with_search_dir(dir: impl Into<PathBuf>) -> Self {
Self {
search_dir: Some(dir.into()),
}
}
#[must_use]
pub fn search_dir(&self) -> Option<&Path> {
self.search_dir.as_deref()
}
}
#[cfg(feature = "derive")]
impl Default for StorageResolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "derive")]
impl SchemaResolver for StorageResolver {
fn resolve(&self, type_path: &str) -> Option<Schema> {
if let Some(ref dir) = self.search_dir {
super::storage::find_schema_in(type_path, dir).ok()
} else {
super::storage::find_schema(type_path).ok()
}
}
}
struct ValidationContext<'a, R: SchemaResolver> {
resolver: &'a R,
visiting: HashSet<String>,
}
impl<'a, R: SchemaResolver> ValidationContext<'a, R> {
fn new(resolver: &'a R) -> Self {
Self {
resolver,
visiting: HashSet::new(),
}
}
fn is_visiting(&self, type_path: &str) -> bool {
self.visiting.contains(type_path)
}
fn start_visiting(&mut self, type_path: &str) {
self.visiting.insert(type_path.to_string());
}
fn stop_visiting(&mut self, type_path: &str) {
self.visiting.remove(type_path);
}
}
pub fn validate(value: &Value, schema: &Schema) -> Result<()> {
validate_with_resolver(value, schema, &AcceptAllResolver)
}
pub fn validate_type(value: &Value, kind: &TypeKind) -> Result<()> {
validate_type_with_resolver(value, kind, &AcceptAllResolver)
}
pub fn validate_with_resolver<R: SchemaResolver>(
value: &Value,
schema: &Schema,
resolver: &R,
) -> Result<()> {
let mut ctx = ValidationContext::new(resolver);
validate_type_internal(value, &schema.kind, &mut ctx)
.map_err(|e| SchemaError::Validation(Box::new(e)))
}
pub fn validate_type_with_resolver<R: SchemaResolver>(
value: &Value,
kind: &TypeKind,
resolver: &R,
) -> Result<()> {
let mut ctx = ValidationContext::new(resolver);
validate_type_internal(value, kind, &mut ctx).map_err(|e| SchemaError::Validation(Box::new(e)))
}
pub fn validate_expr(expr: &Expr<'_>, schema: &Schema) -> Result<()> {
validate_expr_with_resolver(expr, schema, &AcceptAllResolver)
}
pub fn validate_expr_type(expr: &Expr<'_>, kind: &TypeKind) -> Result<()> {
validate_expr_type_with_resolver(expr, kind, &AcceptAllResolver)
}
pub fn validate_expr_with_resolver<R: SchemaResolver>(
expr: &Expr<'_>,
schema: &Schema,
resolver: &R,
) -> Result<()> {
let mut ctx = ValidationContext::new(resolver);
validate_expr_internal(expr, &schema.kind, &mut ctx)
.map_err(|e| SchemaError::Validation(Box::new(e)))
}
pub fn validate_expr_type_with_resolver<R: SchemaResolver>(
expr: &Expr<'_>,
kind: &TypeKind,
resolver: &R,
) -> Result<()> {
let mut ctx = ValidationContext::new(resolver);
validate_expr_internal(expr, kind, &mut ctx).map_err(|e| SchemaError::Validation(Box::new(e)))
}
pub fn validate_expr_collect_all<R: SchemaResolver>(
expr: &Expr<'_>,
schema: &Schema,
resolver: &R,
) -> Vec<ValidationError> {
let mut ctx = ValidationContext::new(resolver);
let mut errors = Vec::new();
validate_expr_collect_internal(expr, &schema.kind, &mut ctx, &mut errors);
errors
}
use crate::value::Number;
#[allow(clippy::result_large_err)]
fn validate_integer_value(number: &Number, kind: &TypeKind) -> ValidationResult<()> {
if matches!(number, Number::F32(_) | Number::F64(_)) {
return Err(ValidationError::type_mismatch(
kind_to_type_name(kind),
"float",
));
}
match kind {
TypeKind::I8 => validate_signed_range::<i8>(number, "i8"),
TypeKind::I16 => validate_signed_range::<i16>(number, "i16"),
TypeKind::I32 => validate_signed_range::<i32>(number, "i32"),
TypeKind::I64 => validate_signed_range::<i64>(number, "i64"),
#[cfg(feature = "integer128")]
TypeKind::I128 => validate_signed_range::<i128>(number, "i128"),
TypeKind::U8 => validate_unsigned_range::<u8>(number, "u8"),
TypeKind::U16 => validate_unsigned_range::<u16>(number, "u16"),
TypeKind::U32 => validate_unsigned_range::<u32>(number, "u32"),
TypeKind::U64 => validate_unsigned_range::<u64>(number, "u64"),
#[cfg(feature = "integer128")]
TypeKind::U128 => validate_unsigned_range::<u128>(number, "u128"),
_ => Ok(()), }
}
fn kind_to_type_name(kind: &TypeKind) -> &'static str {
match kind {
TypeKind::I8 => "i8",
TypeKind::I16 => "i16",
TypeKind::I32 => "i32",
TypeKind::I64 => "i64",
#[cfg(feature = "integer128")]
TypeKind::I128 => "i128",
TypeKind::U8 => "u8",
TypeKind::U16 => "u16",
TypeKind::U32 => "u32",
TypeKind::U64 => "u64",
#[cfg(feature = "integer128")]
TypeKind::U128 => "u128",
TypeKind::F32 => "f32",
TypeKind::F64 => "f64",
_ => "integer",
}
}
#[allow(clippy::result_large_err)]
fn validate_signed_range<T>(number: &Number, type_name: &'static str) -> ValidationResult<()>
where
T: TryFrom<i64> + TryFrom<i128>,
{
let value_i128 = number_to_i128(number);
if T::try_from(value_i128).is_err() {
return Err(ValidationError::integer_out_of_bounds(
value_i128.to_string(),
type_name,
));
}
Ok(())
}
#[allow(clippy::result_large_err)]
fn validate_unsigned_range<T>(number: &Number, type_name: &'static str) -> ValidationResult<()>
where
T: TryFrom<u64> + TryFrom<u128>,
{
if is_negative_number(number) {
return Err(ValidationError::type_mismatch(
type_name,
"negative integer",
));
}
let value_u128 = number_to_u128(number);
if T::try_from(value_u128).is_err() {
return Err(ValidationError::integer_out_of_bounds(
value_u128.to_string(),
type_name,
));
}
Ok(())
}
fn is_negative_number(number: &Number) -> bool {
match number {
Number::I8(v) => *v < 0,
Number::I16(v) => *v < 0,
Number::I32(v) => *v < 0,
Number::I64(v) => *v < 0,
#[cfg(feature = "integer128")]
Number::I128(v) => *v < 0,
Number::U8(_) | Number::U16(_) | Number::U32(_) | Number::U64(_) => false,
#[cfg(feature = "integer128")]
Number::U128(_) => false,
Number::F32(v) => v.get() < 0.0,
Number::F64(v) => v.get() < 0.0,
#[cfg(not(doc))]
Number::__NonExhaustive(never) => never.never(),
}
}
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
fn number_to_i128(number: &Number) -> i128 {
match number {
Number::I8(v) => i128::from(*v),
Number::I16(v) => i128::from(*v),
Number::I32(v) => i128::from(*v),
Number::I64(v) => i128::from(*v),
#[cfg(feature = "integer128")]
Number::I128(v) => *v,
Number::U8(v) => i128::from(*v),
Number::U16(v) => i128::from(*v),
Number::U32(v) => i128::from(*v),
Number::U64(v) => i128::from(*v),
#[cfg(feature = "integer128")]
Number::U128(v) => *v as i128,
Number::F32(v) => v.get() as i128,
Number::F64(v) => v.get() as i128,
#[cfg(not(doc))]
Number::__NonExhaustive(never) => never.never(),
}
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
fn number_to_u128(number: &Number) -> u128 {
match number {
Number::I8(v) => *v as u128,
Number::I16(v) => *v as u128,
Number::I32(v) => *v as u128,
Number::I64(v) => *v as u128,
#[cfg(feature = "integer128")]
Number::I128(v) => *v as u128,
Number::U8(v) => u128::from(*v),
Number::U16(v) => u128::from(*v),
Number::U32(v) => u128::from(*v),
Number::U64(v) => u128::from(*v),
#[cfg(feature = "integer128")]
Number::U128(v) => *v,
Number::F32(v) => v.get() as u128,
Number::F64(v) => v.get() as u128,
#[cfg(not(doc))]
Number::__NonExhaustive(never) => never.never(),
}
}
#[allow(clippy::result_large_err)]
fn validate_type_internal<R: SchemaResolver>(
value: &Value,
kind: &TypeKind,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
match kind {
TypeKind::Bool => match value {
Value::Bool(_) => Ok(()),
_ => Err(type_mismatch("Bool", value)),
},
TypeKind::I8
| TypeKind::I16
| TypeKind::I32
| TypeKind::I64
| TypeKind::I128
| TypeKind::U8
| TypeKind::U16
| TypeKind::U32
| TypeKind::U64
| TypeKind::U128 => match value {
Value::Number(n) => validate_integer_value(n, kind),
_ => Err(type_mismatch("integer", value)),
},
TypeKind::F32 | TypeKind::F64 => match value {
Value::Number(_) => Ok(()),
_ => Err(type_mismatch("float", value)),
},
TypeKind::Char => match value {
Value::Char(_) => Ok(()),
_ => Err(type_mismatch("Char", value)),
},
TypeKind::String => match value {
Value::String(_) => Ok(()),
_ => Err(type_mismatch("String", value)),
},
TypeKind::Unit => match value {
Value::Unit => Ok(()),
_ => Err(type_mismatch("Unit", value)),
},
TypeKind::Option(inner) => match value {
Value::Option(None) => Ok(()),
Value::Option(Some(v)) => validate_type_internal(v, inner, ctx),
_ => Err(type_mismatch("Option", value)),
},
TypeKind::List(inner) => match value {
Value::Seq(items) => {
for (i, item) in items.iter().enumerate() {
validate_type_internal(item, inner, ctx).map_err(|e| e.in_element(i))?;
}
Ok(())
}
_ => Err(type_mismatch("List", value)),
},
TypeKind::Map { key, value: val_ty } => match value {
Value::Map(map) => {
for (k, v) in map.iter() {
validate_type_internal(k, key, ctx).map_err(ValidationError::in_map_key)?;
validate_type_internal(v, val_ty, ctx)
.map_err(|e| e.in_map_value(format!("{k:?}")))?;
}
Ok(())
}
_ => Err(type_mismatch("Map", value)),
},
TypeKind::Tuple(types) => match value {
Value::Tuple(items) | Value::Seq(items) => {
if items.len() != types.len() {
return Err(ValidationError::length_mismatch(
types.len().to_string(),
items.len(),
));
}
for (i, (item, ty)) in items.iter().zip(types.iter()).enumerate() {
validate_type_internal(item, ty, ctx).map_err(|e| e.in_element(i))?;
}
Ok(())
}
_ => Err(type_mismatch("Tuple", value)),
},
TypeKind::Struct { fields } => validate_struct_internal(value, fields, ctx),
TypeKind::Enum { variants } => validate_enum_internal(value, variants, ctx),
TypeKind::TypeRef(type_path) => {
if ctx.is_visiting(type_path) {
return Ok(());
}
match ctx.resolver.resolve(type_path) {
Some(schema) => {
ctx.start_visiting(type_path);
let result = validate_type_internal(value, &schema.kind, ctx)
.map_err(|e| e.in_type_ref(type_path));
ctx.stop_visiting(type_path);
result
}
None => {
Ok(())
}
}
}
}
}
#[derive(Debug)]
enum FlattenedTarget {
Struct {
fields: Vec<Field>,
presence_based: bool,
},
MapValue(TypeKind),
}
fn resolve_flattened_kind<R: SchemaResolver>(
kind: &TypeKind,
ctx: &mut ValidationContext<R>,
) -> Option<(TypeKind, bool)> {
let mut presence_based = false;
let mut current = kind.clone();
let mut seen_refs = HashSet::new();
for _ in 0..8 {
match current {
TypeKind::Option(inner) => {
presence_based = true;
current = *inner;
}
TypeKind::TypeRef(path) => {
if !seen_refs.insert(path.clone()) {
return None;
}
match ctx.resolver.resolve(&path) {
Some(schema) => {
current = schema.kind;
}
None => return None,
}
}
_ => return Some((current, presence_based)),
}
}
Some((current, presence_based))
}
fn collect_flattened_targets<R: SchemaResolver>(
fields: &[Field],
ctx: &mut ValidationContext<R>,
) -> Vec<FlattenedTarget> {
let mut targets = Vec::new();
for field in fields {
if !field.flattened {
continue;
}
let Some((kind, mut presence_based)) = resolve_flattened_kind(&field.ty, ctx) else {
continue;
};
presence_based |= field.optional;
match kind {
TypeKind::Struct { fields } => {
targets.push(FlattenedTarget::Struct {
fields,
presence_based,
});
}
TypeKind::Map { key, value } => {
if matches!(*key, TypeKind::String) {
targets.push(FlattenedTarget::MapValue(*value));
}
}
_ => {}
}
}
targets
}
#[allow(clippy::result_large_err)]
fn validate_struct_fields_inner<'a, R: SchemaResolver>(
field_iter: impl Iterator<Item = (&'a str, &'a Value)>,
fields: &[Field],
flattened_targets: &[FlattenedTarget],
map_value_type: Option<&TypeKind>,
has_field: impl Fn(&str) -> bool,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
let explicit_fields: Vec<_> = fields.iter().filter(|f| !f.flattened).collect();
for (key_str, val) in field_iter {
let field = explicit_fields.iter().find(|f| f.name == key_str);
if let Some(field) = field {
validate_type_internal(val, &field.ty, ctx).map_err(|e| e.in_field(key_str))?;
}
let mut matched_flattened_struct = false;
for target in flattened_targets {
if let FlattenedTarget::Struct { fields, .. } = target
&& let Some(inner) = fields.iter().find(|f| f.name == key_str)
{
matched_flattened_struct = true;
validate_type_internal(val, &inner.ty, ctx).map_err(|e| e.in_field(key_str))?;
}
}
if field.is_none() && !matched_flattened_struct {
if let Some(map_value_type) = map_value_type {
validate_type_internal(val, map_value_type, ctx)
.map_err(|e| e.in_field(key_str))?;
} else {
return Err(ValidationError::unknown_field(
key_str.to_owned(),
&[] as &[&str],
));
}
}
}
for field in &explicit_fields {
if !field.optional && !has_field(&field.name) {
return Err(ValidationError::missing_field(field.name.clone()));
}
}
for target in flattened_targets {
let FlattenedTarget::Struct {
fields,
presence_based,
} = target
else {
continue;
};
if *presence_based && !fields.iter().any(|field| has_field(&field.name)) {
continue;
}
for field in fields {
if !field.optional && !has_field(&field.name) {
return Err(ValidationError::missing_field(field.name.clone()));
}
}
}
Ok(())
}
#[allow(clippy::result_large_err)]
fn validate_struct_internal<R: SchemaResolver>(
value: &Value,
fields: &[Field],
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
let flattened_targets = collect_flattened_targets(fields, ctx);
let map_value_type = flattened_targets.iter().find_map(|target| {
if let FlattenedTarget::MapValue(value_type) = target {
Some(value_type)
} else {
None
}
});
match value {
Value::Unit => validate_struct_fields_inner(
core::iter::empty(),
fields,
&flattened_targets,
map_value_type,
|_| false,
ctx,
),
Value::Struct(struct_fields) => validate_struct_fields_inner(
struct_fields.iter().map(|(k, v)| (k.as_str(), v)),
fields,
&flattened_targets,
map_value_type,
|name| struct_fields.iter().any(|(k, _)| k == name),
ctx,
),
Value::Named {
content: crate::value::NamedContent::Struct(struct_fields),
..
} => validate_struct_fields_inner(
struct_fields.iter().map(|(k, v)| (k.as_str(), v)),
fields,
&flattened_targets,
map_value_type,
|name| struct_fields.iter().any(|(k, _)| k == name),
ctx,
),
Value::Map(map) => {
let string_fields: core::result::Result<Vec<_>, _> = map
.iter()
.map(|(k, v)| match k {
Value::String(s) => Ok((s.as_str(), v)),
_ => Err(type_mismatch("String (field name)", k)),
})
.collect();
let string_fields = string_fields?;
validate_struct_fields_inner(
string_fields.iter().copied(),
fields,
&flattened_targets,
map_value_type,
|name| {
map.iter()
.any(|(k, _)| matches!(k, Value::String(s) if s == name))
},
ctx,
)
}
_ => Err(type_mismatch("Struct", value)),
}
}
#[allow(clippy::result_large_err)]
fn validate_enum_internal<R: SchemaResolver>(
value: &Value,
variants: &[Variant],
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
use crate::value::NamedContent;
enum VariantContent<'a> {
None,
Named(&'a NamedContent),
Value(&'a Value),
}
let (variant_name, content): (&str, VariantContent) = match value {
Value::String(s) => (s.as_str(), VariantContent::None),
Value::Named { name, content } => (name.as_str(), VariantContent::Named(content)),
Value::Map(map) if map.len() == 1 => {
let Some((k, v)) = map.iter().next() else {
return Err(type_mismatch("Enum", value));
};
match k {
Value::String(s) => (s.as_str(), VariantContent::Value(v)),
_ => return Err(type_mismatch("Enum variant name", k)),
}
}
_ => return Err(type_mismatch("Enum", value)),
};
let variant = variants
.iter()
.find(|v| v.name == variant_name)
.ok_or_else(|| ValidationError::unknown_variant(variant_name.to_owned(), &[] as &[&str]))?;
let validate_struct_fields = |fields: &[Field],
struct_fields: &[(String, Value)],
ctx: &mut ValidationContext<R>|
-> ValidationResult<()> {
for (key, val) in struct_fields {
let field = fields.iter().find(|f| f.name == *key).ok_or_else(|| {
ValidationError::unknown_field(key.clone(), &[] as &[&str]).in_variant(variant_name)
})?;
validate_type_internal(val, &field.ty, ctx)
.map_err(|e| e.in_field(key).in_variant(variant_name))?;
}
for field in fields {
if !field.optional && !struct_fields.iter().any(|(k, _)| k == &field.name) {
return Err(
ValidationError::missing_field(field.name.clone()).in_variant(variant_name)
);
}
}
Ok(())
};
let validate_tuple_elements = |types: &[TypeKind],
items: &[Value],
ctx: &mut ValidationContext<R>|
-> ValidationResult<()> {
if items.len() != types.len() {
return Err(
ValidationError::length_mismatch(types.len().to_string(), items.len())
.in_variant(variant_name),
);
}
for (i, (item, ty)) in items.iter().zip(types.iter()).enumerate() {
validate_type_internal(item, ty, ctx)
.map_err(|e| e.in_element(i).in_variant(variant_name))?;
}
Ok(())
};
match (&variant.kind, content) {
(
VariantKind::Unit,
VariantContent::None
| VariantContent::Named(NamedContent::Unit)
| VariantContent::Value(Value::Unit),
) => Ok(()),
(
VariantKind::Tuple(types),
VariantContent::Named(NamedContent::Tuple(items))
| VariantContent::Value(Value::Seq(items) | Value::Tuple(items)),
) => validate_tuple_elements(types, items, ctx),
(
VariantKind::Struct(fields),
VariantContent::Named(NamedContent::Struct(struct_fields)),
) => validate_struct_fields(fields, struct_fields, ctx),
(VariantKind::Struct(fields), VariantContent::Value(Value::Struct(struct_fields))) => {
validate_struct_fields(fields, struct_fields, ctx)
}
(VariantKind::Unit, _) => {
Err(ValidationError::type_mismatch("Unit", "non-unit content").in_variant(variant_name))
}
(_, VariantContent::None) => {
Err(ValidationError::type_mismatch("variant content", "none").in_variant(variant_name))
}
(_, _) => Err(ValidationError::type_mismatch(
format!("{variant_kind:?}", variant_kind = variant.kind),
"mismatched content",
)
.in_variant(variant_name)),
}
}
fn type_mismatch(expected: &str, value: &Value) -> ValidationError {
let actual = match value {
Value::Bool(_) => "Bool",
Value::Char(_) => "Char",
Value::Map(_) => "Map",
Value::Number(_) => "Number",
Value::Option(_) => "Option",
Value::String(_) => "String",
Value::Seq(_) => "Seq",
Value::Unit => "Unit",
Value::Bytes(_) => "Bytes",
Value::Tuple(_) => "Tuple",
Value::Struct(_) => "Struct",
Value::Named { .. } => "Named",
};
ValidationError::type_mismatch(expected, actual)
}
fn expr_type_name(expr: &Expr<'_>) -> &'static str {
match expr {
Expr::Unit(_) => "Unit",
Expr::Bool(_) => "Bool",
Expr::Char(_) => "Char",
Expr::Byte(_) => "Byte",
Expr::Number(_) => "Number",
Expr::String(_) => "String",
Expr::Bytes(_) => "Bytes",
Expr::Option(_) => "Option",
Expr::Seq(_) => "Seq",
Expr::Map(_) => "Map",
Expr::Tuple(_) => "Tuple",
Expr::AnonStruct(_) => "Struct",
Expr::Struct(_) => "Named",
Expr::Error(_) => "Error",
}
}
fn expr_type_mismatch(expected: &str, expr: &Expr<'_>) -> ValidationError {
ValidationError::with_span(
crate::error::ErrorKind::TypeMismatch {
expected: expected.to_string(),
found: expr_type_name(expr).to_string(),
},
*expr.span(),
)
}
fn expr_type_mismatch_found(expected: &str, found: &str, span: Span) -> ValidationError {
ValidationError::with_span(
crate::error::ErrorKind::TypeMismatch {
expected: expected.to_string(),
found: found.to_string(),
},
span,
)
}
fn integer_out_of_bounds_error(
value: &str,
target_type: &'static str,
span: Span,
) -> ValidationError {
ValidationError::with_span(
crate::error::ErrorKind::IntegerOutOfBounds {
value: value.to_string().into(),
target_type,
},
span,
)
}
use crate::ast::NumberKind;
#[allow(clippy::result_large_err)]
fn validate_integer_expr(
expr: &crate::ast::NumberExpr<'_>,
kind: &TypeKind,
) -> ValidationResult<()> {
if matches!(expr.kind, NumberKind::Float | NumberKind::SpecialFloat) {
return Err(expr_type_mismatch_found(
kind_to_type_name(kind),
"float",
expr.span,
));
}
let is_unsigned = matches!(
kind,
TypeKind::U8 | TypeKind::U16 | TypeKind::U32 | TypeKind::U64 | TypeKind::U128
);
if is_unsigned && expr.kind == NumberKind::NegativeInteger {
return Err(expr_type_mismatch_found(
kind_to_type_name(kind),
"negative integer",
expr.span,
));
}
let raw = expr.raw.as_ref();
validate_integer_range_from_raw(
raw,
kind,
expr.span,
expr.kind == NumberKind::NegativeInteger,
)
}
#[allow(clippy::result_large_err)]
fn validate_integer_range_from_raw(
raw: &str,
kind: &TypeKind,
span: Span,
is_negative: bool,
) -> ValidationResult<()> {
let (value_str, radix) = parse_integer_prefix(raw, is_negative);
let clean: String = value_str.chars().filter(|c| *c != '_').collect();
match kind {
TypeKind::I8 | TypeKind::I16 | TypeKind::I32 | TypeKind::I64 => {
let value = if radix == 10 {
clean.parse::<i128>()
} else {
i128::from_str_radix(&clean, radix)
};
let value = value
.map_err(|_| integer_out_of_bounds_error(raw, kind_to_type_name(kind), span))?;
let in_range = match kind {
TypeKind::I8 => i8::try_from(value).is_ok(),
TypeKind::I16 => i16::try_from(value).is_ok(),
TypeKind::I32 => i32::try_from(value).is_ok(),
TypeKind::I64 => i64::try_from(value).is_ok(),
_ => true,
};
if !in_range {
return Err(integer_out_of_bounds_error(
raw,
kind_to_type_name(kind),
span,
));
}
}
#[cfg(feature = "integer128")]
TypeKind::I128 => {
let value = if radix == 10 {
clean.parse::<i128>()
} else {
i128::from_str_radix(&clean, radix)
};
if value.is_err() {
return Err(integer_out_of_bounds_error(raw, "i128", span));
}
}
TypeKind::U8 | TypeKind::U16 | TypeKind::U32 | TypeKind::U64 => {
let value = if radix == 10 {
clean.parse::<u128>()
} else {
u128::from_str_radix(&clean, radix)
};
let value = value
.map_err(|_| integer_out_of_bounds_error(raw, kind_to_type_name(kind), span))?;
let in_range = match kind {
TypeKind::U8 => u8::try_from(value).is_ok(),
TypeKind::U16 => u16::try_from(value).is_ok(),
TypeKind::U32 => u32::try_from(value).is_ok(),
TypeKind::U64 => u64::try_from(value).is_ok(),
_ => true,
};
if !in_range {
return Err(integer_out_of_bounds_error(
raw,
kind_to_type_name(kind),
span,
));
}
}
#[cfg(feature = "integer128")]
TypeKind::U128 => {
let value = if radix == 10 {
clean.parse::<u128>()
} else {
u128::from_str_radix(&clean, radix)
};
if value.is_err() {
return Err(integer_out_of_bounds_error(raw, "u128", span));
}
}
_ => {} }
Ok(())
}
fn parse_integer_prefix(raw: &str, is_negative: bool) -> (&str, u32) {
let s = if is_negative && raw.starts_with('-') {
&raw[1..]
} else {
raw
};
if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
(rest, 16)
} else if let Some(rest) = s.strip_prefix("0b").or_else(|| s.strip_prefix("0B")) {
(rest, 2)
} else if let Some(rest) = s.strip_prefix("0o").or_else(|| s.strip_prefix("0O")) {
(rest, 8)
} else if is_negative {
(raw, 10)
} else {
(s, 10)
}
}
fn missing_field_error(field: &str, span: Span) -> ValidationError {
ValidationError::with_span(
crate::error::ErrorKind::MissingField {
field: field.to_string().into(),
outer: None,
},
span,
)
}
fn unknown_field_error(field: &str, span: Span) -> ValidationError {
ValidationError::with_span(
crate::error::ErrorKind::UnknownField {
field: field.to_string().into(),
expected: &[],
outer: None,
},
span,
)
}
fn unknown_variant_error(variant: &str, span: Span) -> ValidationError {
ValidationError::with_span(
crate::error::ErrorKind::UnknownVariant {
variant: variant.to_string().into(),
expected: &[],
outer: None,
},
span,
)
}
fn length_mismatch_error(expected: usize, found: usize, span: Span) -> ValidationError {
ValidationError::with_span(
crate::error::ErrorKind::LengthMismatch {
expected: expected.to_string(),
found,
context: None,
},
span,
)
}
#[allow(clippy::result_large_err)]
fn validate_expr_internal<R: SchemaResolver>(
expr: &Expr<'_>,
kind: &TypeKind,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
match kind {
TypeKind::Bool => match expr {
Expr::Bool(_) => Ok(()),
_ => Err(expr_type_mismatch("Bool", expr)),
},
TypeKind::I8
| TypeKind::I16
| TypeKind::I32
| TypeKind::I64
| TypeKind::I128
| TypeKind::U8
| TypeKind::U16
| TypeKind::U32
| TypeKind::U64
| TypeKind::U128 => match expr {
Expr::Number(n) => validate_integer_expr(n, kind),
_ => Err(expr_type_mismatch("integer", expr)),
},
TypeKind::F32 | TypeKind::F64 => match expr {
Expr::Number(_) => Ok(()),
_ => Err(expr_type_mismatch("float", expr)),
},
TypeKind::Char => match expr {
Expr::Char(_) => Ok(()),
_ => Err(expr_type_mismatch("Char", expr)),
},
TypeKind::String => match expr {
Expr::String(_) => Ok(()),
_ => Err(expr_type_mismatch("String", expr)),
},
TypeKind::Unit => match expr {
Expr::Unit(_) => Ok(()),
_ => Err(expr_type_mismatch("Unit", expr)),
},
TypeKind::Option(inner) => validate_option_expr(expr, inner, ctx),
TypeKind::List(inner) => validate_list_expr(expr, inner, ctx),
TypeKind::Map { key, value: val_ty } => validate_map_expr(expr, key, val_ty, ctx),
TypeKind::Tuple(types) => validate_tuple_expr(expr, types, ctx),
TypeKind::Struct { fields } => validate_struct_expr(expr, fields, ctx),
TypeKind::Enum { variants } => validate_enum_expr(expr, variants, ctx),
TypeKind::TypeRef(type_path) => validate_typeref_expr(expr, type_path, ctx),
}
}
#[allow(clippy::result_large_err)]
fn validate_option_expr<R: SchemaResolver>(
expr: &Expr<'_>,
inner: &TypeKind,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
match expr {
Expr::Option(opt) => match &opt.value {
None => Ok(()),
Some(val) => validate_expr_internal(&val.expr, inner, ctx),
},
Expr::Struct(s) if s.name.name == "None" && s.body.is_none() => Ok(()),
Expr::Struct(s) if s.name.name == "Some" => {
if let Some(StructBody::Tuple(body)) = &s.body
&& body.elements.len() == 1
{
return validate_expr_internal(&body.elements[0].expr, inner, ctx);
}
Err(expr_type_mismatch("Option", expr))
}
_ => Err(expr_type_mismatch("Option", expr)),
}
}
#[allow(clippy::result_large_err)]
fn validate_list_expr<R: SchemaResolver>(
expr: &Expr<'_>,
inner: &TypeKind,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
match expr {
Expr::Seq(seq) => {
for (i, item) in seq.items.iter().enumerate() {
validate_expr_internal(&item.expr, inner, ctx).map_err(|e| e.in_element(i))?;
}
Ok(())
}
_ => Err(expr_type_mismatch("List", expr)),
}
}
#[allow(clippy::result_large_err)]
fn validate_map_expr<R: SchemaResolver>(
expr: &Expr<'_>,
key_ty: &TypeKind,
val_ty: &TypeKind,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
match expr {
Expr::Map(map) => {
for entry in &map.entries {
validate_expr_internal(&entry.key, key_ty, ctx)
.map_err(ValidationError::in_map_key)?;
let key_str = format_expr_as_key(&entry.key);
validate_expr_internal(&entry.value, val_ty, ctx)
.map_err(|e| e.in_map_value(key_str))?;
}
Ok(())
}
_ => Err(expr_type_mismatch("Map", expr)),
}
}
fn format_expr_as_key(expr: &Expr<'_>) -> String {
match expr {
Expr::String(s) => s.value.clone(),
Expr::Number(n) => n.raw.to_string(),
Expr::Bool(b) => b.value.to_string(),
Expr::Char(c) => c.value.to_string(),
_ => "<complex>".to_string(),
}
}
#[allow(clippy::result_large_err)]
fn validate_tuple_expr<R: SchemaResolver>(
expr: &Expr<'_>,
types: &[TypeKind],
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
let items: &[_] = match expr {
Expr::Tuple(t) => &t.elements,
Expr::Seq(s) => {
if s.items.len() != types.len() {
return Err(length_mismatch_error(
types.len(),
s.items.len(),
*expr.span(),
));
}
for (i, (item, ty)) in s.items.iter().zip(types.iter()).enumerate() {
validate_expr_internal(&item.expr, ty, ctx).map_err(|e| e.in_element(i))?;
}
return Ok(());
}
Expr::Struct(s) => {
if let Some(StructBody::Tuple(body)) = &s.body {
&body.elements
} else {
return Err(expr_type_mismatch("Tuple", expr));
}
}
_ => return Err(expr_type_mismatch("Tuple", expr)),
};
if items.len() != types.len() {
return Err(length_mismatch_error(
types.len(),
items.len(),
*expr.span(),
));
}
for (i, (item, ty)) in items.iter().zip(types.iter()).enumerate() {
validate_expr_internal(&item.expr, ty, ctx).map_err(|e| e.in_element(i))?;
}
Ok(())
}
#[derive(Debug)]
enum ExprFlattenedTarget {
Struct {
fields: Vec<Field>,
presence_based: bool,
},
MapValue(TypeKind),
}
fn collect_expr_flattened_targets<R: SchemaResolver>(
fields: &[Field],
ctx: &mut ValidationContext<R>,
) -> Vec<ExprFlattenedTarget> {
let mut targets = Vec::new();
for field in fields {
if !field.flattened {
continue;
}
let Some((kind, mut presence_based)) = resolve_flattened_kind(&field.ty, ctx) else {
continue;
};
presence_based |= field.optional;
match kind {
TypeKind::Struct { fields } => {
targets.push(ExprFlattenedTarget::Struct {
fields,
presence_based,
});
}
TypeKind::Map { key, value } => {
if matches!(*key, TypeKind::String) {
targets.push(ExprFlattenedTarget::MapValue(*value));
}
}
_ => {}
}
}
targets
}
#[allow(clippy::result_large_err)]
fn validate_struct_expr<R: SchemaResolver>(
expr: &Expr<'_>,
fields: &[Field],
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
let flattened_targets = collect_expr_flattened_targets(fields, ctx);
let map_value_type = flattened_targets.iter().find_map(|target| {
if let ExprFlattenedTarget::MapValue(value_type) = target {
Some(value_type)
} else {
None
}
});
match expr {
Expr::Unit(_) => validate_struct_fields_expr(
&[],
fields,
&flattened_targets,
map_value_type,
*expr.span(),
ctx,
),
Expr::AnonStruct(anon) => validate_struct_fields_expr(
&anon.fields,
fields,
&flattened_targets,
map_value_type,
*expr.span(),
ctx,
),
Expr::Struct(s) => {
match &s.body {
Some(StructBody::Fields(f)) => validate_struct_fields_body_expr(
&f.fields,
fields,
&flattened_targets,
map_value_type,
*expr.span(),
ctx,
),
Some(StructBody::Tuple(_)) => {
Err(expr_type_mismatch("Struct", expr))
}
None => {
validate_struct_fields_expr(
&[],
fields,
&flattened_targets,
map_value_type,
*expr.span(),
ctx,
)
}
}
}
_ => Err(expr_type_mismatch("Struct", expr)),
}
}
#[allow(clippy::result_large_err)]
fn validate_struct_fields_expr<R: SchemaResolver>(
ast_fields: &[crate::ast::StructField<'_>],
schema_fields: &[Field],
flattened_targets: &[ExprFlattenedTarget],
map_value_type: Option<&TypeKind>,
struct_span: Span,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
let explicit_fields: Vec<_> = schema_fields.iter().filter(|f| !f.flattened).collect();
for ast_field in ast_fields {
let key_str = ast_field.name.name.as_ref();
let schema_field = explicit_fields.iter().find(|f| f.name == key_str);
if let Some(field) = schema_field {
validate_expr_internal(&ast_field.value, &field.ty, ctx)
.map_err(|e| e.in_field(key_str))?;
}
let mut matched_flattened_struct = false;
for target in flattened_targets {
if let ExprFlattenedTarget::Struct { fields, .. } = target
&& let Some(inner) = fields.iter().find(|f| f.name == key_str)
{
matched_flattened_struct = true;
validate_expr_internal(&ast_field.value, &inner.ty, ctx)
.map_err(|e| e.in_field(key_str))?;
}
}
if schema_field.is_none() && !matched_flattened_struct {
if let Some(map_value_type) = map_value_type {
validate_expr_internal(&ast_field.value, map_value_type, ctx)
.map_err(|e| e.in_field(key_str))?;
} else {
return Err(unknown_field_error(key_str, ast_field.name.span));
}
}
}
let has_field = |name: &str| ast_fields.iter().any(|f| f.name.name == name);
for field in &explicit_fields {
if !field.optional && !has_field(&field.name) {
return Err(missing_field_error(&field.name, struct_span));
}
}
for target in flattened_targets {
let ExprFlattenedTarget::Struct {
fields,
presence_based,
} = target
else {
continue;
};
if *presence_based && !fields.iter().any(|field| has_field(&field.name)) {
continue;
}
for field in fields {
if !field.optional && !has_field(&field.name) {
return Err(missing_field_error(&field.name, struct_span));
}
}
}
Ok(())
}
#[allow(clippy::result_large_err)]
fn validate_struct_fields_body_expr<R: SchemaResolver>(
ast_fields: &[crate::ast::StructField<'_>],
schema_fields: &[Field],
flattened_targets: &[ExprFlattenedTarget],
map_value_type: Option<&TypeKind>,
struct_span: Span,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
validate_struct_fields_expr(
ast_fields,
schema_fields,
flattened_targets,
map_value_type,
struct_span,
ctx,
)
}
#[allow(clippy::result_large_err)]
fn validate_enum_expr<R: SchemaResolver>(
expr: &Expr<'_>,
variants: &[Variant],
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
let (variant_name, variant_span, content): (&str, Span, EnumExprContent<'_>) = match expr {
Expr::String(s) => (s.value.as_str(), s.span, EnumExprContent::Unit),
Expr::Option(opt) => {
if let Some(val) = &opt.value {
("Some", opt.span, EnumExprContent::Tuple(&[&val.expr]))
} else {
("None", opt.span, EnumExprContent::Unit)
}
}
Expr::Struct(s) => {
let name = s.name.name.as_ref();
let span = s.name.span;
match &s.body {
None => (name, span, EnumExprContent::Unit),
Some(StructBody::Tuple(body)) => {
let exprs: Vec<_> = body.elements.iter().map(|e| &e.expr).collect();
(name, span, EnumExprContent::TupleOwned(exprs))
}
Some(StructBody::Fields(body)) => {
(name, span, EnumExprContent::Struct(&body.fields))
}
}
}
_ => return Err(expr_type_mismatch("Enum", expr)),
};
let variant = variants
.iter()
.find(|v| v.name == variant_name)
.ok_or_else(|| unknown_variant_error(variant_name, variant_span))?;
let validate_tuple_content =
|items: &[&Expr<'_>], types: &[TypeKind], ctx: &mut ValidationContext<R>| {
for (i, (item, ty)) in items.iter().zip(types.iter()).enumerate() {
validate_expr_internal(item, ty, ctx)
.map_err(|e| e.in_element(i).in_variant(variant_name))?;
}
Ok(())
};
match (&variant.kind, content) {
(VariantKind::Unit, EnumExprContent::Unit) => Ok(()),
(VariantKind::Tuple(types), EnumExprContent::Tuple(items))
if items.len() == types.len() =>
{
validate_tuple_content(items, types, ctx)
}
(VariantKind::Tuple(types), EnumExprContent::TupleOwned(ref items))
if items.len() == types.len() =>
{
validate_tuple_content(items, types, ctx)
}
(VariantKind::Struct(fields), EnumExprContent::Struct(ast_fields)) => {
validate_variant_struct_fields(ast_fields, fields, variant_name, variant_span, ctx)
}
(VariantKind::Unit, _) => Err(ValidationError::with_span(
crate::error::ErrorKind::TypeMismatch {
expected: "Unit".to_string(),
found: "non-unit content".to_string(),
},
variant_span,
)
.in_variant(variant_name)),
(_, EnumExprContent::Unit) => Err(ValidationError::with_span(
crate::error::ErrorKind::TypeMismatch {
expected: "variant content".to_string(),
found: "none".to_string(),
},
variant_span,
)
.in_variant(variant_name)),
(VariantKind::Tuple(types), _) => Err(ValidationError::with_span(
crate::error::ErrorKind::TypeMismatch {
expected: format!("Tuple({} elements)", types.len()),
found: "mismatched content".to_string(),
},
variant_span,
)
.in_variant(variant_name)),
(VariantKind::Struct(_), _) => Err(ValidationError::with_span(
crate::error::ErrorKind::TypeMismatch {
expected: "Struct".to_string(),
found: "mismatched content".to_string(),
},
variant_span,
)
.in_variant(variant_name)),
}
}
enum EnumExprContent<'a> {
Unit,
Tuple(&'a [&'a Expr<'a>]),
TupleOwned(Vec<&'a Expr<'a>>),
Struct(&'a [crate::ast::StructField<'a>]),
}
#[allow(clippy::result_large_err)]
fn validate_variant_struct_fields<R: SchemaResolver>(
ast_fields: &[crate::ast::StructField<'_>],
schema_fields: &[Field],
variant_name: &str,
variant_span: Span,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
for ast_field in ast_fields {
let key = ast_field.name.name.as_ref();
let field = schema_fields
.iter()
.find(|f| f.name == key)
.ok_or_else(|| {
unknown_field_error(key, ast_field.name.span).in_variant(variant_name)
})?;
validate_expr_internal(&ast_field.value, &field.ty, ctx)
.map_err(|e| e.in_field(key).in_variant(variant_name))?;
}
for field in schema_fields {
if !field.optional && !ast_fields.iter().any(|f| f.name.name == field.name) {
return Err(missing_field_error(&field.name, variant_span).in_variant(variant_name));
}
}
Ok(())
}
#[allow(clippy::result_large_err)]
fn validate_typeref_expr<R: SchemaResolver>(
expr: &Expr<'_>,
type_path: &str,
ctx: &mut ValidationContext<R>,
) -> ValidationResult<()> {
if ctx.is_visiting(type_path) {
return Ok(());
}
match ctx.resolver.resolve(type_path) {
Some(schema) => {
ctx.start_visiting(type_path);
let result = validate_expr_internal(expr, &schema.kind, ctx)
.map_err(|e| e.in_type_ref(type_path));
ctx.stop_visiting(type_path);
result
}
None => Ok(()),
}
}
fn validate_expr_collect_internal<R: SchemaResolver>(
expr: &Expr<'_>,
kind: &TypeKind,
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
if matches!(expr, Expr::Error(_)) {
return;
}
match kind {
TypeKind::Bool => {
if !matches!(expr, Expr::Bool(_)) {
errors.push(expr_type_mismatch("Bool", expr));
}
}
TypeKind::I8
| TypeKind::I16
| TypeKind::I32
| TypeKind::I64
| TypeKind::I128
| TypeKind::U8
| TypeKind::U16
| TypeKind::U32
| TypeKind::U64
| TypeKind::U128 => match expr {
Expr::Number(n) => {
if let Err(e) = validate_integer_expr(n, kind) {
errors.push(e);
}
}
_ => {
errors.push(expr_type_mismatch("integer", expr));
}
},
TypeKind::F32 | TypeKind::F64 => {
if !matches!(expr, Expr::Number(_)) {
errors.push(expr_type_mismatch("float", expr));
}
}
TypeKind::Char => {
if !matches!(expr, Expr::Char(_)) {
errors.push(expr_type_mismatch("Char", expr));
}
}
TypeKind::String => {
if !matches!(expr, Expr::String(_)) {
errors.push(expr_type_mismatch("String", expr));
}
}
TypeKind::Unit => {
if !matches!(expr, Expr::Unit(_)) {
errors.push(expr_type_mismatch("Unit", expr));
}
}
TypeKind::Option(inner) => {
validate_option_expr_collect(expr, inner, ctx, errors);
}
TypeKind::List(inner) => {
validate_list_expr_collect(expr, inner, ctx, errors);
}
TypeKind::Map { key, value: val_ty } => {
validate_map_expr_collect(expr, key, val_ty, ctx, errors);
}
TypeKind::Tuple(types) => {
validate_tuple_expr_collect(expr, types, ctx, errors);
}
TypeKind::Struct { fields } => {
validate_struct_expr_collect(expr, fields, ctx, errors);
}
TypeKind::Enum { variants } => {
validate_enum_expr_collect(expr, variants, ctx, errors);
}
TypeKind::TypeRef(type_path) => {
validate_typeref_expr_collect(expr, type_path, ctx, errors);
}
}
}
fn validate_option_expr_collect<R: SchemaResolver>(
expr: &Expr<'_>,
inner: &TypeKind,
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
match expr {
Expr::Error(_) => {} Expr::Option(opt) => {
if let Some(val) = &opt.value {
validate_expr_collect_internal(&val.expr, inner, ctx, errors);
}
}
Expr::Struct(s) if s.name.name == "None" && s.body.is_none() => {}
Expr::Struct(s) if s.name.name == "Some" => {
if let Some(StructBody::Tuple(body)) = &s.body
&& body.elements.len() == 1
{
validate_expr_collect_internal(&body.elements[0].expr, inner, ctx, errors);
return;
}
errors.push(expr_type_mismatch("Option", expr));
}
_ => {
errors.push(expr_type_mismatch("Option", expr));
}
}
}
fn validate_list_expr_collect<R: SchemaResolver>(
expr: &Expr<'_>,
inner: &TypeKind,
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
match expr {
Expr::Error(_) => {} Expr::Seq(seq) => {
for (i, item) in seq.items.iter().enumerate() {
let mut item_errors = Vec::new();
validate_expr_collect_internal(&item.expr, inner, ctx, &mut item_errors);
for e in item_errors {
errors.push(e.in_element(i));
}
}
}
_ => {
errors.push(expr_type_mismatch("List", expr));
}
}
}
fn validate_map_expr_collect<R: SchemaResolver>(
expr: &Expr<'_>,
key_ty: &TypeKind,
val_ty: &TypeKind,
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
match expr {
Expr::Error(_) => {} Expr::Map(map) => {
for entry in &map.entries {
let mut key_errors = Vec::new();
validate_expr_collect_internal(&entry.key, key_ty, ctx, &mut key_errors);
for e in key_errors {
errors.push(ValidationError::in_map_key(e));
}
let key_str = format_expr_as_key(&entry.key);
let mut val_errors = Vec::new();
validate_expr_collect_internal(&entry.value, val_ty, ctx, &mut val_errors);
for e in val_errors {
errors.push(e.in_map_value(key_str.clone()));
}
}
}
_ => {
errors.push(expr_type_mismatch("Map", expr));
}
}
}
fn validate_tuple_expr_collect<R: SchemaResolver>(
expr: &Expr<'_>,
types: &[TypeKind],
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
let items: &[_] = match expr {
Expr::Error(_) => return, Expr::Tuple(t) => &t.elements,
Expr::Seq(s) => {
if s.items.len() != types.len() {
errors.push(length_mismatch_error(
types.len(),
s.items.len(),
*expr.span(),
));
return;
}
for (i, (item, ty)) in s.items.iter().zip(types.iter()).enumerate() {
let mut item_errors = Vec::new();
validate_expr_collect_internal(&item.expr, ty, ctx, &mut item_errors);
for e in item_errors {
errors.push(e.in_element(i));
}
}
return;
}
Expr::Struct(s) => {
if let Some(StructBody::Tuple(body)) = &s.body {
&body.elements
} else {
errors.push(expr_type_mismatch("Tuple", expr));
return;
}
}
_ => {
errors.push(expr_type_mismatch("Tuple", expr));
return;
}
};
if items.len() != types.len() {
errors.push(length_mismatch_error(
types.len(),
items.len(),
*expr.span(),
));
return;
}
for (i, (item, ty)) in items.iter().zip(types.iter()).enumerate() {
let mut item_errors = Vec::new();
validate_expr_collect_internal(&item.expr, ty, ctx, &mut item_errors);
for e in item_errors {
errors.push(e.in_element(i));
}
}
}
fn validate_struct_expr_collect<R: SchemaResolver>(
expr: &Expr<'_>,
fields: &[Field],
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
let flattened_targets = collect_expr_flattened_targets(fields, ctx);
let map_value_type = flattened_targets.iter().find_map(|target| {
if let ExprFlattenedTarget::MapValue(value_type) = target {
Some(value_type)
} else {
None
}
});
match expr {
Expr::Error(_) => {} Expr::Unit(_) => {
validate_struct_fields_expr_collect(
&[],
fields,
&flattened_targets,
map_value_type,
*expr.span(),
ctx,
errors,
);
}
Expr::AnonStruct(anon) => {
validate_struct_fields_expr_collect(
&anon.fields,
fields,
&flattened_targets,
map_value_type,
*expr.span(),
ctx,
errors,
);
}
Expr::Struct(s) => match &s.body {
Some(StructBody::Fields(f)) => {
validate_struct_fields_expr_collect(
&f.fields,
fields,
&flattened_targets,
map_value_type,
*expr.span(),
ctx,
errors,
);
}
Some(StructBody::Tuple(_)) => {
errors.push(expr_type_mismatch("Struct", expr));
}
None => {
validate_struct_fields_expr_collect(
&[],
fields,
&flattened_targets,
map_value_type,
*expr.span(),
ctx,
errors,
);
}
},
_ => {
errors.push(expr_type_mismatch("Struct", expr));
}
}
}
fn validate_struct_fields_expr_collect<R: SchemaResolver>(
ast_fields: &[crate::ast::StructField<'_>],
schema_fields: &[Field],
flattened_targets: &[ExprFlattenedTarget],
map_value_type: Option<&TypeKind>,
struct_span: Span,
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
let explicit_fields: Vec<_> = schema_fields.iter().filter(|f| !f.flattened).collect();
for ast_field in ast_fields {
if matches!(ast_field.value, Expr::Error(_)) {
continue;
}
let key_str = ast_field.name.name.as_ref();
let schema_field = explicit_fields.iter().find(|f| f.name == key_str);
if let Some(field) = schema_field {
let mut field_errors = Vec::new();
validate_expr_collect_internal(&ast_field.value, &field.ty, ctx, &mut field_errors);
for e in field_errors {
errors.push(e.in_field(key_str));
}
}
let mut matched_flattened_struct = false;
for target in flattened_targets {
if let ExprFlattenedTarget::Struct {
fields: inner_fields,
..
} = target
&& let Some(inner) = inner_fields.iter().find(|f| f.name == key_str)
{
matched_flattened_struct = true;
let mut field_errors = Vec::new();
validate_expr_collect_internal(&ast_field.value, &inner.ty, ctx, &mut field_errors);
for e in field_errors {
errors.push(e.in_field(key_str));
}
}
}
if schema_field.is_none() && !matched_flattened_struct {
if let Some(map_value_type) = map_value_type {
let mut field_errors = Vec::new();
validate_expr_collect_internal(
&ast_field.value,
map_value_type,
ctx,
&mut field_errors,
);
for e in field_errors {
errors.push(e.in_field(key_str));
}
} else {
errors.push(unknown_field_error(key_str, ast_field.name.span));
}
}
}
let has_field = |name: &str| ast_fields.iter().any(|f| f.name.name == name);
for field in &explicit_fields {
if !field.optional && !has_field(&field.name) {
errors.push(missing_field_error(&field.name, struct_span));
}
}
for target in flattened_targets {
let ExprFlattenedTarget::Struct {
fields: inner_fields,
presence_based,
} = target
else {
continue;
};
if *presence_based && !inner_fields.iter().any(|field| has_field(&field.name)) {
continue;
}
for field in inner_fields {
if !field.optional && !has_field(&field.name) {
errors.push(missing_field_error(&field.name, struct_span));
}
}
}
}
fn validate_enum_tuple_content<R: SchemaResolver>(
items: &[&Expr<'_>],
types: &[TypeKind],
variant_name: &str,
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
for (i, (item, ty)) in items.iter().zip(types.iter()).enumerate() {
let mut item_errors = Vec::new();
validate_expr_collect_internal(item, ty, ctx, &mut item_errors);
for e in item_errors {
errors.push(e.in_element(i).in_variant(variant_name));
}
}
}
fn variant_kind_mismatch_error(
expected: &str,
found: &str,
variant_name: &str,
variant_span: Span,
) -> ValidationError {
ValidationError::with_span(
crate::error::ErrorKind::TypeMismatch {
expected: expected.to_string(),
found: found.to_string(),
},
variant_span,
)
.in_variant(variant_name)
}
fn validate_enum_expr_collect<R: SchemaResolver>(
expr: &Expr<'_>,
variants: &[Variant],
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
if matches!(expr, Expr::Error(_)) {
return;
}
let (variant_name, variant_span, content): (&str, Span, EnumExprContent<'_>) = match expr {
Expr::String(s) => (s.value.as_str(), s.span, EnumExprContent::Unit),
Expr::Option(opt) => {
if let Some(val) = &opt.value {
("Some", opt.span, EnumExprContent::Tuple(&[&val.expr]))
} else {
("None", opt.span, EnumExprContent::Unit)
}
}
Expr::Struct(s) => {
let name = s.name.name.as_ref();
let span = s.name.span;
match &s.body {
None => (name, span, EnumExprContent::Unit),
Some(StructBody::Tuple(body)) => {
let exprs: Vec<_> = body.elements.iter().map(|e| &e.expr).collect();
(name, span, EnumExprContent::TupleOwned(exprs))
}
Some(StructBody::Fields(body)) => {
(name, span, EnumExprContent::Struct(&body.fields))
}
}
}
_ => {
errors.push(expr_type_mismatch("Enum", expr));
return;
}
};
let Some(variant) = variants.iter().find(|v| v.name == variant_name) else {
errors.push(unknown_variant_error(variant_name, variant_span));
return;
};
match (&variant.kind, content) {
(VariantKind::Unit, EnumExprContent::Unit) => {}
(VariantKind::Tuple(types), EnumExprContent::Tuple(items))
if items.len() == types.len() =>
{
validate_enum_tuple_content(items, types, variant_name, ctx, errors);
}
(VariantKind::Tuple(types), EnumExprContent::TupleOwned(ref items))
if items.len() == types.len() =>
{
validate_enum_tuple_content(items, types, variant_name, ctx, errors);
}
(VariantKind::Struct(fields), EnumExprContent::Struct(ast_fields)) => {
validate_variant_struct_fields_collect(
ast_fields,
fields,
variant_name,
variant_span,
ctx,
errors,
);
}
(VariantKind::Unit, _) => {
errors.push(variant_kind_mismatch_error(
"Unit",
"non-unit content",
variant_name,
variant_span,
));
}
(_, EnumExprContent::Unit) => {
errors.push(variant_kind_mismatch_error(
"variant content",
"none",
variant_name,
variant_span,
));
}
(VariantKind::Tuple(types), _) => {
errors.push(variant_kind_mismatch_error(
&format!("Tuple({} elements)", types.len()),
"mismatched content",
variant_name,
variant_span,
));
}
(VariantKind::Struct(_), _) => {
errors.push(variant_kind_mismatch_error(
"Struct",
"mismatched content",
variant_name,
variant_span,
));
}
}
}
fn validate_variant_struct_fields_collect<R: SchemaResolver>(
ast_fields: &[crate::ast::StructField<'_>],
schema_fields: &[Field],
variant_name: &str,
variant_span: Span,
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
for ast_field in ast_fields {
if matches!(ast_field.value, Expr::Error(_)) {
continue;
}
let key = ast_field.name.name.as_ref();
let Some(field) = schema_fields.iter().find(|f| f.name == key) else {
errors.push(unknown_field_error(key, ast_field.name.span).in_variant(variant_name));
continue;
};
let mut field_errors = Vec::new();
validate_expr_collect_internal(&ast_field.value, &field.ty, ctx, &mut field_errors);
for e in field_errors {
errors.push(e.in_field(key).in_variant(variant_name));
}
}
for field in schema_fields {
if !field.optional && !ast_fields.iter().any(|f| f.name.name == field.name) {
errors.push(missing_field_error(&field.name, variant_span).in_variant(variant_name));
}
}
}
fn validate_typeref_expr_collect<R: SchemaResolver>(
expr: &Expr<'_>,
type_path: &str,
ctx: &mut ValidationContext<R>,
errors: &mut Vec<ValidationError>,
) {
if matches!(expr, Expr::Error(_)) {
return;
}
if ctx.is_visiting(type_path) {
return;
}
if let Some(schema) = ctx.resolver.resolve(type_path) {
ctx.start_visiting(type_path);
let mut inner_errors = Vec::new();
validate_expr_collect_internal(expr, &schema.kind, ctx, &mut inner_errors);
for e in inner_errors {
errors.push(e.in_type_ref(type_path));
}
ctx.stop_visiting(type_path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{Field, Schema, TypeKind, Variant};
#[test]
fn test_validate_primitives() {
assert!(validate_type(&Value::Bool(true), &TypeKind::Bool).is_ok());
assert!(validate_type(&Value::Number(42.into()), &TypeKind::I32).is_ok());
assert!(validate_type(&Value::String("hello".into()), &TypeKind::String).is_ok());
assert!(validate_type(&Value::Char('a'), &TypeKind::Char).is_ok());
assert!(validate_type(&Value::Unit, &TypeKind::Unit).is_ok());
}
#[test]
fn test_validate_type_mismatch() {
assert!(validate_type(&Value::Bool(true), &TypeKind::String).is_err());
assert!(validate_type(&Value::String("hello".into()), &TypeKind::Bool).is_err());
}
#[test]
fn test_validate_list() {
let list_type = TypeKind::List(Box::new(TypeKind::I32));
let value = Value::Seq(vec![Value::Number(1.into()), Value::Number(2.into())]);
assert!(validate_type(&value, &list_type).is_ok());
let bad_value = Value::Seq(vec![Value::Number(1.into()), Value::String("bad".into())]);
assert!(validate_type(&bad_value, &list_type).is_err());
}
#[test]
fn test_validate_struct() {
let schema = Schema::new(TypeKind::Struct {
fields: vec![
Field::new("port", TypeKind::U16),
Field::optional("host", TypeKind::String),
],
});
let value: Value = "(port: 8080, host: \"localhost\")"
.parse::<Value>()
.unwrap();
assert!(validate(&value, &schema).is_ok());
let value: Value = "(port: 8080)".parse::<Value>().unwrap();
assert!(validate(&value, &schema).is_ok());
let value: Value = "(host: \"localhost\")".parse::<Value>().unwrap();
assert!(validate(&value, &schema).is_err());
}
#[test]
fn test_validate_enum() {
let schema = Schema::new(TypeKind::Enum {
variants: vec![
Variant::unit("None"),
Variant::tuple("Some", vec![TypeKind::I32]),
Variant::struct_variant("Complex", vec![Field::new("value", TypeKind::String)]),
],
});
let value: Value = "\"None\"".parse::<Value>().unwrap();
assert!(validate(&value, &schema).is_ok());
let value: Value = "\"Unknown\"".parse::<Value>().unwrap();
assert!(validate(&value, &schema).is_err());
}
#[test]
fn test_error_path_context() {
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new(
"items",
TypeKind::List(Box::new(TypeKind::String)),
)],
});
let value: Value = "(items: [\"ok\", 42])".parse::<Value>().unwrap();
let err = validate(&value, &schema).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("field 'items'"), "Error: {msg}");
assert!(msg.contains("element 1"), "Error: {msg}");
}
mod numeric_validation {
use super::*;
use crate::{
ast::parse_document,
value::{F32, F64},
};
#[test]
fn test_float_rejected_for_integer_type() {
let value = Value::Number(Number::F64(F64::new(2.5)));
let result = validate_type(&value, &TypeKind::I32);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("i32") && msg.contains("float"), "Error: {msg}");
}
#[test]
fn test_negative_rejected_for_unsigned_type() {
let value = Value::Number(Number::I32(-5));
let result = validate_type(&value, &TypeKind::U8);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("u8") && msg.contains("negative"),
"Error: {msg}"
);
}
#[test]
fn test_out_of_range_rejected_unsigned() {
let value = Value::Number(Number::U16(300));
let result = validate_type(&value, &TypeKind::U8);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("u8") && msg.contains("out of bounds"),
"Error: {msg}"
);
}
#[test]
fn test_out_of_range_rejected_signed() {
let value = Value::Number(Number::I16(-200));
let result = validate_type(&value, &TypeKind::I8);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("i8") && msg.contains("out of bounds"),
"Error: {msg}"
);
}
#[test]
fn test_edge_case_valid_u8() {
let value = Value::Number(Number::U8(255));
let result = validate_type(&value, &TypeKind::U8);
assert!(result.is_ok());
}
#[test]
fn test_edge_case_valid_i8() {
let value = Value::Number(Number::I8(-128));
let result = validate_type(&value, &TypeKind::I8);
assert!(result.is_ok());
}
#[test]
fn test_integer_accepted_for_float() {
let value = Value::Number(Number::I32(42));
assert!(validate_type(&value, &TypeKind::F32).is_ok());
assert!(validate_type(&value, &TypeKind::F64).is_ok());
}
#[test]
fn test_float_f32_accepted_for_f32() {
let value = Value::Number(Number::F32(F32::new(2.5)));
assert!(validate_type(&value, &TypeKind::F32).is_ok());
}
#[test]
fn test_ast_float_rejected_for_integer() {
let source = "(value: 2.5)";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::I32)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let msg = v.to_string();
assert!(msg.contains("i32") && msg.contains("float"), "Error: {msg}");
let span = v.span();
assert!(!span.is_synthetic());
assert_eq!(&source[span.start_offset..span.end_offset], "2.5");
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_negative_rejected_for_unsigned() {
let source = "(value: -5)";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::U8)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let msg = v.to_string();
assert!(
msg.contains("u8") && msg.contains("negative"),
"Error: {msg}"
);
let span = v.span();
assert!(!span.is_synthetic());
assert_eq!(&source[span.start_offset..span.end_offset], "-5");
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_out_of_range_unsigned() {
let source = "(value: 300)";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::U8)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let msg = v.to_string();
assert!(
msg.contains("u8") && msg.contains("out of bounds"),
"Error: {msg}"
);
let span = v.span();
assert!(!span.is_synthetic());
assert_eq!(&source[span.start_offset..span.end_offset], "300");
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_out_of_range_signed() {
let source = "(value: -200)";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::I8)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let msg = v.to_string();
assert!(
msg.contains("i8") && msg.contains("out of bounds"),
"Error: {msg}"
);
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_hex_out_of_range() {
let source = "(value: 0x100)"; let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::U8)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let msg = v.to_string();
assert!(
msg.contains("u8") && msg.contains("out of bounds"),
"Error: {msg}"
);
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_hex_valid() {
let source = "(value: 0xFF)"; let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::U8)],
});
let result = validate_expr(doc.value.as_ref().unwrap(), &schema);
assert!(result.is_ok());
}
#[test]
fn test_ast_binary_out_of_range() {
let source = "(value: 0b100000000)"; let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::U8)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let msg = v.to_string();
assert!(
msg.contains("u8") && msg.contains("out of bounds"),
"Error: {msg}"
);
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_special_float_rejected_for_integer() {
let source = "(value: inf)";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::I32)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let msg = v.to_string();
assert!(msg.contains("i32") && msg.contains("float"), "Error: {msg}");
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_collect_all_multiple_numeric_errors() {
let source = "(a: 2.5, b: -5, c: 300)";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![
Field::new("a", TypeKind::I32),
Field::new("b", TypeKind::U8),
Field::new("c", TypeKind::U8),
],
});
let errors =
validate_expr_collect_all(doc.value.as_ref().unwrap(), &schema, &AcceptAllResolver);
assert_eq!(errors.len(), 3, "Expected 3 errors, got: {errors:?}");
}
}
mod ast_validation {
use super::*;
use crate::{ast::parse_document, error::PathSegment};
#[test]
fn test_ast_validation_basic() {
let source = "(port: 8080, host: \"localhost\")";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![
Field::new("port", TypeKind::U16),
Field::optional("host", TypeKind::String),
],
});
let result = validate_expr(doc.value.as_ref().unwrap(), &schema);
assert!(result.is_ok());
}
#[test]
fn test_ast_validation_type_mismatch_has_span() {
let source = "(value: \"wrong\")";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("value", TypeKind::I32)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let span = v.span();
assert!(!span.is_synthetic(), "Expected non-synthetic span");
assert_eq!(&source[span.start_offset..span.end_offset], "\"wrong\"");
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_validation_nested_error_has_correct_span() {
let source = r#"(items: [1, "wrong", 3])"#;
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("items", TypeKind::List(Box::new(TypeKind::I32)))],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let span = v.span();
assert!(!span.is_synthetic(), "Expected non-synthetic span");
assert_eq!(
&source[span.start_offset..span.end_offset],
"\"wrong\"",
"Span should point to the wrong value"
);
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_validation_unknown_field_has_span() {
let source = "(known: 1, unknown_field: 2)";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new("known", TypeKind::I32)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let span = v.span();
assert!(!span.is_synthetic(), "Expected non-synthetic span");
assert_eq!(&source[span.start_offset..span.end_offset], "unknown_field");
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_validation_missing_field_uses_struct_span() {
let source = "(optional_field: 1)";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Struct {
fields: vec![
Field::new("required", TypeKind::I32),
Field::optional("optional_field", TypeKind::I32),
],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let span = v.span();
assert!(!span.is_synthetic(), "Expected non-synthetic span");
assert_eq!(&source[span.start_offset..span.end_offset], source);
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_validation_deeply_nested_error() {
let source = r#"Config(
data: (
items: [
(name: "first"),
(name: 123),
],
),
)"#;
let doc = parse_document(source).unwrap();
let item_schema = TypeKind::Struct {
fields: vec![Field::new("name", TypeKind::String)],
};
let schema = Schema::new(TypeKind::Struct {
fields: vec![Field::new(
"data",
TypeKind::Struct {
fields: vec![Field::new("items", TypeKind::List(Box::new(item_schema)))],
},
)],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let span = v.span();
assert!(!span.is_synthetic(), "Expected non-synthetic span");
assert_eq!(&source[span.start_offset..span.end_offset], "123");
assert!(
v.path()
.iter()
.any(|p| matches!(p, PathSegment::Field(f) if f == "name"))
);
assert!(
v.path()
.iter()
.any(|p| matches!(p, PathSegment::Element(1)))
);
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_validation_option_some_with_span() {
let source = "Some(\"wrong\")";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Option(Box::new(TypeKind::I32)));
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let span = v.span();
assert!(!span.is_synthetic(), "Expected non-synthetic span");
assert_eq!(&source[span.start_offset..span.end_offset], "\"wrong\"");
} else {
panic!("Expected validation error");
}
}
#[test]
fn test_ast_validation_enum_unknown_variant_span() {
let source = "BadVariant";
let doc = parse_document(source).unwrap();
let schema = Schema::new(TypeKind::Enum {
variants: vec![
crate::schema::Variant::unit("GoodVariant"),
crate::schema::Variant::unit("OtherVariant"),
],
});
let err = validate_expr(doc.value.as_ref().unwrap(), &schema).unwrap_err();
if let crate::schema::SchemaError::Validation(v) = &err {
let span = v.span();
assert!(!span.is_synthetic(), "Expected non-synthetic span");
assert_eq!(&source[span.start_offset..span.end_offset], "BadVariant");
} else {
panic!("Expected validation error");
}
}
}
}