1use eure_document::document::NodeId;
8use eure_document::parse::{BestParseVariantMatch, ParseError, UnionParseError};
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
60impl UnionParseError for ValidatorError {
61 fn as_parse_error(&self) -> Option<&ParseError> {
62 ValidatorError::as_parse_error(self)
63 }
64
65 fn from_no_matching_variant(
66 _node_id: NodeId,
67 variant: Option<String>,
68 _best_match: Option<BestParseVariantMatch>,
69 failures: &[(String, Self)],
70 ) -> Self {
71 if failures
72 .iter()
73 .any(|(_, error)| matches!(error, ValidatorError::InnerErrorsPropagated))
74 {
75 return ValidatorError::InnerErrorsPropagated;
76 }
77 ValidatorError::InvalidVariantTag {
78 tag: variant.unwrap_or_default(),
79 reason: "type mismatch".to_string(),
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq)]
107pub struct BestVariantMatch {
108 pub variant_name: String,
110 pub variant_schema_id: SchemaNodeId,
112 pub error: Box<ValidationError>,
114 pub all_errors: Vec<ValidationError>,
116 pub depth: usize,
118 pub error_count: usize,
120}
121
122#[derive(Debug, Clone, Error, PartialEq)]
131pub enum ValidationError {
132 #[error("Type mismatch: expected {expected}, got {actual} at path {path}")]
133 TypeMismatch {
134 expected: String,
135 actual: String,
136 path: EurePath,
137 node_id: NodeId,
138 schema_node_id: SchemaNodeId,
139 },
140
141 #[error("{}", format_missing_required_fields(fields, path))]
142 MissingRequiredField {
143 fields: Vec<String>,
144 path: EurePath,
145 node_id: NodeId,
146 schema_node_id: SchemaNodeId,
147 },
148
149 #[error("Unknown field '{field}' at path {path}")]
150 UnknownField {
151 field: String,
152 path: EurePath,
153 node_id: NodeId,
154 schema_node_id: SchemaNodeId,
155 },
156
157 #[error("Value {value} is out of range at path {path}")]
158 OutOfRange {
159 value: String,
160 path: EurePath,
161 node_id: NodeId,
162 schema_node_id: SchemaNodeId,
163 },
164
165 #[error("String length {length} is out of bounds at path {path}")]
166 StringLengthOutOfBounds {
167 length: usize,
168 min: Option<u32>,
169 max: Option<u32>,
170 path: EurePath,
171 node_id: NodeId,
172 schema_node_id: SchemaNodeId,
173 },
174
175 #[error("String does not match pattern '{pattern}' at path {path}")]
176 PatternMismatch {
177 pattern: String,
178 path: EurePath,
179 node_id: NodeId,
180 schema_node_id: SchemaNodeId,
181 },
182
183 #[error("Array length {length} is out of bounds at path {path}")]
184 ArrayLengthOutOfBounds {
185 length: usize,
186 min: Option<u32>,
187 max: Option<u32>,
188 path: EurePath,
189 node_id: NodeId,
190 schema_node_id: SchemaNodeId,
191 },
192
193 #[error("Map size {size} is out of bounds at path {path}")]
194 MapSizeOutOfBounds {
195 size: usize,
196 min: Option<u32>,
197 max: Option<u32>,
198 path: EurePath,
199 node_id: NodeId,
200 schema_node_id: SchemaNodeId,
201 },
202
203 #[error("Tuple length mismatch: expected {expected}, got {actual} at path {path}")]
204 TupleLengthMismatch {
205 expected: usize,
206 actual: usize,
207 path: EurePath,
208 node_id: NodeId,
209 schema_node_id: SchemaNodeId,
210 },
211
212 #[error("Array elements must be unique at path {path}")]
213 ArrayNotUnique {
214 path: EurePath,
215 node_id: NodeId,
216 schema_node_id: SchemaNodeId,
217 },
218
219 #[error("Array must contain required element at path {path}")]
220 ArrayMissingContains {
221 path: EurePath,
222 node_id: NodeId,
223 schema_node_id: SchemaNodeId,
224 },
225
226 #[error("{}", format_no_variant_matched(path, best_match))]
235 NoVariantMatched {
236 path: EurePath,
237 best_match: Option<Box<BestVariantMatch>>,
239 node_id: NodeId,
240 schema_node_id: SchemaNodeId,
241 },
242
243 #[error("Multiple variants matched for union at path {path}: {variants:?}")]
244 AmbiguousUnion {
245 path: EurePath,
246 variants: Vec<String>,
247 node_id: NodeId,
248 schema_node_id: SchemaNodeId,
249 },
250
251 #[error("Invalid variant tag '{tag}' at path {path}")]
252 InvalidVariantTag {
253 tag: String,
254 path: EurePath,
255 node_id: NodeId,
256 schema_node_id: SchemaNodeId,
257 },
258
259 #[error("Conflicting variant tags: $variant = {explicit}, repr = {repr} at path {path}")]
260 ConflictingVariantTags {
261 explicit: String,
262 repr: String,
263 path: EurePath,
264 node_id: NodeId,
265 schema_node_id: SchemaNodeId,
266 },
267
268 #[error("Variant '{variant}' requires explicit $variant tag at path {path}")]
269 RequiresExplicitVariant {
270 variant: String,
271 path: EurePath,
272 node_id: NodeId,
273 schema_node_id: SchemaNodeId,
274 },
275
276 #[error("Literal value mismatch at path {path}")]
277 LiteralMismatch {
278 expected: String,
279 actual: String,
280 path: EurePath,
281 node_id: NodeId,
282 schema_node_id: SchemaNodeId,
283 },
284
285 #[error("Language mismatch: expected {expected}, got {actual} at path {path}")]
286 LanguageMismatch {
287 expected: String,
288 actual: String,
289 path: EurePath,
290 node_id: NodeId,
291 schema_node_id: SchemaNodeId,
292 },
293
294 #[error("Invalid key type at path {path}")]
295 InvalidKeyType {
296 key: ObjectKey,
298 path: EurePath,
299 node_id: NodeId,
300 schema_node_id: SchemaNodeId,
301 },
302
303 #[error("Integer not a multiple of {divisor} at path {path}")]
304 NotMultipleOf {
305 divisor: String,
306 path: EurePath,
307 node_id: NodeId,
308 schema_node_id: SchemaNodeId,
309 },
310
311 #[error("Undefined type reference '{name}' at path {path}")]
312 UndefinedTypeReference {
313 name: String,
314 path: EurePath,
315 node_id: NodeId,
316 schema_node_id: SchemaNodeId,
317 },
318
319 #[error(
320 "Invalid flatten target: expected Record, Union, or Map, got {actual_kind} at path {path}"
321 )]
322 InvalidFlattenTarget {
323 actual_kind: crate::SchemaKind,
325 path: EurePath,
326 node_id: NodeId,
327 schema_node_id: SchemaNodeId,
328 },
329
330 #[error("Flatten map key '{key}' does not match pattern at path {path}")]
331 FlattenMapKeyMismatch {
332 key: String,
334 pattern: Option<String>,
336 path: EurePath,
337 node_id: NodeId,
338 schema_node_id: SchemaNodeId,
339 },
340
341 #[error("Missing required extension '{extension}' at path {path}")]
342 MissingRequiredExtension {
343 extension: String,
344 path: EurePath,
345 node_id: NodeId,
346 schema_node_id: SchemaNodeId,
347 },
348
349 #[error("{}", format_parse_error(path, error))]
352 ParseError {
353 path: EurePath,
354 node_id: NodeId,
355 schema_node_id: SchemaNodeId,
356 error: eure_document::parse::ParseError,
357 },
358}
359
360fn format_missing_required_fields(fields: &[String], path: &EurePath) -> String {
362 match fields.len() {
363 1 => format!("Missing required field '{}' at path {}", fields[0], path),
364 _ => {
365 let field_list = fields
366 .iter()
367 .map(|f| format!("'{}'", f))
368 .collect::<Vec<_>>()
369 .join(", ");
370 format!("Missing required fields {} at path {}", field_list, path)
371 }
372 }
373}
374
375fn format_parse_error(path: &EurePath, error: &eure_document::parse::ParseError) -> String {
377 use eure_document::parse::ParseErrorKind;
378 match &error.kind {
379 ParseErrorKind::UnknownVariant(name) => {
380 format!("Invalid variant tag '{name}' at path {path}")
381 }
382 ParseErrorKind::ConflictingVariantTags { explicit, repr } => {
383 format!("Conflicting variant tags: $variant = {explicit}, repr = {repr} at path {path}")
384 }
385 ParseErrorKind::InvalidVariantType(kind) => {
386 format!("$variant must be a string, got {kind:?} at path {path}")
387 }
388 ParseErrorKind::InvalidVariantPath(path_str) => {
389 format!("Invalid $variant path syntax: '{path_str}' at path {path}")
390 }
391 _ => format!("{} at path {}", error.kind, path),
393 }
394}
395
396fn format_no_variant_matched(
402 path: &EurePath,
403 best_match: &Option<Box<BestVariantMatch>>,
404) -> String {
405 match best_match {
406 Some(best) => {
407 let is_nested_union = matches!(
409 best.error.as_ref(),
410 ValidationError::NoVariantMatched { .. }
411 );
412
413 if is_nested_union {
414 let mut msg = best.error.to_string();
416 if best.all_errors.len() > 1 {
417 msg.push_str(&format!(" (and {} more errors)", best.all_errors.len() - 1));
418 }
419 msg
420 } else {
421 let mut msg = best.error.to_string();
423 if best.all_errors.len() > 1 {
424 msg.push_str(&format!(" (and {} more errors)", best.all_errors.len() - 1));
425 }
426 msg.push_str(&format!(
427 " (based on nearest variant '{}' for union at path {})",
428 best.variant_name, path
429 ));
430 msg
431 }
432 }
433 None => format!("No variant matched for union at path {path}"),
434 }
435}
436
437impl ValidationError {
438 pub fn node_ids(&self) -> (NodeId, SchemaNodeId) {
440 match self {
441 Self::TypeMismatch {
442 node_id,
443 schema_node_id,
444 ..
445 }
446 | Self::MissingRequiredField {
447 node_id,
448 schema_node_id,
449 ..
450 }
451 | Self::UnknownField {
452 node_id,
453 schema_node_id,
454 ..
455 }
456 | Self::OutOfRange {
457 node_id,
458 schema_node_id,
459 ..
460 }
461 | Self::StringLengthOutOfBounds {
462 node_id,
463 schema_node_id,
464 ..
465 }
466 | Self::PatternMismatch {
467 node_id,
468 schema_node_id,
469 ..
470 }
471 | Self::ArrayLengthOutOfBounds {
472 node_id,
473 schema_node_id,
474 ..
475 }
476 | Self::MapSizeOutOfBounds {
477 node_id,
478 schema_node_id,
479 ..
480 }
481 | Self::TupleLengthMismatch {
482 node_id,
483 schema_node_id,
484 ..
485 }
486 | Self::ArrayNotUnique {
487 node_id,
488 schema_node_id,
489 ..
490 }
491 | Self::ArrayMissingContains {
492 node_id,
493 schema_node_id,
494 ..
495 }
496 | Self::NoVariantMatched {
497 node_id,
498 schema_node_id,
499 ..
500 }
501 | Self::AmbiguousUnion {
502 node_id,
503 schema_node_id,
504 ..
505 }
506 | Self::InvalidVariantTag {
507 node_id,
508 schema_node_id,
509 ..
510 }
511 | Self::ConflictingVariantTags {
512 node_id,
513 schema_node_id,
514 ..
515 }
516 | Self::RequiresExplicitVariant {
517 node_id,
518 schema_node_id,
519 ..
520 }
521 | Self::LiteralMismatch {
522 node_id,
523 schema_node_id,
524 ..
525 }
526 | Self::LanguageMismatch {
527 node_id,
528 schema_node_id,
529 ..
530 }
531 | Self::InvalidKeyType {
532 node_id,
533 schema_node_id,
534 ..
535 }
536 | Self::NotMultipleOf {
537 node_id,
538 schema_node_id,
539 ..
540 }
541 | Self::UndefinedTypeReference {
542 node_id,
543 schema_node_id,
544 ..
545 }
546 | Self::InvalidFlattenTarget {
547 node_id,
548 schema_node_id,
549 ..
550 }
551 | Self::FlattenMapKeyMismatch {
552 node_id,
553 schema_node_id,
554 ..
555 }
556 | Self::MissingRequiredExtension {
557 node_id,
558 schema_node_id,
559 ..
560 }
561 | Self::ParseError {
562 node_id,
563 schema_node_id,
564 ..
565 } => (*node_id, *schema_node_id),
566 }
567 }
568
569 pub fn deepest_error(&self) -> &ValidationError {
577 match self {
578 Self::NoVariantMatched {
579 best_match: Some(best),
580 ..
581 } => {
582 match best.error.as_ref() {
584 Self::NoVariantMatched { .. } => best.error.deepest_error(),
586 Self::TypeMismatch { .. }
588 | Self::LiteralMismatch { .. }
589 | Self::LanguageMismatch { .. }
590 | Self::OutOfRange { .. }
591 | Self::NotMultipleOf { .. }
592 | Self::PatternMismatch { .. }
593 | Self::StringLengthOutOfBounds { .. }
594 | Self::InvalidKeyType { .. }
595 | Self::UnknownField { .. } => best.error.deepest_error(),
596 _ => self,
598 }
599 }
600 _ => self,
601 }
602 }
603
604 pub fn depth(&self) -> usize {
609 match self {
610 Self::TypeMismatch { path, .. }
611 | Self::MissingRequiredField { path, .. }
612 | Self::UnknownField { path, .. }
613 | Self::OutOfRange { path, .. }
614 | Self::StringLengthOutOfBounds { path, .. }
615 | Self::PatternMismatch { path, .. }
616 | Self::ArrayLengthOutOfBounds { path, .. }
617 | Self::MapSizeOutOfBounds { path, .. }
618 | Self::TupleLengthMismatch { path, .. }
619 | Self::ArrayNotUnique { path, .. }
620 | Self::ArrayMissingContains { path, .. }
621 | Self::NoVariantMatched { path, .. }
622 | Self::AmbiguousUnion { path, .. }
623 | Self::InvalidVariantTag { path, .. }
624 | Self::ConflictingVariantTags { path, .. }
625 | Self::RequiresExplicitVariant { path, .. }
626 | Self::LiteralMismatch { path, .. }
627 | Self::LanguageMismatch { path, .. }
628 | Self::InvalidKeyType { path, .. }
629 | Self::NotMultipleOf { path, .. }
630 | Self::UndefinedTypeReference { path, .. }
631 | Self::InvalidFlattenTarget { path, .. }
632 | Self::FlattenMapKeyMismatch { path, .. }
633 | Self::MissingRequiredExtension { path, .. }
634 | Self::ParseError { path, .. } => path.0.len(),
635 }
636 }
637
638 pub fn priority_score(&self) -> u8 {
643 match self {
644 Self::UnknownField { .. } => 95,
648 Self::MissingRequiredField { .. } => 90,
649 Self::TypeMismatch { .. } => 80,
650 Self::TupleLengthMismatch { .. } => 70,
651 Self::LiteralMismatch { .. } => 70,
652 Self::InvalidVariantTag { .. } => 65,
653 Self::NoVariantMatched { .. } => 60, Self::MissingRequiredExtension { .. } => 50,
655 Self::ParseError { .. } => 40, Self::OutOfRange { .. } => 30,
657 Self::StringLengthOutOfBounds { .. } => 30,
658 Self::PatternMismatch { .. } => 30,
659 Self::FlattenMapKeyMismatch { .. } => 30, Self::ArrayLengthOutOfBounds { .. } => 30,
661 Self::MapSizeOutOfBounds { .. } => 30,
662 Self::NotMultipleOf { .. } => 30,
663 Self::ArrayNotUnique { .. } => 25,
664 Self::ArrayMissingContains { .. } => 25,
665 Self::InvalidKeyType { .. } => 20,
666 Self::LanguageMismatch { .. } => 20,
667 Self::AmbiguousUnion { .. } => 0, Self::ConflictingVariantTags { .. } => 0, Self::UndefinedTypeReference { .. } => 0, Self::InvalidFlattenTarget { .. } => 0, Self::RequiresExplicitVariant { .. } => 0, }
673 }
674}
675
676#[derive(Debug, Clone, PartialEq)]
682pub enum ValidationWarning {
683 UnknownExtension { name: String, path: EurePath },
685 DeprecatedField { field: String, path: EurePath },
687}
688
689fn compute_depth_and_structural_match(errors: &[ValidationError]) -> (usize, bool) {
708 let min_depth = errors.iter().map(|e| e.depth()).min().unwrap_or(0);
710
711 let mut max_depth = 0;
712 let mut structural_match = true;
713
714 for error in errors {
715 match error {
716 ValidationError::NoVariantMatched { best_match, .. } => {
718 if let Some(best) = best_match {
719 let (nested_depth, nested_structural) =
721 compute_depth_and_structural_match(&best.all_errors);
722 max_depth = max_depth.max(nested_depth);
723 if !nested_structural {
725 structural_match = false;
726 }
727 }
728 }
729 ValidationError::TypeMismatch { .. } if error.depth() == min_depth => {
731 max_depth = max_depth.max(error.depth());
732 structural_match = false;
733 }
734 _ => {
736 max_depth = max_depth.max(error.depth());
737 }
738 }
739 }
740
741 (max_depth, structural_match)
742}
743
744pub fn select_best_variant_match(
756 variant_errors: Vec<(String, SchemaNodeId, Vec<ValidationError>)>,
757) -> Option<BestVariantMatch> {
758 if variant_errors.is_empty() {
759 return None;
760 }
761
762 let best = variant_errors
764 .into_iter()
765 .filter(|(_, _, errors)| !errors.is_empty())
766 .max_by_key(|(_, _, errors)| {
767 let (max_depth, structural_match) = compute_depth_and_structural_match(errors);
769 let error_count = errors.len();
770 let max_priority = errors.iter().map(|e| e.priority_score()).max().unwrap_or(0);
771
772 (
778 structural_match,
779 max_depth,
780 usize::MAX - error_count,
781 max_priority,
782 )
783 });
784
785 best.map(|(variant_name, variant_schema_id, mut errors)| {
786 let depth = errors.iter().map(|e| e.depth()).max().unwrap_or(0);
787 let error_count = errors.len();
788
789 errors.sort_by_key(|e| {
791 (
792 std::cmp::Reverse(e.priority_score()),
793 std::cmp::Reverse(e.depth()),
794 )
795 });
796 let primary_error = errors.first().cloned().unwrap();
797
798 BestVariantMatch {
799 variant_name,
800 variant_schema_id,
801 error: Box::new(primary_error),
802 all_errors: errors,
803 depth,
804 error_count,
805 }
806 })
807}