1use eure_document::document::NodeId;
8use eure_document::parse::ParseError;
9use eure_document::path::EurePath;
10use eure_document::value::ObjectKey;
11use thiserror::Error;
12
13use crate::SchemaNodeId;
14
15#[derive(Debug, Clone, Error, PartialEq)]
24pub enum ValidatorError {
25 #[error("undefined type reference: {name}")]
27 UndefinedTypeReference { name: String },
28
29 #[error("invalid variant tag '{tag}': {reason}")]
31 InvalidVariantTag { tag: String, reason: String },
32
33 #[error("conflicting variant tags: $variant = {explicit}, repr = {repr}")]
35 ConflictingVariantTags { explicit: String, repr: String },
36
37 #[error("cross-schema reference not supported: {namespace}.{name}")]
39 CrossSchemaReference { namespace: String, name: String },
40
41 #[error("parse error: {0}")]
43 DocumentParseError(#[from] ParseError),
44
45 #[error("inner errors propagated")]
47 InnerErrorsPropagated,
48}
49
50impl ValidatorError {
51 pub fn as_parse_error(&self) -> Option<&ParseError> {
53 match self {
54 ValidatorError::DocumentParseError(e) => Some(e),
55 _ => None,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq)]
83pub struct BestVariantMatch {
84 pub variant_name: String,
86 pub variant_schema_id: SchemaNodeId,
88 pub error: Box<ValidationError>,
90 pub all_errors: Vec<ValidationError>,
92 pub depth: usize,
94 pub error_count: usize,
96}
97
98#[derive(Debug, Clone, Error, PartialEq)]
107pub enum ValidationError {
108 #[error("Type mismatch: expected {expected}, got {actual} at path {path}")]
109 TypeMismatch {
110 expected: String,
111 actual: String,
112 path: EurePath,
113 node_id: NodeId,
114 schema_node_id: SchemaNodeId,
115 },
116
117 #[error("{}", format_missing_required_fields(fields, path))]
118 MissingRequiredField {
119 fields: Vec<String>,
120 path: EurePath,
121 node_id: NodeId,
122 schema_node_id: SchemaNodeId,
123 },
124
125 #[error("Unknown field '{field}' at path {path}")]
126 UnknownField {
127 field: String,
128 path: EurePath,
129 node_id: NodeId,
130 schema_node_id: SchemaNodeId,
131 },
132
133 #[error("Value {value} is out of range at path {path}")]
134 OutOfRange {
135 value: String,
136 path: EurePath,
137 node_id: NodeId,
138 schema_node_id: SchemaNodeId,
139 },
140
141 #[error("String length {length} is out of bounds at path {path}")]
142 StringLengthOutOfBounds {
143 length: usize,
144 min: Option<u32>,
145 max: Option<u32>,
146 path: EurePath,
147 node_id: NodeId,
148 schema_node_id: SchemaNodeId,
149 },
150
151 #[error("String does not match pattern '{pattern}' at path {path}")]
152 PatternMismatch {
153 pattern: String,
154 path: EurePath,
155 node_id: NodeId,
156 schema_node_id: SchemaNodeId,
157 },
158
159 #[error("Array length {length} is out of bounds at path {path}")]
160 ArrayLengthOutOfBounds {
161 length: usize,
162 min: Option<u32>,
163 max: Option<u32>,
164 path: EurePath,
165 node_id: NodeId,
166 schema_node_id: SchemaNodeId,
167 },
168
169 #[error("Map size {size} is out of bounds at path {path}")]
170 MapSizeOutOfBounds {
171 size: usize,
172 min: Option<u32>,
173 max: Option<u32>,
174 path: EurePath,
175 node_id: NodeId,
176 schema_node_id: SchemaNodeId,
177 },
178
179 #[error("Tuple length mismatch: expected {expected}, got {actual} at path {path}")]
180 TupleLengthMismatch {
181 expected: usize,
182 actual: usize,
183 path: EurePath,
184 node_id: NodeId,
185 schema_node_id: SchemaNodeId,
186 },
187
188 #[error("Array elements must be unique at path {path}")]
189 ArrayNotUnique {
190 path: EurePath,
191 node_id: NodeId,
192 schema_node_id: SchemaNodeId,
193 },
194
195 #[error("Array must contain required element at path {path}")]
196 ArrayMissingContains {
197 path: EurePath,
198 node_id: NodeId,
199 schema_node_id: SchemaNodeId,
200 },
201
202 #[error("{}", format_no_variant_matched(path, best_match))]
211 NoVariantMatched {
212 path: EurePath,
213 best_match: Option<Box<BestVariantMatch>>,
215 node_id: NodeId,
216 schema_node_id: SchemaNodeId,
217 },
218
219 #[error("Multiple variants matched for union at path {path}: {variants:?}")]
220 AmbiguousUnion {
221 path: EurePath,
222 variants: Vec<String>,
223 node_id: NodeId,
224 schema_node_id: SchemaNodeId,
225 },
226
227 #[error("Invalid variant tag '{tag}' at path {path}")]
228 InvalidVariantTag {
229 tag: String,
230 path: EurePath,
231 node_id: NodeId,
232 schema_node_id: SchemaNodeId,
233 },
234
235 #[error("Conflicting variant tags: $variant = {explicit}, repr = {repr} at path {path}")]
236 ConflictingVariantTags {
237 explicit: String,
238 repr: String,
239 path: EurePath,
240 node_id: NodeId,
241 schema_node_id: SchemaNodeId,
242 },
243
244 #[error("Variant '{variant}' requires explicit $variant tag at path {path}")]
245 RequiresExplicitVariant {
246 variant: String,
247 path: EurePath,
248 node_id: NodeId,
249 schema_node_id: SchemaNodeId,
250 },
251
252 #[error("Literal value mismatch at path {path}")]
253 LiteralMismatch {
254 expected: String,
255 actual: String,
256 path: EurePath,
257 node_id: NodeId,
258 schema_node_id: SchemaNodeId,
259 },
260
261 #[error("Language mismatch: expected {expected}, got {actual} at path {path}")]
262 LanguageMismatch {
263 expected: String,
264 actual: String,
265 path: EurePath,
266 node_id: NodeId,
267 schema_node_id: SchemaNodeId,
268 },
269
270 #[error("Invalid key type at path {path}")]
271 InvalidKeyType {
272 key: ObjectKey,
274 path: EurePath,
275 node_id: NodeId,
276 schema_node_id: SchemaNodeId,
277 },
278
279 #[error("Integer not a multiple of {divisor} at path {path}")]
280 NotMultipleOf {
281 divisor: String,
282 path: EurePath,
283 node_id: NodeId,
284 schema_node_id: SchemaNodeId,
285 },
286
287 #[error("Undefined type reference '{name}' at path {path}")]
288 UndefinedTypeReference {
289 name: String,
290 path: EurePath,
291 node_id: NodeId,
292 schema_node_id: SchemaNodeId,
293 },
294
295 #[error(
296 "Invalid flatten target: expected Record, Union, or Map, got {actual_kind} at path {path}"
297 )]
298 InvalidFlattenTarget {
299 actual_kind: crate::SchemaKind,
301 path: EurePath,
302 node_id: NodeId,
303 schema_node_id: SchemaNodeId,
304 },
305
306 #[error("Flatten map key '{key}' does not match pattern at path {path}")]
307 FlattenMapKeyMismatch {
308 key: String,
310 pattern: Option<String>,
312 path: EurePath,
313 node_id: NodeId,
314 schema_node_id: SchemaNodeId,
315 },
316
317 #[error("Missing required extension '{extension}' at path {path}")]
318 MissingRequiredExtension {
319 extension: String,
320 path: EurePath,
321 node_id: NodeId,
322 schema_node_id: SchemaNodeId,
323 },
324
325 #[error("{}", format_parse_error(path, error))]
328 ParseError {
329 path: EurePath,
330 node_id: NodeId,
331 schema_node_id: SchemaNodeId,
332 error: eure_document::parse::ParseError,
333 },
334}
335
336fn format_missing_required_fields(fields: &[String], path: &EurePath) -> String {
338 match fields.len() {
339 1 => format!("Missing required field '{}' at path {}", fields[0], path),
340 _ => {
341 let field_list = fields
342 .iter()
343 .map(|f| format!("'{}'", f))
344 .collect::<Vec<_>>()
345 .join(", ");
346 format!("Missing required fields {} at path {}", field_list, path)
347 }
348 }
349}
350
351fn format_parse_error(path: &EurePath, error: &eure_document::parse::ParseError) -> String {
353 use eure_document::parse::ParseErrorKind;
354 match &error.kind {
355 ParseErrorKind::UnknownVariant(name) => {
356 format!("Invalid variant tag '{name}' at path {path}")
357 }
358 ParseErrorKind::ConflictingVariantTags { explicit, repr } => {
359 format!("Conflicting variant tags: $variant = {explicit}, repr = {repr} at path {path}")
360 }
361 ParseErrorKind::InvalidVariantType(kind) => {
362 format!("$variant must be a string, got {kind:?} at path {path}")
363 }
364 ParseErrorKind::InvalidVariantPath(path_str) => {
365 format!("Invalid $variant path syntax: '{path_str}' at path {path}")
366 }
367 _ => format!("{} at path {}", error.kind, path),
369 }
370}
371
372fn format_no_variant_matched(
378 path: &EurePath,
379 best_match: &Option<Box<BestVariantMatch>>,
380) -> String {
381 match best_match {
382 Some(best) => {
383 let is_nested_union = matches!(
385 best.error.as_ref(),
386 ValidationError::NoVariantMatched { .. }
387 );
388
389 if is_nested_union {
390 let mut msg = best.error.to_string();
392 if best.all_errors.len() > 1 {
393 msg.push_str(&format!(" (and {} more errors)", best.all_errors.len() - 1));
394 }
395 msg
396 } else {
397 let mut msg = best.error.to_string();
399 if best.all_errors.len() > 1 {
400 msg.push_str(&format!(" (and {} more errors)", best.all_errors.len() - 1));
401 }
402 msg.push_str(&format!(
403 " (based on nearest variant '{}' for union at path {})",
404 best.variant_name, path
405 ));
406 msg
407 }
408 }
409 None => format!("No variant matched for union at path {path}"),
410 }
411}
412
413impl ValidationError {
414 pub fn node_ids(&self) -> (NodeId, SchemaNodeId) {
416 match self {
417 Self::TypeMismatch {
418 node_id,
419 schema_node_id,
420 ..
421 }
422 | Self::MissingRequiredField {
423 node_id,
424 schema_node_id,
425 ..
426 }
427 | Self::UnknownField {
428 node_id,
429 schema_node_id,
430 ..
431 }
432 | Self::OutOfRange {
433 node_id,
434 schema_node_id,
435 ..
436 }
437 | Self::StringLengthOutOfBounds {
438 node_id,
439 schema_node_id,
440 ..
441 }
442 | Self::PatternMismatch {
443 node_id,
444 schema_node_id,
445 ..
446 }
447 | Self::ArrayLengthOutOfBounds {
448 node_id,
449 schema_node_id,
450 ..
451 }
452 | Self::MapSizeOutOfBounds {
453 node_id,
454 schema_node_id,
455 ..
456 }
457 | Self::TupleLengthMismatch {
458 node_id,
459 schema_node_id,
460 ..
461 }
462 | Self::ArrayNotUnique {
463 node_id,
464 schema_node_id,
465 ..
466 }
467 | Self::ArrayMissingContains {
468 node_id,
469 schema_node_id,
470 ..
471 }
472 | Self::NoVariantMatched {
473 node_id,
474 schema_node_id,
475 ..
476 }
477 | Self::AmbiguousUnion {
478 node_id,
479 schema_node_id,
480 ..
481 }
482 | Self::InvalidVariantTag {
483 node_id,
484 schema_node_id,
485 ..
486 }
487 | Self::ConflictingVariantTags {
488 node_id,
489 schema_node_id,
490 ..
491 }
492 | Self::RequiresExplicitVariant {
493 node_id,
494 schema_node_id,
495 ..
496 }
497 | Self::LiteralMismatch {
498 node_id,
499 schema_node_id,
500 ..
501 }
502 | Self::LanguageMismatch {
503 node_id,
504 schema_node_id,
505 ..
506 }
507 | Self::InvalidKeyType {
508 node_id,
509 schema_node_id,
510 ..
511 }
512 | Self::NotMultipleOf {
513 node_id,
514 schema_node_id,
515 ..
516 }
517 | Self::UndefinedTypeReference {
518 node_id,
519 schema_node_id,
520 ..
521 }
522 | Self::InvalidFlattenTarget {
523 node_id,
524 schema_node_id,
525 ..
526 }
527 | Self::FlattenMapKeyMismatch {
528 node_id,
529 schema_node_id,
530 ..
531 }
532 | Self::MissingRequiredExtension {
533 node_id,
534 schema_node_id,
535 ..
536 }
537 | Self::ParseError {
538 node_id,
539 schema_node_id,
540 ..
541 } => (*node_id, *schema_node_id),
542 }
543 }
544
545 pub fn deepest_error(&self) -> &ValidationError {
553 match self {
554 Self::NoVariantMatched {
555 best_match: Some(best),
556 ..
557 } => {
558 match best.error.as_ref() {
560 Self::NoVariantMatched { .. } => best.error.deepest_error(),
562 Self::TypeMismatch { .. }
564 | Self::LiteralMismatch { .. }
565 | Self::LanguageMismatch { .. }
566 | Self::OutOfRange { .. }
567 | Self::NotMultipleOf { .. }
568 | Self::PatternMismatch { .. }
569 | Self::StringLengthOutOfBounds { .. }
570 | Self::InvalidKeyType { .. }
571 | Self::UnknownField { .. } => best.error.deepest_error(),
572 _ => self,
574 }
575 }
576 _ => self,
577 }
578 }
579
580 pub fn depth(&self) -> usize {
585 match self {
586 Self::TypeMismatch { path, .. }
587 | Self::MissingRequiredField { path, .. }
588 | Self::UnknownField { path, .. }
589 | Self::OutOfRange { path, .. }
590 | Self::StringLengthOutOfBounds { path, .. }
591 | Self::PatternMismatch { path, .. }
592 | Self::ArrayLengthOutOfBounds { path, .. }
593 | Self::MapSizeOutOfBounds { path, .. }
594 | Self::TupleLengthMismatch { path, .. }
595 | Self::ArrayNotUnique { path, .. }
596 | Self::ArrayMissingContains { path, .. }
597 | Self::NoVariantMatched { path, .. }
598 | Self::AmbiguousUnion { path, .. }
599 | Self::InvalidVariantTag { path, .. }
600 | Self::ConflictingVariantTags { path, .. }
601 | Self::RequiresExplicitVariant { path, .. }
602 | Self::LiteralMismatch { path, .. }
603 | Self::LanguageMismatch { path, .. }
604 | Self::InvalidKeyType { path, .. }
605 | Self::NotMultipleOf { path, .. }
606 | Self::UndefinedTypeReference { path, .. }
607 | Self::InvalidFlattenTarget { path, .. }
608 | Self::FlattenMapKeyMismatch { path, .. }
609 | Self::MissingRequiredExtension { path, .. }
610 | Self::ParseError { path, .. } => path.0.len(),
611 }
612 }
613
614 pub fn priority_score(&self) -> u8 {
619 match self {
620 Self::UnknownField { .. } => 95,
624 Self::MissingRequiredField { .. } => 90,
625 Self::TypeMismatch { .. } => 80,
626 Self::TupleLengthMismatch { .. } => 70,
627 Self::LiteralMismatch { .. } => 70,
628 Self::InvalidVariantTag { .. } => 65,
629 Self::NoVariantMatched { .. } => 60, Self::MissingRequiredExtension { .. } => 50,
631 Self::ParseError { .. } => 40, Self::OutOfRange { .. } => 30,
633 Self::StringLengthOutOfBounds { .. } => 30,
634 Self::PatternMismatch { .. } => 30,
635 Self::FlattenMapKeyMismatch { .. } => 30, Self::ArrayLengthOutOfBounds { .. } => 30,
637 Self::MapSizeOutOfBounds { .. } => 30,
638 Self::NotMultipleOf { .. } => 30,
639 Self::ArrayNotUnique { .. } => 25,
640 Self::ArrayMissingContains { .. } => 25,
641 Self::InvalidKeyType { .. } => 20,
642 Self::LanguageMismatch { .. } => 20,
643 Self::AmbiguousUnion { .. } => 0, Self::ConflictingVariantTags { .. } => 0, Self::UndefinedTypeReference { .. } => 0, Self::InvalidFlattenTarget { .. } => 0, Self::RequiresExplicitVariant { .. } => 0, }
649 }
650}
651
652#[derive(Debug, Clone, PartialEq)]
658pub enum ValidationWarning {
659 UnknownExtension { name: String, path: EurePath },
661 DeprecatedField { field: String, path: EurePath },
663}
664
665fn compute_depth_and_structural_match(errors: &[ValidationError]) -> (usize, bool) {
684 let min_depth = errors.iter().map(|e| e.depth()).min().unwrap_or(0);
686
687 let mut max_depth = 0;
688 let mut structural_match = true;
689
690 for error in errors {
691 match error {
692 ValidationError::NoVariantMatched { best_match, .. } => {
694 if let Some(best) = best_match {
695 let (nested_depth, nested_structural) =
697 compute_depth_and_structural_match(&best.all_errors);
698 max_depth = max_depth.max(nested_depth);
699 if !nested_structural {
701 structural_match = false;
702 }
703 }
704 }
705 ValidationError::TypeMismatch { .. } if error.depth() == min_depth => {
707 max_depth = max_depth.max(error.depth());
708 structural_match = false;
709 }
710 _ => {
712 max_depth = max_depth.max(error.depth());
713 }
714 }
715 }
716
717 (max_depth, structural_match)
718}
719
720pub fn select_best_variant_match(
732 variant_errors: Vec<(String, SchemaNodeId, Vec<ValidationError>)>,
733) -> Option<BestVariantMatch> {
734 if variant_errors.is_empty() {
735 return None;
736 }
737
738 let best = variant_errors
740 .into_iter()
741 .filter(|(_, _, errors)| !errors.is_empty())
742 .max_by_key(|(_, _, errors)| {
743 let (max_depth, structural_match) = compute_depth_and_structural_match(errors);
745 let error_count = errors.len();
746 let max_priority = errors.iter().map(|e| e.priority_score()).max().unwrap_or(0);
747
748 (
754 structural_match,
755 max_depth,
756 usize::MAX - error_count,
757 max_priority,
758 )
759 });
760
761 best.map(|(variant_name, variant_schema_id, mut errors)| {
762 let depth = errors.iter().map(|e| e.depth()).max().unwrap_or(0);
763 let error_count = errors.len();
764
765 errors.sort_by_key(|e| {
767 (
768 std::cmp::Reverse(e.priority_score()),
769 std::cmp::Reverse(e.depth()),
770 )
771 });
772 let primary_error = errors.first().cloned().unwrap();
773
774 BestVariantMatch {
775 variant_name,
776 variant_schema_id,
777 error: Box::new(primary_error),
778 all_errors: errors,
779 depth,
780 error_count,
781 }
782 })
783}