1use std::fmt;
48use thiserror::Error;
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Location {
53 pub line: usize,
55 pub column: usize,
57 pub byte_offset: usize,
59}
60
61impl Location {
62 #[must_use]
64 pub fn new(line: usize, column: usize, byte_offset: usize) -> Self {
65 Self {
66 line,
67 column,
68 byte_offset,
69 }
70 }
71}
72
73impl fmt::Display for Location {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(f, "line {}, column {}", self.line, self.column)
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct Span {
82 pub start: Location,
84 pub end: Location,
86}
87
88impl Span {
89 #[must_use]
91 pub fn new(start: Location, end: Location) -> Self {
92 Self { start, end }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ErrorContext {
102 pub location: Option<Location>,
104 pub snippet: Option<String>,
106}
107
108impl ErrorContext {
109 #[must_use]
111 pub fn new(location: Option<Location>, snippet: Option<String>) -> Self {
112 Self { location, snippet }
113 }
114
115 #[must_use]
117 pub fn boxed(location: Option<Location>, snippet: Option<String>) -> Option<Box<ErrorContext>> {
118 if location.is_none() && snippet.is_none() {
119 None
120 } else {
121 Some(Box::new(ErrorContext { location, snippet }))
122 }
123 }
124}
125
126#[derive(Error, Debug, Clone, PartialEq)]
128pub enum YamlError {
129 #[error("YAML parse error: {message}")]
131 ParseError {
132 message: String,
134 context: Option<Box<ErrorContext>>,
136 },
137
138 #[error("Root must be a YAML mapping, found {found}")]
140 InvalidRootType {
141 found: String,
143 context: Option<Box<ErrorContext>>,
145 },
146
147 #[error("Non-string keys not supported, found {key_type} at path {path}")]
149 NonStringKey {
150 key_type: String,
152 path: String,
154 context: Option<Box<ErrorContext>>,
156 },
157
158 #[error("Invalid number format: {value}")]
160 InvalidNumber {
161 value: String,
163 context: Option<Box<ErrorContext>>,
165 },
166
167 #[error("Invalid expression: {message}")]
169 InvalidExpression {
170 message: String,
172 context: Option<Box<ErrorContext>>,
174 },
175
176 #[error("Invalid reference format: {message}")]
178 InvalidReference {
179 message: String,
181 context: Option<Box<ErrorContext>>,
183 },
184
185 #[error("Nested objects not allowed in scalar context at path {path}")]
187 NestedObjectInScalar {
188 path: String,
190 context: Option<Box<ErrorContext>>,
192 },
193
194 #[error("Invalid tensor element at path {path}: must be number or sequence")]
196 InvalidTensorElement {
197 path: String,
199 expected: String,
201 found: String,
203 context: Option<Box<ErrorContext>>,
205 },
206
207 #[error("Resource limit exceeded: {limit_type} (limit: {limit}, actual: {actual})")]
209 ResourceLimitExceeded {
210 limit_type: String,
212 limit: usize,
214 actual: usize,
216 context: Option<Box<ErrorContext>>,
218 },
219
220 #[error(
222 "Maximum nesting depth of {max_depth} exceeded at depth {actual_depth} at path {path}"
223 )]
224 MaxDepthExceeded {
225 max_depth: usize,
227 actual_depth: usize,
229 path: String,
231 context: Option<Box<ErrorContext>>,
233 },
234
235 #[error("Document size {size} bytes exceeds maximum of {max_size} bytes")]
237 DocumentTooLarge {
238 size: usize,
240 max_size: usize,
242 context: Option<Box<ErrorContext>>,
244 },
245
246 #[error("Array length {length} exceeds maximum of {max_length} at path {path}")]
248 ArrayTooLong {
249 length: usize,
251 max_length: usize,
253 path: String,
255 context: Option<Box<ErrorContext>>,
257 },
258
259 #[error("Conversion error: {message}")]
261 Conversion {
262 message: String,
264 context: Option<Box<ErrorContext>>,
266 },
267
268 #[error("Forward reference: alias '*{alias}' at line {line} references undefined anchor")]
270 ForwardReference {
271 alias: String,
273 line: usize,
275 },
276
277 #[error("Circular anchor reference: {cycle_path}")]
279 CircularReference {
280 cycle_path: String,
282 anchors: Vec<String>,
284 locations: Vec<usize>,
286 },
287
288 #[error("Invalid anchor name '{name}': {reason}")]
290 InvalidAnchorName {
291 name: String,
293 reason: String,
295 },
296
297 #[error(
299 "Anchor '{name}' redefined at line {new_line} (previously defined at line {old_line})"
300 )]
301 AnchorRedefinition {
302 name: String,
304 old_line: usize,
306 new_line: usize,
308 },
309}
310
311impl YamlError {
312 #[must_use]
314 pub fn location(&self) -> Option<&Location> {
315 match self {
316 Self::ParseError { context, .. }
317 | Self::InvalidRootType { context, .. }
318 | Self::NonStringKey { context, .. }
319 | Self::InvalidNumber { context, .. }
320 | Self::InvalidExpression { context, .. }
321 | Self::InvalidReference { context, .. }
322 | Self::NestedObjectInScalar { context, .. }
323 | Self::InvalidTensorElement { context, .. }
324 | Self::ResourceLimitExceeded { context, .. }
325 | Self::MaxDepthExceeded { context, .. }
326 | Self::DocumentTooLarge { context, .. }
327 | Self::ArrayTooLong { context, .. }
328 | Self::Conversion { context, .. } => {
329 context.as_ref().and_then(|c| c.location.as_ref())
330 }
331 Self::ForwardReference { .. }
333 | Self::CircularReference { .. }
334 | Self::InvalidAnchorName { .. }
335 | Self::AnchorRedefinition { .. } => None,
336 }
337 }
338
339 #[must_use]
341 pub fn snippet(&self) -> Option<&str> {
342 match self {
343 Self::ParseError { context, .. }
344 | Self::InvalidRootType { context, .. }
345 | Self::NonStringKey { context, .. }
346 | Self::InvalidNumber { context, .. }
347 | Self::InvalidExpression { context, .. }
348 | Self::InvalidReference { context, .. }
349 | Self::NestedObjectInScalar { context, .. }
350 | Self::InvalidTensorElement { context, .. }
351 | Self::ResourceLimitExceeded { context, .. }
352 | Self::MaxDepthExceeded { context, .. }
353 | Self::DocumentTooLarge { context, .. }
354 | Self::ArrayTooLong { context, .. }
355 | Self::Conversion { context, .. } => {
356 context.as_ref().and_then(|c| c.snippet.as_deref())
357 }
358 Self::ForwardReference { .. }
360 | Self::CircularReference { .. }
361 | Self::InvalidAnchorName { .. }
362 | Self::AnchorRedefinition { .. } => None,
363 }
364 }
365
366 #[must_use]
368 pub fn path(&self) -> Option<&str> {
369 match self {
370 Self::NonStringKey { path, .. }
371 | Self::NestedObjectInScalar { path, .. }
372 | Self::InvalidTensorElement { path, .. }
373 | Self::MaxDepthExceeded { path, .. }
374 | Self::ArrayTooLong { path, .. } => Some(path),
375 _ => None,
376 }
377 }
378
379 #[must_use]
381 pub fn suggestions(&self) -> Vec<String> {
382 match self {
383 Self::ParseError { .. } => vec![
384 "Check YAML syntax for missing or extra colons, brackets, or quotes".to_string(),
385 "Ensure proper indentation (YAML is whitespace-sensitive)".to_string(),
386 "Verify that strings with special characters are quoted".to_string(),
387 ],
388 Self::InvalidRootType { found, .. } => vec![
389 format!("Expected a YAML mapping at the root, but found {}", found),
390 "HEDL documents must start with a mapping (key-value pairs)".to_string(),
391 "Example:\nname: value\ncount: 42".to_string(),
392 ],
393 Self::NonStringKey { key_type, .. } => vec![
394 format!("YAML keys must be strings, but found {}", key_type),
395 "Convert the key to a string by wrapping it in quotes".to_string(),
396 "Example: \"123\": value".to_string(),
397 ],
398 Self::InvalidNumber { value, .. } => vec![
399 format!("The value '{}' is not a valid number", value),
400 "Ensure numbers are in a valid format (e.g., 42, 3.14, -10)".to_string(),
401 ],
402 Self::InvalidExpression { .. } => vec![
403 "Expression syntax must be $(...)".to_string(),
404 "Example: $(add(x, 1))".to_string(),
405 "Check for balanced parentheses and valid identifiers".to_string(),
406 ],
407 Self::InvalidReference { .. } => vec![
408 "Reference format must be @id or @Type:id".to_string(),
409 "Use mapping format: { \"@ref\": \"@user1\" }".to_string(),
410 "Example: { \"@ref\": \"@User:user1\" }".to_string(),
411 ],
412 Self::NestedObjectInScalar { .. } => vec![
413 "Nested objects are not allowed in this context".to_string(),
414 "Consider moving the object to a separate field or list".to_string(),
415 ],
416 Self::InvalidTensorElement {
417 expected, found, ..
418 } => vec![
419 format!("Tensor elements must be {}, but found {}", expected, found),
420 "Ensure all array elements are numbers or nested arrays of numbers".to_string(),
421 "Example: [1, 2, 3] or [[1, 2], [3, 4]]".to_string(),
422 ],
423 Self::ResourceLimitExceeded {
424 limit_type,
425 limit,
426 actual,
427 ..
428 } => vec![
429 format!("{} is {}, exceeding limit of {}", limit_type, actual, limit),
430 "Consider reducing the size or increasing the limit".to_string(),
431 ],
432 Self::MaxDepthExceeded { max_depth, .. } => vec![
433 format!("Maximum nesting depth is {}", max_depth),
434 "Reduce nesting levels or increase max_nesting_depth in config".to_string(),
435 format!(
436 "Use FromYamlConfig::builder().max_nesting_depth({}).build()",
437 max_depth * 2
438 ),
439 ],
440 Self::DocumentTooLarge { max_size, .. } => vec![
441 format!("Maximum document size is {} bytes", max_size),
442 "Split the document into smaller files or increase max_document_size".to_string(),
443 "Use FromYamlConfig::builder().max_document_size(N).build()".to_string(),
444 ],
445 Self::ArrayTooLong {
446 max_length, length, ..
447 } => vec![
448 format!(
449 "Array has {} elements, exceeding limit of {}",
450 length, max_length
451 ),
452 format!(
453 "Consider splitting into smaller arrays or increasing max_array_length to {}",
454 length
455 ),
456 "Use FromYamlConfig::builder().max_array_length(N).build()".to_string(),
457 ],
458 Self::Conversion { .. } => {
459 vec!["Check that the YAML structure matches the expected HEDL format".to_string()]
460 }
461 Self::ForwardReference { alias, .. } => vec![
462 format!(
463 "The alias '*{}' references an anchor that hasn't been defined yet",
464 alias
465 ),
466 "Define the anchor before using it as an alias".to_string(),
467 "Example: &anchor_name value ... *anchor_name".to_string(),
468 ],
469 Self::CircularReference { anchors, .. } => vec![
470 format!(
471 "Circular reference detected involving anchors: {}",
472 anchors.join(", ")
473 ),
474 "Break the circular dependency by restructuring your YAML".to_string(),
475 ],
476 Self::InvalidAnchorName { name, reason, .. } => vec![
477 format!("The anchor name '{}' is invalid: {}", name, reason),
478 "Use alphanumeric characters and underscores for anchor names".to_string(),
479 ],
480 Self::AnchorRedefinition { name, old_line, .. } => vec![
481 format!(
482 "The anchor '{}' was already defined at line {}",
483 name, old_line
484 ),
485 "Use unique names for each anchor in your document".to_string(),
486 ],
487 }
488 }
489}
490
491impl From<serde_yaml::Error> for YamlError {
492 fn from(err: serde_yaml::Error) -> Self {
493 let location = err.location().map(|loc| Location {
494 line: loc.line(),
495 column: loc.column(),
496 byte_offset: loc.index(),
497 });
498
499 YamlError::ParseError {
500 message: err.to_string(),
501 context: ErrorContext::boxed(location, None),
502 }
503 }
504}
505
506impl From<String> for YamlError {
507 fn from(err: String) -> Self {
508 YamlError::Conversion {
509 message: err,
510 context: None,
511 }
512 }
513}
514
515impl From<&str> for YamlError {
516 fn from(err: &str) -> Self {
517 YamlError::Conversion {
518 message: err.to_string(),
519 context: None,
520 }
521 }
522}
523
524impl From<hedl_core::lex::LexError> for YamlError {
525 fn from(err: hedl_core::lex::LexError) -> Self {
526 YamlError::InvalidExpression {
527 message: err.to_string(),
528 context: None,
529 }
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn test_location_new() {
539 let loc = Location::new(10, 5, 123);
540 assert_eq!(loc.line, 10);
541 assert_eq!(loc.column, 5);
542 assert_eq!(loc.byte_offset, 123);
543 }
544
545 #[test]
546 fn test_location_display() {
547 let loc = Location::new(42, 10, 456);
548 assert_eq!(loc.to_string(), "line 42, column 10");
549 }
550
551 #[test]
552 fn test_span_new() {
553 let start = Location::new(1, 1, 0);
554 let end = Location::new(1, 10, 9);
555 let span = Span::new(start.clone(), end.clone());
556 assert_eq!(span.start, start);
557 assert_eq!(span.end, end);
558 }
559
560 #[test]
561 fn test_parse_error_display() {
562 let err = YamlError::ParseError {
563 message: "invalid syntax".to_string(),
564 context: None,
565 };
566 let display = err.to_string();
567 assert!(display.contains("YAML parse error: invalid syntax"));
568 assert!(!err.suggestions().is_empty());
570 }
571
572 #[test]
573 fn test_parse_error_with_location() {
574 let err = YamlError::ParseError {
575 message: "invalid syntax".to_string(),
576 context: ErrorContext::boxed(Some(Location::new(3, 5, 20)), None),
577 };
578 let display = err.to_string();
579 assert!(display.contains("invalid syntax"));
580 let loc = err.location().unwrap();
582 assert_eq!(loc.line, 3);
583 assert_eq!(loc.column, 5);
584 assert_eq!(loc.to_string(), "line 3, column 5");
585 }
586
587 #[test]
588 fn test_invalid_root_type_display() {
589 let err = YamlError::InvalidRootType {
590 found: "sequence".to_string(),
591 context: None,
592 };
593 let display = err.to_string();
594 assert!(display.contains("Root must be a YAML mapping, found sequence"));
595 assert!(!err.suggestions().is_empty());
597 }
598
599 #[test]
600 fn test_non_string_key_display() {
601 let err = YamlError::NonStringKey {
602 key_type: "number".to_string(),
603 path: "root.config".to_string(),
604 context: None,
605 };
606 let display = err.to_string();
607 assert!(display.contains("Non-string keys not supported"));
608 assert!(display.contains("number"));
609 assert!(display.contains("root.config"));
610 }
611
612 #[test]
613 fn test_resource_limit_exceeded_display() {
614 let err = YamlError::ResourceLimitExceeded {
615 limit_type: "array_length".to_string(),
616 limit: 1000,
617 actual: 2000,
618 context: None,
619 };
620 let display = err.to_string();
621 assert!(display.contains("Resource limit exceeded"));
622 assert!(display.contains("1000"));
623 assert!(display.contains("2000"));
624 }
625
626 #[test]
627 fn test_max_depth_exceeded_display() {
628 let err = YamlError::MaxDepthExceeded {
629 max_depth: 100,
630 actual_depth: 150,
631 path: "root.deep.path".to_string(),
632 context: None,
633 };
634 let display = err.to_string();
635 assert!(display.contains("Maximum nesting depth"));
636 assert!(display.contains("100"));
637 assert!(display.contains("150"));
638 assert!(display.contains("root.deep.path"));
639 }
640
641 #[test]
642 fn test_document_too_large_display() {
643 let err = YamlError::DocumentTooLarge {
644 size: 20_000_000,
645 max_size: 10_000_000,
646 context: None,
647 };
648 let display = err.to_string();
649 assert!(display.contains("Document size"));
650 assert!(display.contains("20000000"));
651 assert!(display.contains("10000000"));
652 }
653
654 #[test]
655 fn test_array_too_long_display() {
656 let err = YamlError::ArrayTooLong {
657 length: 2000,
658 max_length: 1000,
659 path: "root.items".to_string(),
660 context: None,
661 };
662 let display = err.to_string();
663 assert!(display.contains("Array length"));
664 assert!(display.contains("2000"));
665 assert!(display.contains("1000"));
666 assert!(display.contains("root.items"));
667 }
668
669 #[test]
670 fn test_error_clone() {
671 let err1 = YamlError::ParseError {
672 message: "test".to_string(),
673 context: None,
674 };
675 let err2 = err1.clone();
676 assert_eq!(err1, err2);
677 }
678
679 #[test]
680 fn test_error_equality() {
681 let err1 = YamlError::ParseError {
682 message: "test".to_string(),
683 context: None,
684 };
685 let err2 = YamlError::ParseError {
686 message: "test".to_string(),
687 context: None,
688 };
689 let err3 = YamlError::ParseError {
690 message: "different".to_string(),
691 context: None,
692 };
693
694 assert_eq!(err1, err2);
695 assert_ne!(err1, err3);
696 }
697
698 #[test]
699 fn test_from_string() {
700 let err: YamlError = "test error".to_string().into();
701 match err {
702 YamlError::Conversion { message, .. } => assert_eq!(message, "test error"),
703 _ => panic!("Expected Conversion error"),
704 }
705 }
706
707 #[test]
708 fn test_from_str() {
709 let err: YamlError = "test error".into();
710 match err {
711 YamlError::Conversion { message, .. } => assert_eq!(message, "test error"),
712 _ => panic!("Expected Conversion error"),
713 }
714 }
715
716 #[test]
717 fn test_forward_reference_display() {
718 let err = YamlError::ForwardReference {
719 alias: "undefined".to_string(),
720 line: 5,
721 };
722 assert_eq!(
723 err.to_string(),
724 "Forward reference: alias '*undefined' at line 5 references undefined anchor"
725 );
726 }
727
728 #[test]
729 fn test_circular_reference_display() {
730 let err = YamlError::CircularReference {
731 cycle_path: "a -> b -> c -> a".to_string(),
732 anchors: vec!["a".to_string(), "b".to_string(), "c".to_string()],
733 locations: vec![1, 5, 10],
734 };
735 assert_eq!(
736 err.to_string(),
737 "Circular anchor reference: a -> b -> c -> a"
738 );
739 }
740
741 #[test]
742 fn test_invalid_anchor_name_display() {
743 let err = YamlError::InvalidAnchorName {
744 name: "__reserved".to_string(),
745 reason: "Names starting with __ are reserved".to_string(),
746 };
747 assert_eq!(
748 err.to_string(),
749 "Invalid anchor name '__reserved': Names starting with __ are reserved"
750 );
751 }
752
753 #[test]
754 fn test_anchor_redefinition_display() {
755 let err = YamlError::AnchorRedefinition {
756 name: "anchor1".to_string(),
757 old_line: 5,
758 new_line: 10,
759 };
760 assert_eq!(
761 err.to_string(),
762 "Anchor 'anchor1' redefined at line 10 (previously defined at line 5)"
763 );
764 }
765
766 #[test]
767 fn test_location_method() {
768 let loc = Location::new(5, 10, 50);
769 let err = YamlError::ParseError {
770 message: "test".to_string(),
771 context: ErrorContext::boxed(Some(loc.clone()), None),
772 };
773 assert_eq!(err.location(), Some(&loc));
774 }
775
776 #[test]
777 fn test_snippet_method() {
778 let err = YamlError::ParseError {
779 message: "test".to_string(),
780 context: ErrorContext::boxed(None, Some("test snippet".to_string())),
781 };
782 assert_eq!(err.snippet(), Some("test snippet"));
783 }
784
785 #[test]
786 fn test_path_method() {
787 let err = YamlError::NonStringKey {
788 key_type: "number".to_string(),
789 path: "root.items".to_string(),
790 context: None,
791 };
792 assert_eq!(err.path(), Some("root.items"));
793 }
794
795 #[test]
796 fn test_suggestions_parse_error() {
797 let err = YamlError::ParseError {
798 message: "test".to_string(),
799 context: None,
800 };
801 let suggestions = err.suggestions();
802 assert!(!suggestions.is_empty());
803 assert!(suggestions[0].contains("syntax"));
804 }
805
806 #[test]
807 fn test_suggestions_non_string_key() {
808 let err = YamlError::NonStringKey {
809 key_type: "number".to_string(),
810 path: "test".to_string(),
811 context: None,
812 };
813 let suggestions = err.suggestions();
814 assert!(!suggestions.is_empty());
815 assert!(suggestions[0].contains("strings"));
816 }
817
818 #[test]
819 fn test_error_with_all_fields() {
820 let loc = Location::new(10, 5, 100);
821 let err = YamlError::NonStringKey {
822 key_type: "number".to_string(),
823 path: "root.config".to_string(),
824 context: ErrorContext::boxed(Some(loc), Some(" 123: value".to_string())),
825 };
826
827 let display = err.to_string();
829 assert!(display.contains("root.config"));
830 assert!(display.contains("number"));
831
832 let location = err.location().unwrap();
834 assert_eq!(location.line, 10);
835 assert_eq!(location.column, 5);
836 assert_eq!(location.to_string(), "line 10, column 5");
837
838 assert_eq!(err.snippet().unwrap(), " 123: value");
840
841 let suggestions = err.suggestions();
843 assert!(!suggestions.is_empty());
844 }
845
846 #[test]
847 fn test_from_serde_yaml_error() {
848 let yaml = "{ invalid: [";
850 let result: Result<serde_yaml::Value, serde_yaml::Error> = serde_yaml::from_str(yaml);
851 assert!(result.is_err());
852
853 let serde_err = result.unwrap_err();
854 let yaml_err: YamlError = serde_err.into();
855
856 match yaml_err {
857 YamlError::ParseError {
858 message, context, ..
859 } => {
860 assert!(!message.is_empty());
861 if let Some(ctx) = &context {
863 if let Some(loc) = &ctx.location {
864 assert!(loc.line > 0);
865 assert!(loc.column > 0);
866 }
867 }
868 }
869 _ => panic!("Expected ParseError"),
870 }
871 }
872}