Skip to main content

serializer/
error.rs

1//! Comprehensive error types for dx-serializer
2//!
3//! This module provides a unified error handling system with detailed
4//! location information for debugging and user-friendly error messages.
5
6use std::fmt;
7use thiserror::Error;
8
9/// Serializer result type using [`DxError`] as the shared error.
10pub type Result<T> = std::result::Result<T, DxError>;
11
12/// Maximum length for error snippets (characters of context around error)
13pub const MAX_SNIPPET_LENGTH: usize = 50;
14
15/// Source location information for parse errors
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct SourceLocation {
18    /// Line number (1-indexed)
19    pub line: usize,
20    /// Column number (1-indexed)
21    pub column: usize,
22    /// Byte offset from start of input
23    pub offset: usize,
24}
25
26impl SourceLocation {
27    /// Create a new source location
28    pub fn new(line: usize, column: usize, offset: usize) -> Self {
29        Self {
30            line,
31            column,
32            offset,
33        }
34    }
35
36    /// Create a source location from a byte offset and input
37    pub fn from_offset(input: &[u8], offset: usize) -> Self {
38        let mut line = 1;
39        let mut column = 1;
40
41        for (i, &byte) in input.iter().enumerate() {
42            if i >= offset {
43                break;
44            }
45            if byte == b'\n' {
46                line += 1;
47                column = 1;
48            } else {
49                column += 1;
50            }
51        }
52
53        Self {
54            line,
55            column,
56            offset,
57        }
58    }
59}
60
61impl fmt::Display for SourceLocation {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "line {}, column {}", self.line, self.column)
64    }
65}
66
67/// Extract a snippet of input around the error location
68///
69/// Returns up to MAX_SNIPPET_LENGTH characters centered around the offset,
70/// with invalid UTF-8 bytes replaced with replacement characters.
71pub fn extract_snippet(input: &[u8], offset: usize) -> String {
72    if input.is_empty() {
73        return String::new();
74    }
75
76    // Clamp offset to valid range
77    let offset = offset.min(input.len().saturating_sub(1));
78
79    // Calculate start and end positions for the snippet
80    let half_len = MAX_SNIPPET_LENGTH / 2;
81    let start = offset.saturating_sub(half_len);
82    let end = (offset + half_len).min(input.len());
83
84    // Extract the slice
85    let slice = &input[start..end];
86
87    // Convert to string, replacing invalid UTF-8
88    let snippet: String = String::from_utf8_lossy(slice)
89        .chars()
90        .filter(|c| !c.is_control() || *c == ' ' || *c == '\t')
91        .collect();
92
93    // If snippet is empty or only whitespace, show a placeholder
94    if snippet.trim().is_empty() && !slice.is_empty() {
95        return format!("<{} bytes>", slice.len());
96    }
97
98    snippet
99}
100
101/// Magic bytes for DX binary format
102pub const DX_MAGIC: [u8; 2] = [0x5A, 0x44]; // "ZD" in ASCII
103
104/// Current binary format version
105pub const DX_VERSION: u8 = 1;
106
107/// Maximum input size (100 MB) - prevents memory exhaustion attacks
108pub const MAX_INPUT_SIZE: usize = 100 * 1024 * 1024;
109
110/// Maximum recursion depth for nested structures - prevents stack overflow
111pub const MAX_RECURSION_DEPTH: usize = 1000;
112
113/// Maximum table row count - prevents memory exhaustion
114pub const MAX_TABLE_ROWS: usize = 10_000_000;
115
116/// Comprehensive error type for all DX serializer operations
117#[derive(Error, Debug, Clone, PartialEq)]
118pub enum DxError {
119    // === Parse Errors ===
120    /// Unexpected end of input during parsing
121    #[error("Unexpected end of input at position {0}")]
122    UnexpectedEof(usize),
123
124    /// Parse error with location information and snippet
125    #[error("Parse error at {location}: {message}\n  --> {snippet}")]
126    ParseError {
127        /// Source location where parsing failed.
128        location: SourceLocation,
129        /// Human-readable parse failure message.
130        message: String,
131        /// Snippet of the problematic input (up to 50 chars)
132        snippet: String,
133    },
134
135    /// Invalid syntax at a specific position
136    #[error("Invalid syntax at position {pos}: {msg}")]
137    InvalidSyntax {
138        /// Byte position where the syntax error was detected.
139        pos: usize,
140        /// Human-readable syntax failure message.
141        msg: String,
142    },
143
144    // === Schema Errors ===
145    /// Schema validation error
146    #[error("Schema error: {0}")]
147    SchemaError(String),
148
149    /// Type mismatch during parsing or conversion
150    #[error("Type mismatch: expected {expected}, found {actual}")]
151    TypeMismatch {
152        /// Expected type or schema name.
153        expected: String,
154        /// Actual type or value kind that was found.
155        actual: String,
156    },
157
158    // === Reference Errors ===
159    /// Unknown alias reference
160    #[error("Unknown alias: {0}")]
161    UnknownAlias(String),
162
163    /// Unknown anchor reference
164    #[error("Unknown anchor: {0}")]
165    UnknownAnchor(String),
166
167    // === Type Errors ===
168    /// Invalid type hint in schema
169    #[error("Invalid type hint: {0}")]
170    InvalidTypeHint(String),
171
172    /// Invalid number format
173    #[error("Invalid number format: {0}")]
174    InvalidNumber(String),
175
176    // === Encoding Errors ===
177    /// Invalid UTF-8 sequence
178    #[error("Invalid UTF-8 at byte offset {offset}")]
179    Utf8Error {
180        /// Byte offset where invalid UTF-8 begins.
181        offset: usize,
182    },
183
184    /// Invalid Base62 character
185    #[error("Invalid Base62 character '{char}' at position {position}: {message}")]
186    Base62Error {
187        /// Invalid Base62 character.
188        char: char,
189        /// Character position where the error was found.
190        position: usize,
191        /// Human-readable Base62 failure message.
192        message: String,
193    },
194
195    /// Integer overflow during encoding/decoding
196    #[error("Integer overflow")]
197    IntegerOverflow,
198
199    // === Binary Format Errors ===
200    /// Invalid magic bytes in binary header
201    #[error("Invalid magic bytes: expected [0x5A, 0x44], got [{0:#04X}, {1:#04X}]")]
202    InvalidMagic(u8, u8),
203
204    /// Unsupported binary format version
205    #[error("Unsupported version {found}, expected {expected}")]
206    UnsupportedVersion {
207        /// Version found in the input.
208        found: u8,
209        /// Version supported by this crate.
210        expected: u8,
211    },
212
213    /// Buffer too small for operation
214    #[error("Buffer too small: need {required} bytes, have {available}")]
215    BufferTooSmall {
216        /// Required number of bytes.
217        required: usize,
218        /// Available number of bytes.
219        available: usize,
220    },
221
222    // === Compression Errors ===
223    /// Compression operation failed
224    #[error("Compression error: {0}")]
225    CompressionError(String),
226
227    /// Decompression operation failed
228    #[error("Decompression error: {0}")]
229    DecompressionError(String),
230
231    // === I/O Errors ===
232    /// General I/O error (wraps std::io::Error message)
233    #[error("IO error: {0}")]
234    Io(String),
235
236    /// Platform not supported for operation
237    #[error("Unsupported platform: {0}")]
238    UnsupportedPlatform(String),
239
240    // === Conversion Errors ===
241    /// Format conversion error
242    #[error("Conversion error: {0}")]
243    ConversionError(String),
244
245    /// Ditto operator without previous value
246    #[error("Ditto without previous value at position {0}")]
247    DittoNoPrevious(usize),
248
249    /// Prefix inheritance failed
250    #[error("Prefix inheritance failed: {0}")]
251    PrefixError(String),
252
253    // === Resource Limit Errors ===
254    /// Input size exceeds maximum allowed
255    #[error("Input too large: {size} bytes exceeds maximum of {max} bytes")]
256    InputTooLarge {
257        /// Input size in bytes.
258        size: usize,
259        /// Maximum allowed input size in bytes.
260        max: usize,
261    },
262
263    /// Recursion depth exceeds maximum allowed
264    #[error("Recursion limit exceeded: depth {depth} exceeds maximum of {max}")]
265    RecursionLimitExceeded {
266        /// Observed recursion depth.
267        depth: usize,
268        /// Maximum allowed recursion depth.
269        max: usize,
270    },
271
272    /// Table row count exceeds maximum allowed
273    #[error("Table too large: {rows} rows exceeds maximum of {max} rows")]
274    TableTooLarge {
275        /// Observed table row count.
276        rows: usize,
277        /// Maximum allowed table row count.
278        max: usize,
279    },
280}
281
282impl DxError {
283    /// Create a parse error with location information and snippet
284    pub fn parse_error(input: &[u8], offset: usize, message: impl Into<String>) -> Self {
285        DxError::ParseError {
286            location: SourceLocation::from_offset(input, offset),
287            message: message.into(),
288            snippet: extract_snippet(input, offset),
289        }
290    }
291
292    /// Create a parse error with explicit location and snippet
293    pub fn parse_error_with_location(
294        location: SourceLocation,
295        message: impl Into<String>,
296        snippet: impl Into<String>,
297    ) -> Self {
298        DxError::ParseError {
299            location,
300            message: message.into(),
301            snippet: snippet.into(),
302        }
303    }
304
305    /// Create a type mismatch error
306    pub fn type_mismatch(expected: impl Into<String>, actual: impl Into<String>) -> Self {
307        DxError::TypeMismatch {
308            expected: expected.into(),
309            actual: actual.into(),
310        }
311    }
312
313    /// Create a UTF-8 error at a specific offset
314    pub fn utf8_error(offset: usize) -> Self {
315        DxError::Utf8Error { offset }
316    }
317
318    /// Create a Base62 error with position
319    pub fn base62_error(char: char, position: usize, message: impl Into<String>) -> Self {
320        DxError::Base62Error {
321            char,
322            position,
323            message: message.into(),
324        }
325    }
326
327    /// Create an invalid magic error
328    pub fn invalid_magic(byte0: u8, byte1: u8) -> Self {
329        DxError::InvalidMagic(byte0, byte1)
330    }
331
332    /// Create an unsupported version error
333    pub fn unsupported_version(found: u8) -> Self {
334        DxError::UnsupportedVersion {
335            found,
336            expected: DX_VERSION,
337        }
338    }
339
340    /// Create a buffer too small error
341    pub fn buffer_too_small(required: usize, available: usize) -> Self {
342        DxError::BufferTooSmall {
343            required,
344            available,
345        }
346    }
347
348    /// Create an input too large error
349    pub fn input_too_large(size: usize) -> Self {
350        DxError::InputTooLarge {
351            size,
352            max: MAX_INPUT_SIZE,
353        }
354    }
355
356    /// Create a recursion limit exceeded error
357    pub fn recursion_limit_exceeded(depth: usize) -> Self {
358        DxError::RecursionLimitExceeded {
359            depth,
360            max: MAX_RECURSION_DEPTH,
361        }
362    }
363
364    /// Create a table too large error
365    pub fn table_too_large(rows: usize) -> Self {
366        DxError::TableTooLarge {
367            rows,
368            max: MAX_TABLE_ROWS,
369        }
370    }
371
372    /// Get the byte offset if available
373    pub fn offset(&self) -> Option<usize> {
374        match self {
375            DxError::UnexpectedEof(offset) => Some(*offset),
376            DxError::ParseError { location, .. } => Some(location.offset),
377            DxError::InvalidSyntax { pos, .. } => Some(*pos),
378            DxError::Utf8Error { offset } => Some(*offset),
379            DxError::Base62Error { position, .. } => Some(*position),
380            DxError::DittoNoPrevious(pos) => Some(*pos),
381            _ => None,
382        }
383    }
384
385    /// Get the source location if available
386    pub fn location(&self) -> Option<&SourceLocation> {
387        match self {
388            DxError::ParseError { location, .. } => Some(location),
389            _ => None,
390        }
391    }
392
393    /// Get the snippet if available
394    pub fn snippet(&self) -> Option<&str> {
395        match self {
396            DxError::ParseError { snippet, .. } => Some(snippet),
397            _ => None,
398        }
399    }
400
401    /// Get line number if available (1-indexed)
402    pub fn line(&self) -> Option<usize> {
403        self.location().map(|loc| loc.line)
404    }
405
406    /// Get column number if available (1-indexed)
407    pub fn column(&self) -> Option<usize> {
408        self.location().map(|loc| loc.column)
409    }
410
411    /// Check if this is a recoverable error
412    pub fn is_recoverable(&self) -> bool {
413        matches!(
414            self,
415            DxError::UnknownAlias(_) | DxError::UnknownAnchor(_) | DxError::TypeMismatch { .. }
416        )
417    }
418}
419
420impl From<std::io::Error> for DxError {
421    fn from(err: std::io::Error) -> Self {
422        DxError::Io(err.to_string())
423    }
424}
425
426impl From<std::str::Utf8Error> for DxError {
427    fn from(err: std::str::Utf8Error) -> Self {
428        DxError::Utf8Error {
429            offset: err.valid_up_to(),
430        }
431    }
432}
433
434impl From<std::string::FromUtf8Error> for DxError {
435    fn from(err: std::string::FromUtf8Error) -> Self {
436        DxError::Utf8Error {
437            offset: err.utf8_error().valid_up_to(),
438        }
439    }
440}
441
442impl From<crate::llm::parser::ParseError> for DxError {
443    /// Convert a ParseError from the LLM parser into a DxError.
444    ///
445    /// This conversion preserves position information where available,
446    /// mapping each ParseError variant to the most appropriate DxError variant.
447    fn from(err: crate::llm::parser::ParseError) -> Self {
448        use crate::llm::parser::ParseError;
449
450        match err {
451            ParseError::UnexpectedChar { ch, pos } => DxError::InvalidSyntax {
452                pos,
453                msg: format!("Unexpected character '{}'", ch),
454            },
455            ParseError::UnexpectedEof => DxError::UnexpectedEof(0),
456            ParseError::InvalidValue { value } => DxError::InvalidSyntax {
457                pos: 0,
458                msg: format!("Invalid value format: {}", value),
459            },
460            ParseError::SchemaMismatch { expected, got } => DxError::SchemaError(format!(
461                "Schema mismatch: expected {} columns, got {}",
462                expected, got
463            )),
464            ParseError::Utf8Error { offset } => DxError::Utf8Error { offset },
465            ParseError::InputTooLarge { size, max } => DxError::InputTooLarge { size, max },
466            ParseError::UnclosedBracket { pos } => DxError::InvalidSyntax {
467                pos,
468                msg: "Unclosed bracket".to_string(),
469            },
470            ParseError::UnclosedParen { pos } => DxError::InvalidSyntax {
471                pos,
472                msg: "Unclosed parenthesis".to_string(),
473            },
474            ParseError::MissingValue { pos } => DxError::InvalidSyntax {
475                pos,
476                msg: "Missing value after '='".to_string(),
477            },
478            ParseError::InvalidTable { msg } => {
479                DxError::SchemaError(format!("Invalid table format: {}", msg))
480            }
481        }
482    }
483}
484
485impl From<crate::llm::convert::ConvertError> for DxError {
486    /// Convert a ConvertError into a DxError.
487    ///
488    /// This conversion handles all ConvertError variants:
489    /// - `LlmParse`: Converts the underlying ParseError to DxError
490    /// - `HumanParse`: Converts to ConversionError with the error message
491    /// - `MachineFormat`: Converts to ConversionError with the error message
492    fn from(err: crate::llm::convert::ConvertError) -> Self {
493        use crate::llm::convert::ConvertError;
494
495        match err {
496            ConvertError::LlmParse(parse_err) => parse_err.into(),
497            ConvertError::HumanParse(human_err) => DxError::ConversionError(human_err.to_string()),
498            ConvertError::MachineFormat { msg } => {
499                DxError::ConversionError(format!("Machine format error: {}", msg))
500            }
501        }
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    #[test]
510    fn test_source_location_from_offset() {
511        let input = b"line1\nline2\nline3";
512
513        // First line
514        let loc = SourceLocation::from_offset(input, 0);
515        assert_eq!(loc.line, 1);
516        assert_eq!(loc.column, 1);
517
518        // Middle of first line
519        let loc = SourceLocation::from_offset(input, 3);
520        assert_eq!(loc.line, 1);
521        assert_eq!(loc.column, 4);
522
523        // Start of second line
524        let loc = SourceLocation::from_offset(input, 6);
525        assert_eq!(loc.line, 2);
526        assert_eq!(loc.column, 1);
527
528        // Middle of third line
529        let loc = SourceLocation::from_offset(input, 14);
530        assert_eq!(loc.line, 3);
531        assert_eq!(loc.column, 3);
532    }
533
534    #[test]
535    fn test_parse_error_with_location() {
536        let input = b"key: value\nbad line here";
537        let err = DxError::parse_error(input, 15, "unexpected token");
538
539        if let DxError::ParseError {
540            location,
541            message,
542            snippet,
543        } = &err
544        {
545            assert_eq!(location.line, 2);
546            assert_eq!(location.column, 5);
547            assert_eq!(message, "unexpected token");
548            assert!(!snippet.is_empty());
549        } else {
550            panic!("Expected ParseError");
551        }
552    }
553
554    #[test]
555    fn test_parse_error_snippet() {
556        let input = b"key: value\nbad line here with more content";
557        let err = DxError::parse_error(input, 15, "unexpected token");
558
559        let snippet = err.snippet().unwrap();
560        assert!(!snippet.is_empty());
561        assert!(snippet.len() <= MAX_SNIPPET_LENGTH);
562    }
563
564    #[test]
565    fn test_extract_snippet() {
566        let input = b"hello world this is a test";
567        let snippet = extract_snippet(input, 6);
568        assert!(!snippet.is_empty());
569        assert!(snippet.contains("world"));
570    }
571
572    #[test]
573    fn test_extract_snippet_empty_input() {
574        let input = b"";
575        let snippet = extract_snippet(input, 0);
576        assert!(snippet.is_empty());
577    }
578
579    #[test]
580    fn test_type_mismatch_error() {
581        let err = DxError::type_mismatch("int", "string");
582        let msg = err.to_string();
583        assert!(msg.contains("expected int"));
584        assert!(msg.contains("found string"));
585    }
586
587    #[test]
588    fn test_invalid_magic() {
589        let err = DxError::invalid_magic(0x00, 0x01);
590        assert!(err.to_string().contains("0x00"));
591        assert!(err.to_string().contains("0x01"));
592    }
593
594    #[test]
595    fn test_buffer_too_small() {
596        let err = DxError::buffer_too_small(100, 50);
597        assert!(err.to_string().contains("100"));
598        assert!(err.to_string().contains("50"));
599    }
600
601    #[test]
602    fn test_error_offset() {
603        assert_eq!(DxError::UnexpectedEof(42).offset(), Some(42));
604        assert_eq!(DxError::utf8_error(10).offset(), Some(10));
605        assert_eq!(DxError::SchemaError("test".into()).offset(), None);
606    }
607
608    #[test]
609    fn test_error_line_column() {
610        let input = b"line1\nline2\nline3";
611        let err = DxError::parse_error(input, 8, "test");
612
613        assert_eq!(err.line(), Some(2));
614        assert_eq!(err.column(), Some(3));
615    }
616
617    #[test]
618    fn test_error_line_column_none() {
619        let err = DxError::SchemaError("test".into());
620        assert_eq!(err.line(), None);
621        assert_eq!(err.column(), None);
622    }
623
624    #[test]
625    fn test_std_error_implementation() {
626        // Verify that DxError implements std::error::Error
627        fn assert_error<E: std::error::Error>(_: &E) {}
628
629        let err = DxError::parse_error(b"test input", 5, "test error");
630        assert_error(&err);
631
632        let err = DxError::type_mismatch("int", "string");
633        assert_error(&err);
634
635        let err = DxError::utf8_error(10);
636        assert_error(&err);
637
638        let err = DxError::Io("file not found".to_string());
639        assert_error(&err);
640    }
641
642    #[test]
643    fn test_error_display_is_actionable() {
644        // Parse error should include location and snippet
645        let input = b"key: value\nbad line here";
646        let err = DxError::parse_error(input, 15, "unexpected token");
647        let msg = err.to_string();
648        assert!(msg.contains("line"));
649        assert!(msg.contains("column"));
650        assert!(msg.contains("unexpected token"));
651
652        // Type mismatch should include both types
653        let err = DxError::type_mismatch("integer", "string");
654        let msg = err.to_string();
655        assert!(msg.contains("integer"));
656        assert!(msg.contains("string"));
657
658        // Buffer error should include sizes
659        let err = DxError::buffer_too_small(100, 50);
660        let msg = err.to_string();
661        assert!(msg.contains("100"));
662        assert!(msg.contains("50"));
663
664        // Security limit errors should include limits
665        let err = DxError::input_too_large(200_000_000);
666        let msg = err.to_string();
667        assert!(msg.contains("200000000"));
668        assert!(msg.contains(&MAX_INPUT_SIZE.to_string()));
669    }
670
671    #[test]
672    fn test_error_from_io() {
673        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
674        let dx_err: DxError = io_err.into();
675        match dx_err {
676            DxError::Io(msg) => assert!(msg.contains("not found")),
677            _ => panic!("Expected Io error"),
678        }
679    }
680
681    #[test]
682    fn test_error_from_utf8() {
683        // Create invalid UTF-8
684        let invalid = vec![0xFF, 0xFE];
685        let result = std::str::from_utf8(&invalid);
686        if let Err(utf8_err) = result {
687            let dx_err: DxError = utf8_err.into();
688            match dx_err {
689                DxError::Utf8Error { offset } => assert_eq!(offset, 0),
690                _ => panic!("Expected Utf8Error"),
691            }
692        }
693    }
694
695    #[test]
696    fn test_error_from_parse_error_unexpected_char() {
697        use crate::llm::parser::ParseError;
698
699        let parse_err = ParseError::UnexpectedChar { ch: '@', pos: 42 };
700        let dx_err: DxError = parse_err.into();
701
702        match dx_err {
703            DxError::InvalidSyntax { pos, msg } => {
704                assert_eq!(pos, 42);
705                assert!(msg.contains('@'));
706            }
707            _ => panic!("Expected InvalidSyntax error"),
708        }
709    }
710
711    #[test]
712    fn test_error_from_parse_error_unexpected_eof() {
713        use crate::llm::parser::ParseError;
714
715        let parse_err = ParseError::UnexpectedEof;
716        let dx_err: DxError = parse_err.into();
717
718        match dx_err {
719            DxError::UnexpectedEof(pos) => assert_eq!(pos, 0),
720            _ => panic!("Expected UnexpectedEof error"),
721        }
722    }
723
724    #[test]
725    fn test_error_from_parse_error_utf8() {
726        use crate::llm::parser::ParseError;
727
728        let parse_err = ParseError::Utf8Error { offset: 123 };
729        let dx_err: DxError = parse_err.into();
730
731        match dx_err {
732            DxError::Utf8Error { offset } => assert_eq!(offset, 123),
733            _ => panic!("Expected Utf8Error"),
734        }
735    }
736
737    #[test]
738    fn test_error_from_parse_error_input_too_large() {
739        use crate::llm::parser::ParseError;
740
741        let parse_err = ParseError::InputTooLarge {
742            size: 200_000_000,
743            max: 100_000_000,
744        };
745        let dx_err: DxError = parse_err.into();
746
747        match dx_err {
748            DxError::InputTooLarge { size, max } => {
749                assert_eq!(size, 200_000_000);
750                assert_eq!(max, 100_000_000);
751            }
752            _ => panic!("Expected InputTooLarge error"),
753        }
754    }
755
756    #[test]
757    fn test_error_from_parse_error_schema_mismatch() {
758        use crate::llm::parser::ParseError;
759
760        let parse_err = ParseError::SchemaMismatch {
761            expected: 5,
762            got: 3,
763        };
764        let dx_err: DxError = parse_err.into();
765
766        match dx_err {
767            DxError::SchemaError(msg) => {
768                assert!(msg.contains("5"));
769                assert!(msg.contains("3"));
770            }
771            _ => panic!("Expected SchemaError"),
772        }
773    }
774
775    #[test]
776    fn test_error_from_parse_error_unclosed_bracket() {
777        use crate::llm::parser::ParseError;
778
779        let parse_err = ParseError::UnclosedBracket { pos: 10 };
780        let dx_err: DxError = parse_err.into();
781
782        match dx_err {
783            DxError::InvalidSyntax { pos, msg } => {
784                assert_eq!(pos, 10);
785                assert!(msg.contains("bracket"));
786            }
787            _ => panic!("Expected InvalidSyntax error"),
788        }
789    }
790
791    #[test]
792    fn test_error_from_parse_error_unclosed_paren() {
793        use crate::llm::parser::ParseError;
794
795        let parse_err = ParseError::UnclosedParen { pos: 20 };
796        let dx_err: DxError = parse_err.into();
797
798        match dx_err {
799            DxError::InvalidSyntax { pos, msg } => {
800                assert_eq!(pos, 20);
801                assert!(msg.contains("parenthesis"));
802            }
803            _ => panic!("Expected InvalidSyntax error"),
804        }
805    }
806
807    #[test]
808    fn test_error_from_parse_error_missing_value() {
809        use crate::llm::parser::ParseError;
810
811        let parse_err = ParseError::MissingValue { pos: 15 };
812        let dx_err: DxError = parse_err.into();
813
814        match dx_err {
815            DxError::InvalidSyntax { pos, msg } => {
816                assert_eq!(pos, 15);
817                assert!(msg.contains("Missing value"));
818            }
819            _ => panic!("Expected InvalidSyntax error"),
820        }
821    }
822
823    #[test]
824    fn test_error_from_parse_error_invalid_table() {
825        use crate::llm::parser::ParseError;
826
827        let parse_err = ParseError::InvalidTable {
828            msg: "Empty schema".to_string(),
829        };
830        let dx_err: DxError = parse_err.into();
831
832        match dx_err {
833            DxError::SchemaError(msg) => {
834                assert!(msg.contains("Empty schema"));
835            }
836            _ => panic!("Expected SchemaError"),
837        }
838    }
839
840    #[test]
841    fn test_error_from_parse_error_invalid_value() {
842        use crate::llm::parser::ParseError;
843
844        let parse_err = ParseError::InvalidValue {
845            value: "bad_value".to_string(),
846        };
847        let dx_err: DxError = parse_err.into();
848
849        match dx_err {
850            DxError::InvalidSyntax { pos, msg } => {
851                assert_eq!(pos, 0);
852                assert!(msg.contains("bad_value"));
853            }
854            _ => panic!("Expected InvalidSyntax error"),
855        }
856    }
857
858    #[test]
859    fn test_error_from_parse_error_preserves_position() {
860        use crate::llm::parser::ParseError;
861
862        // Test that position information is preserved for all variants that have it
863        let test_cases: Vec<(ParseError, Option<usize>)> = vec![
864            (ParseError::UnexpectedChar { ch: 'x', pos: 100 }, Some(100)),
865            (ParseError::UnclosedBracket { pos: 200 }, Some(200)),
866            (ParseError::UnclosedParen { pos: 300 }, Some(300)),
867            (ParseError::MissingValue { pos: 400 }, Some(400)),
868            (ParseError::Utf8Error { offset: 500 }, Some(500)),
869        ];
870
871        for (parse_err, expected_pos) in test_cases {
872            let dx_err: DxError = parse_err.into();
873            assert_eq!(
874                dx_err.offset(),
875                expected_pos,
876                "Position not preserved for error: {:?}",
877                dx_err
878            );
879        }
880    }
881
882    // Tests for From<ConvertError> for DxError
883
884    #[test]
885    fn test_error_from_convert_error_llm_parse() {
886        use crate::llm::convert::ConvertError;
887        use crate::llm::parser::ParseError;
888
889        // ConvertError::LlmParse should delegate to the ParseError conversion
890        let parse_err = ParseError::UnexpectedChar { ch: '#', pos: 50 };
891        let convert_err = ConvertError::LlmParse(parse_err);
892        let dx_err: DxError = convert_err.into();
893
894        match dx_err {
895            DxError::InvalidSyntax { pos, msg } => {
896                assert_eq!(pos, 50);
897                assert!(msg.contains('#'));
898            }
899            _ => panic!("Expected InvalidSyntax error, got {:?}", dx_err),
900        }
901    }
902
903    #[test]
904    fn test_error_from_convert_error_llm_parse_eof() {
905        use crate::llm::convert::ConvertError;
906        use crate::llm::parser::ParseError;
907
908        let parse_err = ParseError::UnexpectedEof;
909        let convert_err = ConvertError::LlmParse(parse_err);
910        let dx_err: DxError = convert_err.into();
911
912        match dx_err {
913            DxError::UnexpectedEof(pos) => assert_eq!(pos, 0),
914            _ => panic!("Expected UnexpectedEof error, got {:?}", dx_err),
915        }
916    }
917
918    #[test]
919    fn test_error_from_convert_error_human_parse() {
920        use crate::llm::convert::ConvertError;
921        use crate::llm::human_parser::HumanParseError;
922
923        let human_err = HumanParseError::InvalidSectionHeader {
924            msg: "Missing closing bracket".to_string(),
925        };
926        let convert_err = ConvertError::HumanParse(human_err);
927        let dx_err: DxError = convert_err.into();
928
929        match dx_err {
930            DxError::ConversionError(msg) => {
931                assert!(msg.contains("Invalid section header"));
932                assert!(msg.contains("Missing closing bracket"));
933            }
934            _ => panic!("Expected ConversionError, got {:?}", dx_err),
935        }
936    }
937
938    #[test]
939    fn test_error_from_convert_error_human_parse_invalid_key_value() {
940        use crate::llm::convert::ConvertError;
941        use crate::llm::human_parser::HumanParseError;
942
943        let human_err = HumanParseError::InvalidKeyValue {
944            msg: "No equals sign found".to_string(),
945        };
946        let convert_err = ConvertError::HumanParse(human_err);
947        let dx_err: DxError = convert_err.into();
948
949        match dx_err {
950            DxError::ConversionError(msg) => {
951                assert!(msg.contains("Invalid key-value pair"));
952                assert!(msg.contains("No equals sign found"));
953            }
954            _ => panic!("Expected ConversionError, got {:?}", dx_err),
955        }
956    }
957
958    #[test]
959    fn test_error_from_convert_error_human_parse_invalid_table() {
960        use crate::llm::convert::ConvertError;
961        use crate::llm::human_parser::HumanParseError;
962
963        let human_err = HumanParseError::InvalidTable {
964            line: 42,
965            msg: "Mismatched columns".to_string(),
966        };
967        let convert_err = ConvertError::HumanParse(human_err);
968        let dx_err: DxError = convert_err.into();
969
970        match dx_err {
971            DxError::ConversionError(msg) => {
972                assert!(msg.contains("Invalid table format"));
973                assert!(msg.contains("42"));
974                assert!(msg.contains("Mismatched columns"));
975            }
976            _ => panic!("Expected ConversionError, got {:?}", dx_err),
977        }
978    }
979
980    #[test]
981    fn test_error_from_convert_error_machine_format() {
982        use crate::llm::convert::ConvertError;
983
984        let convert_err = ConvertError::MachineFormat {
985            msg: "Invalid magic number".to_string(),
986        };
987        let dx_err: DxError = convert_err.into();
988
989        match dx_err {
990            DxError::ConversionError(msg) => {
991                assert!(msg.contains("Machine format error"));
992                assert!(msg.contains("Invalid magic number"));
993            }
994            _ => panic!("Expected ConversionError, got {:?}", dx_err),
995        }
996    }
997
998    #[test]
999    fn test_error_from_convert_error_machine_format_eof() {
1000        use crate::llm::convert::ConvertError;
1001
1002        let convert_err = ConvertError::MachineFormat {
1003            msg: "Unexpected end of data".to_string(),
1004        };
1005        let dx_err: DxError = convert_err.into();
1006
1007        match dx_err {
1008            DxError::ConversionError(msg) => {
1009                assert!(msg.contains("Machine format error"));
1010                assert!(msg.contains("Unexpected end of data"));
1011            }
1012            _ => panic!("Expected ConversionError, got {:?}", dx_err),
1013        }
1014    }
1015
1016    #[test]
1017    fn test_error_from_convert_error_all_variants_convertible() {
1018        use crate::llm::convert::ConvertError;
1019        use crate::llm::human_parser::HumanParseError;
1020        use crate::llm::parser::ParseError;
1021
1022        // Test that all ConvertError variants can be converted to DxError
1023        let test_cases: Vec<ConvertError> = vec![
1024            ConvertError::LlmParse(ParseError::UnexpectedEof),
1025            ConvertError::LlmParse(ParseError::UnexpectedChar { ch: 'x', pos: 0 }),
1026            ConvertError::LlmParse(ParseError::InvalidValue {
1027                value: "test".to_string(),
1028            }),
1029            ConvertError::LlmParse(ParseError::SchemaMismatch {
1030                expected: 3,
1031                got: 2,
1032            }),
1033            ConvertError::LlmParse(ParseError::Utf8Error { offset: 10 }),
1034            ConvertError::LlmParse(ParseError::InputTooLarge {
1035                size: 200,
1036                max: 100,
1037            }),
1038            ConvertError::LlmParse(ParseError::UnclosedBracket { pos: 5 }),
1039            ConvertError::LlmParse(ParseError::UnclosedParen { pos: 6 }),
1040            ConvertError::LlmParse(ParseError::MissingValue { pos: 7 }),
1041            ConvertError::LlmParse(ParseError::InvalidTable {
1042                msg: "test".to_string(),
1043            }),
1044            ConvertError::HumanParse(HumanParseError::InvalidSectionHeader {
1045                msg: "test".to_string(),
1046            }),
1047            ConvertError::HumanParse(HumanParseError::InvalidKeyValue {
1048                msg: "test".to_string(),
1049            }),
1050            ConvertError::HumanParse(HumanParseError::InvalidTable {
1051                line: 1,
1052                msg: "test".to_string(),
1053            }),
1054            ConvertError::HumanParse(HumanParseError::UnexpectedContent {
1055                msg: "test".to_string(),
1056            }),
1057            ConvertError::HumanParse(HumanParseError::InputTooLarge {
1058                size: 200,
1059                max: 100,
1060            }),
1061            ConvertError::HumanParse(HumanParseError::TableTooLarge {
1062                rows: 200,
1063                max: 100,
1064            }),
1065            ConvertError::MachineFormat {
1066                msg: "test".to_string(),
1067            },
1068        ];
1069
1070        for convert_err in test_cases {
1071            let dx_err: DxError = convert_err.into();
1072            // Just verify conversion doesn't panic and produces a non-empty error message
1073            let msg = dx_err.to_string();
1074            assert!(!msg.is_empty(), "Error message should not be empty");
1075        }
1076    }
1077}