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