Skip to main content

hedl_yaml/
error.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Error types for YAML conversion operations.
19//!
20//! ## Error Messages
21//!
22//! The YAML parser provides detailed error messages with:
23//!
24//! - **Precise Location**: Line and column numbers for all errors
25//! - **Code Snippets**: Visual context showing the problematic YAML
26//! - **Path Tracking**: Full path to the error (e.g., `root.users[2].name`)
27//! - **Helpful Suggestions**: Actionable advice for fixing common mistakes
28//!
29//! ### Example Error Output
30//!
31//! ```text
32//! Error: Non-string keys not supported, found number
33//!   at line 3, column 3
34//!   in path: users
35//!
36//!    2 | users:
37//!    3 |   123: invalid
38//!      |   ^^^ error here
39//!    4 |   name: Alice
40//!
41//! Suggestions:
42//!   1. YAML keys must be strings, but found number
43//!   2. Convert the key to a string by wrapping it in quotes
44//!   3. Example: "123": value
45//! ```
46
47use std::fmt;
48use thiserror::Error;
49
50/// Location in the YAML source (line and column).
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Location {
53    /// Line number (1-indexed)
54    pub line: usize,
55    /// Column number (1-indexed)
56    pub column: usize,
57    /// Byte offset in the source
58    pub byte_offset: usize,
59}
60
61impl Location {
62    /// Creates a new location.
63    #[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/// Span in the YAML source (start and end locations).
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct Span {
82    /// Start location
83    pub start: Location,
84    /// End location
85    pub end: Location,
86}
87
88impl Span {
89    /// Creates a new span.
90    #[must_use]
91    pub fn new(start: Location, end: Location) -> Self {
92        Self { start, end }
93    }
94}
95
96/// Source context for an error, including location and code snippet.
97///
98/// Boxed inside error variants to keep the `YamlError` enum small enough
99/// for efficient `Result` return values.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ErrorContext {
102    /// Source location of the error.
103    pub location: Option<Location>,
104    /// Code snippet showing the error context.
105    pub snippet: Option<String>,
106}
107
108impl ErrorContext {
109    /// Creates a new error context with the given location and snippet.
110    #[must_use]
111    pub fn new(location: Option<Location>, snippet: Option<String>) -> Self {
112        Self { location, snippet }
113    }
114
115    /// Creates a boxed error context, or None if both fields are None.
116    #[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/// Errors that can occur during YAML to HEDL conversion.
127#[derive(Error, Debug, Clone, PartialEq)]
128pub enum YamlError {
129    /// YAML parsing failed
130    #[error("YAML parse error: {message}")]
131    ParseError {
132        /// Error message describing the parse failure.
133        message: String,
134        /// Source location and code snippet context.
135        context: Option<Box<ErrorContext>>,
136    },
137
138    /// Root element must be a mapping/object
139    #[error("Root must be a YAML mapping, found {found}")]
140    InvalidRootType {
141        /// The type that was found instead of mapping.
142        found: String,
143        /// Source location and code snippet context.
144        context: Option<Box<ErrorContext>>,
145    },
146
147    /// Non-string key encountered in mapping
148    #[error("Non-string keys not supported, found {key_type} at path {path}")]
149    NonStringKey {
150        /// The type of the non-string key.
151        key_type: String,
152        /// Path to the problematic key.
153        path: String,
154        /// Source location and code snippet context.
155        context: Option<Box<ErrorContext>>,
156    },
157
158    /// Invalid number format
159    #[error("Invalid number format: {value}")]
160    InvalidNumber {
161        /// The invalid number string.
162        value: String,
163        /// Source location and code snippet context.
164        context: Option<Box<ErrorContext>>,
165    },
166
167    /// Invalid expression syntax
168    #[error("Invalid expression: {message}")]
169    InvalidExpression {
170        /// Error message describing the expression issue.
171        message: String,
172        /// Source location and code snippet context.
173        context: Option<Box<ErrorContext>>,
174    },
175
176    /// Invalid reference format
177    #[error("Invalid reference format: {message}")]
178    InvalidReference {
179        /// Error message describing the reference issue.
180        message: String,
181        /// Source location and code snippet context.
182        context: Option<Box<ErrorContext>>,
183    },
184
185    /// Nested objects not allowed in scalar context
186    #[error("Nested objects not allowed in scalar context at path {path}")]
187    NestedObjectInScalar {
188        /// Path where nesting was found.
189        path: String,
190        /// Source location and code snippet context.
191        context: Option<Box<ErrorContext>>,
192    },
193
194    /// Invalid tensor element type
195    #[error("Invalid tensor element at path {path}: must be number or sequence")]
196    InvalidTensorElement {
197        /// Path to the invalid tensor element.
198        path: String,
199        /// Expected element type.
200        expected: String,
201        /// Type that was found.
202        found: String,
203        /// Source location and code snippet context.
204        context: Option<Box<ErrorContext>>,
205    },
206
207    /// Resource limit exceeded
208    #[error("Resource limit exceeded: {limit_type} (limit: {limit}, actual: {actual})")]
209    ResourceLimitExceeded {
210        /// Type of limit that was exceeded.
211        limit_type: String,
212        /// Maximum allowed value.
213        limit: usize,
214        /// Actual value that exceeded the limit.
215        actual: usize,
216        /// Source location and code snippet context.
217        context: Option<Box<ErrorContext>>,
218    },
219
220    /// Maximum nesting depth exceeded
221    #[error(
222        "Maximum nesting depth of {max_depth} exceeded at depth {actual_depth} at path {path}"
223    )]
224    MaxDepthExceeded {
225        /// Maximum allowed nesting depth.
226        max_depth: usize,
227        /// Actual nesting depth encountered.
228        actual_depth: usize,
229        /// Path where excessive nesting was found.
230        path: String,
231        /// Source location and code snippet context.
232        context: Option<Box<ErrorContext>>,
233    },
234
235    /// Document too large
236    #[error("Document size {size} bytes exceeds maximum of {max_size} bytes")]
237    DocumentTooLarge {
238        /// Actual document size in bytes.
239        size: usize,
240        /// Maximum allowed document size in bytes.
241        max_size: usize,
242        /// Source location and code snippet context.
243        context: Option<Box<ErrorContext>>,
244    },
245
246    /// Array too long
247    #[error("Array length {length} exceeds maximum of {max_length} at path {path}")]
248    ArrayTooLong {
249        /// Actual array length.
250        length: usize,
251        /// Maximum allowed array length.
252        max_length: usize,
253        /// Path to the oversized array.
254        path: String,
255        /// Source location and code snippet context.
256        context: Option<Box<ErrorContext>>,
257    },
258
259    /// Generic conversion error
260    #[error("Conversion error: {message}")]
261    Conversion {
262        /// Error message describing the conversion failure.
263        message: String,
264        /// Source location and code snippet context.
265        context: Option<Box<ErrorContext>>,
266    },
267
268    /// Forward reference to undefined anchor
269    #[error("Forward reference: alias '*{alias}' at line {line} references undefined anchor")]
270    ForwardReference {
271        /// Name of the undefined alias.
272        alias: String,
273        /// Line number where the forward reference occurred.
274        line: usize,
275    },
276
277    /// Circular anchor reference detected
278    #[error("Circular anchor reference: {cycle_path}")]
279    CircularReference {
280        /// Path describing the circular reference chain.
281        cycle_path: String,
282        /// Anchor names involved in the cycle.
283        anchors: Vec<String>,
284        /// Line numbers where each anchor is defined.
285        locations: Vec<usize>,
286    },
287
288    /// Invalid anchor name
289    #[error("Invalid anchor name '{name}': {reason}")]
290    InvalidAnchorName {
291        /// The invalid anchor name.
292        name: String,
293        /// Reason why the anchor name is invalid.
294        reason: String,
295    },
296
297    /// Anchor redefinition
298    #[error(
299        "Anchor '{name}' redefined at line {new_line} (previously defined at line {old_line})"
300    )]
301    AnchorRedefinition {
302        /// The redefined anchor name.
303        name: String,
304        /// Line number of the original definition.
305        old_line: usize,
306        /// Line number of the redefinition.
307        new_line: usize,
308    },
309}
310
311impl YamlError {
312    /// Returns the location of the error, if available.
313    #[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            // Anchor-related errors don't have context fields
332            Self::ForwardReference { .. }
333            | Self::CircularReference { .. }
334            | Self::InvalidAnchorName { .. }
335            | Self::AnchorRedefinition { .. } => None,
336        }
337    }
338
339    /// Returns the code snippet, if available.
340    #[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            // Anchor-related errors don't have context fields
359            Self::ForwardReference { .. }
360            | Self::CircularReference { .. }
361            | Self::InvalidAnchorName { .. }
362            | Self::AnchorRedefinition { .. } => None,
363        }
364    }
365
366    /// Returns the path where the error occurred, if applicable.
367    #[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    /// Returns helpful suggestions for fixing the error.
380    #[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        // Suggestions are available via the suggestions() method
569        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        // Location is available via the location() method
581        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        // Suggestions are available via the suggestions() method
596        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        // Check base message contains path
828        let display = err.to_string();
829        assert!(display.contains("root.config"));
830        assert!(display.contains("number"));
831
832        // Location is available via method
833        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        // Snippet is available via method
839        assert_eq!(err.snippet().unwrap(), "  123: value");
840
841        // Suggestions are available via method
842        let suggestions = err.suggestions();
843        assert!(!suggestions.is_empty());
844    }
845
846    #[test]
847    fn test_from_serde_yaml_error() {
848        // Create a malformed YAML to generate a serde_yaml error
849        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                // Location may or may not be present depending on the error
862                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}