use eure_document::document::NodeId;
use eure_document::parse::{BestParseVariantMatch, ParseError, UnionParseError};
use eure_document::path::EurePath;
use eure_document::value::ObjectKey;
use thiserror::Error;
use crate::SchemaNodeId;
#[derive(Debug, Clone, Error, PartialEq)]
pub enum ValidatorError {
#[error("undefined type reference: {name}")]
UndefinedTypeReference { name: String },
#[error("invalid variant tag '{tag}': {reason}")]
InvalidVariantTag { tag: String, reason: String },
#[error("conflicting variant tags: $variant = {explicit}, repr = {repr}")]
ConflictingVariantTags { explicit: String, repr: String },
#[error("cross-schema reference not supported: {namespace}.{name}")]
CrossSchemaReference { namespace: String, name: String },
#[error("parse error: {0}")]
DocumentParseError(#[from] ParseError),
#[error("inner errors propagated")]
InnerErrorsPropagated,
}
impl ValidatorError {
pub fn as_parse_error(&self) -> Option<&ParseError> {
match self {
ValidatorError::DocumentParseError(e) => Some(e),
_ => None,
}
}
}
impl UnionParseError for ValidatorError {
fn as_parse_error(&self) -> Option<&ParseError> {
ValidatorError::as_parse_error(self)
}
fn from_no_matching_variant(
_node_id: NodeId,
variant: Option<String>,
_best_match: Option<BestParseVariantMatch>,
failures: &[(String, Self)],
) -> Self {
if failures
.iter()
.any(|(_, error)| matches!(error, ValidatorError::InnerErrorsPropagated))
{
return ValidatorError::InnerErrorsPropagated;
}
ValidatorError::InvalidVariantTag {
tag: variant.unwrap_or_default(),
reason: "type mismatch".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BestVariantMatch {
pub variant_name: String,
pub variant_schema_id: SchemaNodeId,
pub error: Box<ValidationError>,
pub all_errors: Vec<ValidationError>,
pub depth: usize,
pub error_count: usize,
}
#[derive(Debug, Clone, Error, PartialEq)]
pub enum ValidationError {
#[error("Type mismatch: expected {expected}, got {actual} at path {path}")]
TypeMismatch {
expected: String,
actual: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("{}", format_missing_required_fields(fields, path))]
MissingRequiredField {
fields: Vec<String>,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Unknown field '{field}' at path {path}")]
UnknownField {
field: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Value {value} is out of range at path {path}")]
OutOfRange {
value: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("String length {length} is out of bounds at path {path}")]
StringLengthOutOfBounds {
length: usize,
min: Option<u32>,
max: Option<u32>,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("String does not match pattern '{pattern}' at path {path}")]
PatternMismatch {
pattern: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Array length {length} is out of bounds at path {path}")]
ArrayLengthOutOfBounds {
length: usize,
min: Option<u32>,
max: Option<u32>,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Map size {size} is out of bounds at path {path}")]
MapSizeOutOfBounds {
size: usize,
min: Option<u32>,
max: Option<u32>,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Tuple length mismatch: expected {expected}, got {actual} at path {path}")]
TupleLengthMismatch {
expected: usize,
actual: usize,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Array elements must be unique at path {path}")]
ArrayNotUnique {
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Array must contain required element at path {path}")]
ArrayMissingContains {
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("{}", format_no_variant_matched(path, best_match))]
NoVariantMatched {
path: EurePath,
best_match: Option<Box<BestVariantMatch>>,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Multiple variants matched for union at path {path}: {variants:?}")]
AmbiguousUnion {
path: EurePath,
variants: Vec<String>,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Invalid variant tag '{tag}' at path {path}")]
InvalidVariantTag {
tag: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Conflicting variant tags: $variant = {explicit}, repr = {repr} at path {path}")]
ConflictingVariantTags {
explicit: String,
repr: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Variant '{variant}' requires explicit $variant tag at path {path}")]
RequiresExplicitVariant {
variant: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Literal value mismatch at path {path}")]
LiteralMismatch {
expected: String,
actual: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Language mismatch: expected {expected}, got {actual} at path {path}")]
LanguageMismatch {
expected: String,
actual: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Invalid key type at path {path}")]
InvalidKeyType {
key: ObjectKey,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Integer not a multiple of {divisor} at path {path}")]
NotMultipleOf {
divisor: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Undefined type reference '{name}' at path {path}")]
UndefinedTypeReference {
name: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error(
"Invalid flatten target: expected Record, Union, or Map, got {actual_kind} at path {path}"
)]
InvalidFlattenTarget {
actual_kind: crate::SchemaKind,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Flatten map key '{key}' does not match pattern at path {path}")]
FlattenMapKeyMismatch {
key: String,
pattern: Option<String>,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("Missing required extension '{extension}' at path {path}")]
MissingRequiredExtension {
extension: String,
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
},
#[error("{}", format_parse_error(path, error))]
ParseError {
path: EurePath,
node_id: NodeId,
schema_node_id: SchemaNodeId,
error: eure_document::parse::ParseError,
},
}
fn format_missing_required_fields(fields: &[String], path: &EurePath) -> String {
match fields.len() {
1 => format!("Missing required field '{}' at path {}", fields[0], path),
_ => {
let field_list = fields
.iter()
.map(|f| format!("'{}'", f))
.collect::<Vec<_>>()
.join(", ");
format!("Missing required fields {} at path {}", field_list, path)
}
}
}
fn format_parse_error(path: &EurePath, error: &eure_document::parse::ParseError) -> String {
use eure_document::parse::ParseErrorKind;
match &error.kind {
ParseErrorKind::UnknownVariant(name) => {
format!("Invalid variant tag '{name}' at path {path}")
}
ParseErrorKind::ConflictingVariantTags { explicit, repr } => {
format!("Conflicting variant tags: $variant = {explicit}, repr = {repr} at path {path}")
}
ParseErrorKind::InvalidVariantType(kind) => {
format!("$variant must be a string, got {kind:?} at path {path}")
}
ParseErrorKind::InvalidVariantPath(path_str) => {
format!("Invalid $variant path syntax: '{path_str}' at path {path}")
}
_ => format!("{} at path {}", error.kind, path),
}
}
fn format_no_variant_matched(
path: &EurePath,
best_match: &Option<Box<BestVariantMatch>>,
) -> String {
match best_match {
Some(best) => {
let is_nested_union = matches!(
best.error.as_ref(),
ValidationError::NoVariantMatched { .. }
);
if is_nested_union {
let mut msg = best.error.to_string();
if best.all_errors.len() > 1 {
msg.push_str(&format!(" (and {} more errors)", best.all_errors.len() - 1));
}
msg
} else {
let mut msg = best.error.to_string();
if best.all_errors.len() > 1 {
msg.push_str(&format!(" (and {} more errors)", best.all_errors.len() - 1));
}
msg.push_str(&format!(
" (based on nearest variant '{}' for union at path {})",
best.variant_name, path
));
msg
}
}
None => format!("No variant matched for union at path {path}"),
}
}
impl ValidationError {
pub fn node_ids(&self) -> (NodeId, SchemaNodeId) {
match self {
Self::TypeMismatch {
node_id,
schema_node_id,
..
}
| Self::MissingRequiredField {
node_id,
schema_node_id,
..
}
| Self::UnknownField {
node_id,
schema_node_id,
..
}
| Self::OutOfRange {
node_id,
schema_node_id,
..
}
| Self::StringLengthOutOfBounds {
node_id,
schema_node_id,
..
}
| Self::PatternMismatch {
node_id,
schema_node_id,
..
}
| Self::ArrayLengthOutOfBounds {
node_id,
schema_node_id,
..
}
| Self::MapSizeOutOfBounds {
node_id,
schema_node_id,
..
}
| Self::TupleLengthMismatch {
node_id,
schema_node_id,
..
}
| Self::ArrayNotUnique {
node_id,
schema_node_id,
..
}
| Self::ArrayMissingContains {
node_id,
schema_node_id,
..
}
| Self::NoVariantMatched {
node_id,
schema_node_id,
..
}
| Self::AmbiguousUnion {
node_id,
schema_node_id,
..
}
| Self::InvalidVariantTag {
node_id,
schema_node_id,
..
}
| Self::ConflictingVariantTags {
node_id,
schema_node_id,
..
}
| Self::RequiresExplicitVariant {
node_id,
schema_node_id,
..
}
| Self::LiteralMismatch {
node_id,
schema_node_id,
..
}
| Self::LanguageMismatch {
node_id,
schema_node_id,
..
}
| Self::InvalidKeyType {
node_id,
schema_node_id,
..
}
| Self::NotMultipleOf {
node_id,
schema_node_id,
..
}
| Self::UndefinedTypeReference {
node_id,
schema_node_id,
..
}
| Self::InvalidFlattenTarget {
node_id,
schema_node_id,
..
}
| Self::FlattenMapKeyMismatch {
node_id,
schema_node_id,
..
}
| Self::MissingRequiredExtension {
node_id,
schema_node_id,
..
}
| Self::ParseError {
node_id,
schema_node_id,
..
} => (*node_id, *schema_node_id),
}
}
pub fn deepest_error(&self) -> &ValidationError {
match self {
Self::NoVariantMatched {
best_match: Some(best),
..
} => {
match best.error.as_ref() {
Self::NoVariantMatched { .. } => best.error.deepest_error(),
Self::TypeMismatch { .. }
| Self::LiteralMismatch { .. }
| Self::LanguageMismatch { .. }
| Self::OutOfRange { .. }
| Self::NotMultipleOf { .. }
| Self::PatternMismatch { .. }
| Self::StringLengthOutOfBounds { .. }
| Self::InvalidKeyType { .. }
| Self::UnknownField { .. } => best.error.deepest_error(),
_ => self,
}
}
_ => self,
}
}
pub fn depth(&self) -> usize {
match self {
Self::TypeMismatch { path, .. }
| Self::MissingRequiredField { path, .. }
| Self::UnknownField { path, .. }
| Self::OutOfRange { path, .. }
| Self::StringLengthOutOfBounds { path, .. }
| Self::PatternMismatch { path, .. }
| Self::ArrayLengthOutOfBounds { path, .. }
| Self::MapSizeOutOfBounds { path, .. }
| Self::TupleLengthMismatch { path, .. }
| Self::ArrayNotUnique { path, .. }
| Self::ArrayMissingContains { path, .. }
| Self::NoVariantMatched { path, .. }
| Self::AmbiguousUnion { path, .. }
| Self::InvalidVariantTag { path, .. }
| Self::ConflictingVariantTags { path, .. }
| Self::RequiresExplicitVariant { path, .. }
| Self::LiteralMismatch { path, .. }
| Self::LanguageMismatch { path, .. }
| Self::InvalidKeyType { path, .. }
| Self::NotMultipleOf { path, .. }
| Self::UndefinedTypeReference { path, .. }
| Self::InvalidFlattenTarget { path, .. }
| Self::FlattenMapKeyMismatch { path, .. }
| Self::MissingRequiredExtension { path, .. }
| Self::ParseError { path, .. } => path.0.len(),
}
}
pub fn priority_score(&self) -> u8 {
match self {
Self::UnknownField { .. } => 95,
Self::MissingRequiredField { .. } => 90,
Self::TypeMismatch { .. } => 80,
Self::TupleLengthMismatch { .. } => 70,
Self::LiteralMismatch { .. } => 70,
Self::InvalidVariantTag { .. } => 65,
Self::NoVariantMatched { .. } => 60, Self::MissingRequiredExtension { .. } => 50,
Self::ParseError { .. } => 40, Self::OutOfRange { .. } => 30,
Self::StringLengthOutOfBounds { .. } => 30,
Self::PatternMismatch { .. } => 30,
Self::FlattenMapKeyMismatch { .. } => 30, Self::ArrayLengthOutOfBounds { .. } => 30,
Self::MapSizeOutOfBounds { .. } => 30,
Self::NotMultipleOf { .. } => 30,
Self::ArrayNotUnique { .. } => 25,
Self::ArrayMissingContains { .. } => 25,
Self::InvalidKeyType { .. } => 20,
Self::LanguageMismatch { .. } => 20,
Self::AmbiguousUnion { .. } => 0, Self::ConflictingVariantTags { .. } => 0, Self::UndefinedTypeReference { .. } => 0, Self::InvalidFlattenTarget { .. } => 0, Self::RequiresExplicitVariant { .. } => 0, }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationWarning {
UnknownExtension { name: String, path: EurePath },
DeprecatedField { field: String, path: EurePath },
}
fn compute_depth_and_structural_match(errors: &[ValidationError]) -> (usize, bool) {
let min_depth = errors.iter().map(|e| e.depth()).min().unwrap_or(0);
let mut max_depth = 0;
let mut structural_match = true;
for error in errors {
match error {
ValidationError::NoVariantMatched { best_match, .. } => {
if let Some(best) = best_match {
let (nested_depth, nested_structural) =
compute_depth_and_structural_match(&best.all_errors);
max_depth = max_depth.max(nested_depth);
if !nested_structural {
structural_match = false;
}
}
}
ValidationError::TypeMismatch { .. } if error.depth() == min_depth => {
max_depth = max_depth.max(error.depth());
structural_match = false;
}
_ => {
max_depth = max_depth.max(error.depth());
}
}
}
(max_depth, structural_match)
}
pub fn select_best_variant_match(
variant_errors: Vec<(String, SchemaNodeId, Vec<ValidationError>)>,
) -> Option<BestVariantMatch> {
if variant_errors.is_empty() {
return None;
}
let best = variant_errors
.into_iter()
.filter(|(_, _, errors)| !errors.is_empty())
.max_by_key(|(_, _, errors)| {
let (max_depth, structural_match) = compute_depth_and_structural_match(errors);
let error_count = errors.len();
let max_priority = errors.iter().map(|e| e.priority_score()).max().unwrap_or(0);
(
structural_match,
max_depth,
usize::MAX - error_count,
max_priority,
)
});
best.map(|(variant_name, variant_schema_id, mut errors)| {
let depth = errors.iter().map(|e| e.depth()).max().unwrap_or(0);
let error_count = errors.len();
errors.sort_by_key(|e| {
(
std::cmp::Reverse(e.priority_score()),
std::cmp::Reverse(e.depth()),
)
});
let primary_error = errors.first().cloned().unwrap();
BestVariantMatch {
variant_name,
variant_schema_id,
error: Box::new(primary_error),
all_errors: errors,
depth,
error_count,
}
})
}