lnmp_codec/
error.rs

1//! Error types for LNMP parsing and encoding operations.
2
3use crate::lexer::Token;
4
5/// Context information for error reporting with source snippets
6#[derive(Debug, Clone, PartialEq)]
7pub struct ErrorContext {
8    /// Line number where the error occurred
9    pub line: usize,
10    /// Column number where the error occurred
11    pub column: usize,
12    /// Source snippet showing 3 lines of context around the error
13    pub source_snippet: String,
14    /// Optional source file name
15    pub source_file: Option<String>,
16}
17
18impl ErrorContext {
19    /// Creates a new error context
20    pub fn new(line: usize, column: usize) -> Self {
21        Self {
22            line,
23            column,
24            source_snippet: String::new(),
25            source_file: None,
26        }
27    }
28
29    /// Creates an error context with source snippet
30    pub fn with_snippet(line: usize, column: usize, source: &str) -> Self {
31        let snippet = Self::extract_snippet(source, line);
32        Self {
33            line,
34            column,
35            source_snippet: snippet,
36            source_file: None,
37        }
38    }
39
40    /// Creates an error context with source snippet and file name
41    pub fn with_file(line: usize, column: usize, source: &str, file: String) -> Self {
42        let snippet = Self::extract_snippet(source, line);
43        Self {
44            line,
45            column,
46            source_snippet: snippet,
47            source_file: Some(file),
48        }
49    }
50
51    /// Extracts 3 lines of context around the error line
52    fn extract_snippet(source: &str, error_line: usize) -> String {
53        let lines: Vec<&str> = source.lines().collect();
54        let start = error_line.saturating_sub(2);
55        let end = (error_line + 1).min(lines.len());
56
57        lines[start..end].join("\n")
58    }
59}
60
61/// Error type for LNMP parsing and encoding operations
62#[derive(Debug, Clone, PartialEq)]
63pub enum LnmpError {
64    /// Invalid character encountered during lexical analysis
65    InvalidCharacter {
66        /// The invalid character
67        char: char,
68        /// Line number where the error occurred
69        line: usize,
70        /// Column number where the error occurred
71        column: usize,
72    },
73    /// Unterminated string literal
74    UnterminatedString {
75        /// Line number where the string started
76        line: usize,
77        /// Column number where the string started
78        column: usize,
79    },
80    /// Unexpected token encountered during parsing
81    UnexpectedToken {
82        /// What token was expected
83        expected: String,
84        /// What token was actually found
85        found: Token,
86        /// Line number where the error occurred
87        line: usize,
88        /// Column number where the error occurred
89        column: usize,
90    },
91    /// Invalid field ID (not a valid u16 or out of range)
92    InvalidFieldId {
93        /// The invalid value that was encountered
94        value: String,
95        /// Line number where the error occurred
96        line: usize,
97        /// Column number where the error occurred
98        column: usize,
99    },
100    /// Invalid value for a field
101    InvalidValue {
102        /// The field ID where the error occurred
103        field_id: u16,
104        /// Reason why the value is invalid
105        reason: String,
106        /// Line number where the error occurred
107        line: usize,
108        /// Column number where the error occurred
109        column: usize,
110    },
111    /// Invalid checksum (wrong format or characters)
112    InvalidChecksum {
113        /// The field ID where the error occurred
114        field_id: u16,
115        /// Reason for invalid checksum
116        reason: String,
117        /// Line number where the error occurred
118        line: usize,
119        /// Column number where the error occurred
120        column: usize,
121    },
122    /// Unexpected end of file
123    UnexpectedEof {
124        /// Line number where EOF was encountered
125        line: usize,
126        /// Column number where EOF was encountered
127        column: usize,
128    },
129    /// Invalid escape sequence in a string
130    InvalidEscapeSequence {
131        /// The invalid escape sequence
132        sequence: String,
133        /// Line number where the error occurred
134        line: usize,
135        /// Column number where the error occurred
136        column: usize,
137    },
138    /// Strict mode violation
139    StrictModeViolation {
140        /// Description of the violation
141        reason: String,
142        /// Line number where the error occurred
143        line: usize,
144        /// Column number where the error occurred
145        column: usize,
146    },
147    /// Type hint mismatch
148    TypeHintMismatch {
149        /// The field ID where the error occurred
150        field_id: u16,
151        /// Expected type from hint
152        expected_type: String,
153        /// Actual value that was parsed
154        actual_value: String,
155        /// Line number where the error occurred
156        line: usize,
157        /// Column number where the error occurred
158        column: usize,
159    },
160    /// Invalid type hint
161    InvalidTypeHint {
162        /// The invalid type hint
163        hint: String,
164        /// Line number where the error occurred
165        line: usize,
166        /// Column number where the error occurred
167        column: usize,
168    },
169    /// Checksum mismatch (v0.3)
170    ChecksumMismatch {
171        /// The field ID where the error occurred
172        field_id: u16,
173        /// Expected checksum
174        expected: String,
175        /// Found checksum
176        found: String,
177        /// Line number where the error occurred
178        line: usize,
179        /// Column number where the error occurred
180        column: usize,
181    },
182    /// Nesting depth exceeds maximum allowed (v0.3)
183    NestingTooDeep {
184        /// Maximum allowed nesting depth
185        max_depth: usize,
186        /// Actual depth that was reached
187        actual_depth: usize,
188        /// Line number where the error occurred
189        line: usize,
190        /// Column number where the error occurred
191        column: usize,
192    },
193    /// Invalid nested structure (v0.3)
194    InvalidNestedStructure {
195        /// Description of the structural issue
196        reason: String,
197        /// Line number where the error occurred
198        line: usize,
199        /// Column number where the error occurred
200        column: usize,
201    },
202    /// Duplicate field ID in strict mode
203    DuplicateFieldId {
204        /// The duplicated field ID
205        field_id: u16,
206        /// Line number where duplicate was detected
207        line: usize,
208        /// Column number where duplicate was detected
209        column: usize,
210    },
211    /// Unclosed nested structure (v0.3)
212    UnclosedNestedStructure {
213        /// Type of structure ("record" or "array")
214        structure_type: String,
215        /// Line where structure was opened
216        opened_at_line: usize,
217        /// Column where structure was opened
218        opened_at_column: usize,
219        /// Line where error was detected
220        line: usize,
221        /// Column where error was detected
222        column: usize,
223    },
224}
225
226impl LnmpError {
227    /// Adds error context with source snippet
228    pub fn with_context(self, _context: ErrorContext) -> Self {
229        // For now, we return self as-is since the error already contains line/column
230        // In a future enhancement, we could store the context separately
231        self
232    }
233
234    /// Formats the error with source context for rich error messages
235    pub fn format_with_source(&self, source: &str) -> String {
236        let (line, column) = self.position();
237
238        let error_msg = format!("{}", self);
239        let mut output = String::new();
240
241        output.push_str(&format!("Error: {}\n", error_msg));
242        output.push_str("  |\n");
243
244        // Add source lines with line numbers
245        let lines: Vec<&str> = source.lines().collect();
246        let start = line.saturating_sub(2);
247        let end = (line + 1).min(lines.len());
248
249        for line_num in start..end {
250            let actual_line = line_num + 1; // 1-indexed for display
251            let line_content = lines.get(line_num).unwrap_or(&"");
252            output.push_str(&format!("{} | {}\n", actual_line, line_content));
253
254            // Add error indicator on the error line
255            if line_num + 1 == line {
256                let spaces = " ".repeat(column.saturating_sub(1));
257                output.push_str(&format!("  | {}^ here\n", spaces));
258            }
259        }
260
261        output.push_str("  |\n");
262        output
263    }
264
265    /// Returns the line and column position of the error
266    pub fn position(&self) -> (usize, usize) {
267        match self {
268            LnmpError::InvalidCharacter { line, column, .. } => (*line, *column),
269            LnmpError::UnterminatedString { line, column } => (*line, *column),
270            LnmpError::UnexpectedToken { line, column, .. } => (*line, *column),
271            LnmpError::InvalidFieldId { line, column, .. } => (*line, *column),
272            LnmpError::InvalidValue { line, column, .. } => (*line, *column),
273            LnmpError::UnexpectedEof { line, column } => (*line, *column),
274            LnmpError::InvalidEscapeSequence { line, column, .. } => (*line, *column),
275            LnmpError::StrictModeViolation { line, column, .. } => (*line, *column),
276            LnmpError::TypeHintMismatch { line, column, .. } => (*line, *column),
277            LnmpError::InvalidTypeHint { line, column, .. } => (*line, *column),
278            LnmpError::ChecksumMismatch { line, column, .. } => (*line, *column),
279            LnmpError::InvalidChecksum { line, column, .. } => (*line, *column),
280            LnmpError::NestingTooDeep { line, column, .. } => (*line, *column),
281            LnmpError::InvalidNestedStructure { line, column, .. } => (*line, *column),
282            LnmpError::DuplicateFieldId { line, column, .. } => (*line, *column),
283            LnmpError::UnclosedNestedStructure { line, column, .. } => (*line, *column),
284        }
285    }
286}
287
288impl std::fmt::Display for LnmpError {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        match self {
291            LnmpError::InvalidCharacter { char, line, column } => write!(
292                f,
293                "Invalid character '{}' at line {}, column {}",
294                char, line, column
295            ),
296            LnmpError::UnterminatedString { line, column } => write!(
297                f,
298                "Unterminated string at line {}, column {}",
299                line, column
300            ),
301            LnmpError::UnexpectedToken {
302                expected,
303                found,
304                line,
305                column,
306            } => write!(
307                f,
308                "Unexpected token at line {}, column {}: expected {}, found {:?}",
309                line, column, expected, found
310            ),
311            LnmpError::InvalidFieldId {
312                value,
313                line,
314                column,
315            } => write!(
316                f,
317                "Invalid field ID at line {}, column {}: '{}'",
318                line, column, value
319            ),
320            LnmpError::InvalidValue {
321                field_id,
322                reason,
323                line,
324                column,
325            } => write!(
326                f,
327                "Invalid value for field {} at line {}, column {}: {}",
328                field_id, line, column, reason
329            ),
330            LnmpError::UnexpectedEof { line, column } => write!(
331                f,
332                "Unexpected end of file at line {}, column {}",
333                line, column
334            ),
335            LnmpError::InvalidEscapeSequence {
336                sequence,
337                line,
338                column,
339            } => write!(
340                f,
341                "Invalid escape sequence '{}' at line {}, column {}",
342                sequence, line, column
343            ),
344            LnmpError::StrictModeViolation {
345                reason,
346                line,
347                column,
348            } => write!(
349                f,
350                "Strict mode violation at line {}, column {}: {}",
351                line, column, reason
352            ),
353            LnmpError::TypeHintMismatch {
354                field_id,
355                expected_type,
356                actual_value,
357                line,
358                column,
359            } => write!(
360                f,
361                "Type hint mismatch for field {} at line {}, column {}: expected type '{}', got {}",
362                field_id, line, column, expected_type, actual_value
363            ),
364            LnmpError::InvalidTypeHint { hint, line, column } => write!(
365                f,
366                "Invalid type hint '{}' at line {}, column {}",
367                hint, line, column
368            ),
369            LnmpError::ChecksumMismatch {
370                field_id,
371                expected,
372                found,
373                line,
374                column,
375            } => write!(
376                f,
377                "Checksum mismatch for field {} at line {}, column {}: expected {}, found {}",
378                field_id, line, column, expected, found
379            ),
380            LnmpError::NestingTooDeep {
381                max_depth,
382                actual_depth,
383                line,
384                column,
385            } => write!(
386                f,
387                "Nesting too deep (NestingTooDeep). Maximum nesting depth exceeded at line {}, column {}: maximum depth is {}, but reached {}",
388                line, column, max_depth, actual_depth
389            ),
390            LnmpError::InvalidNestedStructure {
391                reason,
392                line,
393                column,
394            } => write!(
395                f,
396                "Invalid nested structure at line {}, column {}: {}",
397                line, column, reason
398            ),
399            LnmpError::InvalidChecksum { field_id, reason, line, column } => write!(
400                f,
401                "Invalid checksum for field {} at line {}, column {}: {}",
402                field_id, line, column, reason
403            ),
404            LnmpError::UnclosedNestedStructure {
405                structure_type,
406                opened_at_line,
407                opened_at_column,
408                line,
409                column,
410            } => write!(
411                f,
412                "Unclosed {} at line {}, column {} (opened at line {}, column {})",
413                structure_type, line, column, opened_at_line, opened_at_column
414            ),
415            LnmpError::DuplicateFieldId { field_id, line, column } => write!(
416                f,
417                "DuplicateFieldId: Field ID {} appears multiple times at line {}, column {}",
418                field_id, line, column
419            ),
420        }
421    }
422}
423
424impl std::error::Error for LnmpError {}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_error_context_creation() {
432        let ctx = ErrorContext::new(1, 5);
433        assert_eq!(ctx.line, 1);
434        assert_eq!(ctx.column, 5);
435        assert!(ctx.source_snippet.is_empty());
436        assert!(ctx.source_file.is_none());
437    }
438
439    #[test]
440    fn test_error_context_with_snippet() {
441        let source = "F7:b=1\nF12:i=14532#DEADBEEF\nF23:sa=[admin,dev]";
442        let ctx = ErrorContext::with_snippet(2, 15, source);
443        assert_eq!(ctx.line, 2);
444        assert_eq!(ctx.column, 15);
445        assert!(ctx.source_snippet.contains("F12:i=14532"));
446    }
447
448    #[test]
449    fn test_error_context_with_file() {
450        let source = "F7:b=1\nF12:i=14532";
451        let ctx = ErrorContext::with_file(1, 5, source, "test.lnmp".to_string());
452        assert_eq!(ctx.line, 1);
453        assert_eq!(ctx.source_file, Some("test.lnmp".to_string()));
454    }
455
456    #[test]
457    fn test_invalid_character_display() {
458        let error = LnmpError::InvalidCharacter {
459            char: '@',
460            line: 1,
461            column: 5,
462        };
463        let msg = format!("{}", error);
464        assert!(msg.contains("line 1"));
465        assert!(msg.contains("column 5"));
466        assert!(msg.contains("'@'"));
467    }
468
469    #[test]
470    fn test_unterminated_string_display() {
471        let error = LnmpError::UnterminatedString {
472            line: 1,
473            column: 10,
474        };
475        let msg = format!("{}", error);
476        assert!(msg.contains("line 1"));
477        assert!(msg.contains("column 10"));
478        assert!(msg.contains("Unterminated string"));
479    }
480
481    #[test]
482    fn test_unexpected_token_display() {
483        let error = LnmpError::UnexpectedToken {
484            expected: "equals sign".to_string(),
485            found: Token::Semicolon,
486            line: 1,
487            column: 5,
488        };
489        let msg = format!("{}", error);
490        assert!(msg.contains("line 1"));
491        assert!(msg.contains("column 5"));
492        assert!(msg.contains("expected equals sign"));
493    }
494
495    #[test]
496    fn test_invalid_field_id_display() {
497        let error = LnmpError::InvalidFieldId {
498            value: "99999".to_string(),
499            line: 2,
500            column: 3,
501        };
502        let msg = format!("{}", error);
503        assert!(msg.contains("line 2"));
504        assert!(msg.contains("column 3"));
505        assert!(msg.contains("99999"));
506    }
507
508    #[test]
509    fn test_invalid_value_display() {
510        let error = LnmpError::InvalidValue {
511            field_id: 12,
512            reason: "not a valid integer".to_string(),
513            line: 3,
514            column: 10,
515        };
516        let msg = format!("{}", error);
517        assert!(msg.contains("field 12"));
518        assert!(msg.contains("line 3"));
519        assert!(msg.contains("column 10"));
520        assert!(msg.contains("not a valid integer"));
521    }
522
523    #[test]
524    fn test_unexpected_eof_display() {
525        let error = LnmpError::UnexpectedEof { line: 5, column: 1 };
526        let msg = format!("{}", error);
527        assert!(msg.contains("line 5"));
528        assert!(msg.contains("column 1"));
529        assert!(msg.contains("end of file"));
530    }
531
532    #[test]
533    fn test_invalid_escape_sequence_display() {
534        let error = LnmpError::InvalidEscapeSequence {
535            sequence: "\\x".to_string(),
536            line: 1,
537            column: 15,
538        };
539        let msg = format!("{}", error);
540        assert!(msg.contains("line 1"));
541        assert!(msg.contains("column 15"));
542        assert!(msg.contains("\\x"));
543    }
544
545    #[test]
546    fn test_checksum_mismatch_display() {
547        let error = LnmpError::ChecksumMismatch {
548            field_id: 12,
549            expected: "36AAE667".to_string(),
550            found: "DEADBEEF".to_string(),
551            line: 2,
552            column: 15,
553        };
554        let msg = format!("{}", error);
555        assert!(msg.contains("field 12"));
556        assert!(msg.contains("line 2"));
557        assert!(msg.contains("column 15"));
558        assert!(msg.contains("36AAE667"));
559        assert!(msg.contains("DEADBEEF"));
560    }
561
562    #[test]
563    fn test_nesting_too_deep_display() {
564        let error = LnmpError::NestingTooDeep {
565            max_depth: 10,
566            actual_depth: 15,
567            line: 5,
568            column: 20,
569        };
570        let msg = format!("{}", error);
571        assert!(msg.contains("line 5"));
572        assert!(msg.contains("column 20"));
573        assert!(msg.contains("10"));
574        assert!(msg.contains("15"));
575        assert!(msg.contains("Nesting too deep"));
576    }
577
578    #[test]
579    fn test_invalid_nested_structure_display() {
580        let error = LnmpError::InvalidNestedStructure {
581            reason: "mismatched braces".to_string(),
582            line: 3,
583            column: 12,
584        };
585        let msg = format!("{}", error);
586        assert!(msg.contains("line 3"));
587        assert!(msg.contains("column 12"));
588        assert!(msg.contains("mismatched braces"));
589    }
590
591    #[test]
592    fn test_unclosed_nested_structure_display() {
593        let error = LnmpError::UnclosedNestedStructure {
594            structure_type: "record".to_string(),
595            opened_at_line: 1,
596            opened_at_column: 5,
597            line: 3,
598            column: 1,
599        };
600        let msg = format!("{}", error);
601        assert!(msg.contains("record"));
602        assert!(msg.contains("line 3"));
603        assert!(msg.contains("column 1"));
604        assert!(msg.contains("line 1"));
605        assert!(msg.contains("column 5"));
606    }
607
608    #[test]
609    fn test_format_with_source() {
610        let source = "F7:b=1\nF12:i=14532#DEADBEEF\nF23:sa=[admin,dev]";
611        let error = LnmpError::ChecksumMismatch {
612            field_id: 12,
613            expected: "36AAE667".to_string(),
614            found: "DEADBEEF".to_string(),
615            line: 2,
616            column: 15,
617        };
618        let formatted = error.format_with_source(source);
619        assert!(formatted.contains("Error:"));
620        assert!(formatted.contains("F12:i=14532#DEADBEEF"));
621        assert!(formatted.contains("^ here"));
622    }
623
624    #[test]
625    fn test_error_position() {
626        let error = LnmpError::NestingTooDeep {
627            max_depth: 10,
628            actual_depth: 15,
629            line: 5,
630            column: 20,
631        };
632        let (line, column) = error.position();
633        assert_eq!(line, 5);
634        assert_eq!(column, 20);
635    }
636
637    #[test]
638    fn test_error_equality() {
639        let error1 = LnmpError::UnexpectedEof { line: 1, column: 1 };
640        let error2 = LnmpError::UnexpectedEof { line: 1, column: 1 };
641        let error3 = LnmpError::UnexpectedEof { line: 2, column: 1 };
642
643        assert_eq!(error1, error2);
644        assert_ne!(error1, error3);
645    }
646
647    #[test]
648    fn test_error_clone() {
649        let error = LnmpError::InvalidFieldId {
650            value: "test".to_string(),
651            line: 1,
652            column: 1,
653        };
654        let cloned = error.clone();
655        assert_eq!(error, cloned);
656    }
657
658    #[test]
659    fn test_error_context_equality() {
660        let ctx1 = ErrorContext::new(1, 5);
661        let ctx2 = ErrorContext::new(1, 5);
662        let ctx3 = ErrorContext::new(2, 5);
663
664        assert_eq!(ctx1, ctx2);
665        assert_ne!(ctx1, ctx3);
666    }
667}