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    /// Validation error (e.g. field ordering violation)
225    ValidationError(String),
226}
227
228impl LnmpError {
229    /// Adds error context with source snippet
230    pub fn with_context(self, _context: ErrorContext) -> Self {
231        // For now, we return self as-is since the error already contains line/column
232        // In a future enhancement, we could store the context separately
233        self
234    }
235
236    /// Formats the error with source context for rich error messages
237    pub fn format_with_source(&self, source: &str) -> String {
238        let (line, column) = self.position();
239
240        let error_msg = format!("{}", self);
241        let mut output = String::new();
242
243        output.push_str(&format!("Error: {}\n", error_msg));
244        output.push_str("  |\n");
245
246        // Add source lines with line numbers
247        let lines: Vec<&str> = source.lines().collect();
248        let start = line.saturating_sub(2);
249        let end = (line + 1).min(lines.len());
250
251        for line_num in start..end {
252            let actual_line = line_num + 1; // 1-indexed for display
253            let line_content = lines.get(line_num).unwrap_or(&"");
254            output.push_str(&format!("{} | {}\n", actual_line, line_content));
255
256            // Add error indicator on the error line
257            if line_num + 1 == line {
258                let spaces = " ".repeat(column.saturating_sub(1));
259                output.push_str(&format!("  | {}^ here\n", spaces));
260            }
261        }
262
263        output.push_str("  |\n");
264        output
265    }
266
267    /// Returns the line and column position of the error
268    pub fn position(&self) -> (usize, usize) {
269        match self {
270            LnmpError::InvalidCharacter { line, column, .. } => (*line, *column),
271            LnmpError::UnterminatedString { line, column } => (*line, *column),
272            LnmpError::UnexpectedToken { line, column, .. } => (*line, *column),
273            LnmpError::InvalidFieldId { line, column, .. } => (*line, *column),
274            LnmpError::InvalidValue { line, column, .. } => (*line, *column),
275            LnmpError::UnexpectedEof { line, column } => (*line, *column),
276            LnmpError::InvalidEscapeSequence { line, column, .. } => (*line, *column),
277            LnmpError::StrictModeViolation { line, column, .. } => (*line, *column),
278            LnmpError::TypeHintMismatch { line, column, .. } => (*line, *column),
279            LnmpError::InvalidTypeHint { line, column, .. } => (*line, *column),
280            LnmpError::ChecksumMismatch { line, column, .. } => (*line, *column),
281            LnmpError::InvalidChecksum { line, column, .. } => (*line, *column),
282            LnmpError::NestingTooDeep { line, column, .. } => (*line, *column),
283            LnmpError::InvalidNestedStructure { line, column, .. } => (*line, *column),
284            LnmpError::DuplicateFieldId { line, column, .. } => (*line, *column),
285            LnmpError::UnclosedNestedStructure { line, column, .. } => (*line, *column),
286            LnmpError::ValidationError(_) => (0, 0), // Validation errors might not have specific line/col
287        }
288    }
289}
290
291impl std::fmt::Display for LnmpError {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        match self {
294            LnmpError::InvalidCharacter { char, line, column } => write!(
295                f,
296                "Invalid character '{}' at line {}, column {}",
297                char, line, column
298            ),
299            LnmpError::UnterminatedString { line, column } => write!(
300                f,
301                "Unterminated string at line {}, column {}",
302                line, column
303            ),
304            LnmpError::UnexpectedToken {
305                expected,
306                found,
307                line,
308                column,
309            } => write!(
310                f,
311                "Unexpected token at line {}, column {}: expected {}, found {:?}",
312                line, column, expected, found
313            ),
314            LnmpError::InvalidFieldId {
315                value,
316                line,
317                column,
318            } => write!(
319                f,
320                "Invalid field ID at line {}, column {}: '{}'",
321                line, column, value
322            ),
323            LnmpError::InvalidValue {
324                field_id,
325                reason,
326                line,
327                column,
328            } => write!(
329                f,
330                "Invalid value for field {} at line {}, column {}: {}",
331                field_id, line, column, reason
332            ),
333            LnmpError::UnexpectedEof { line, column } => write!(
334                f,
335                "Unexpected end of file at line {}, column {}",
336                line, column
337            ),
338            LnmpError::InvalidEscapeSequence {
339                sequence,
340                line,
341                column,
342            } => write!(
343                f,
344                "Invalid escape sequence '{}' at line {}, column {}",
345                sequence, line, column
346            ),
347            LnmpError::StrictModeViolation {
348                reason,
349                line,
350                column,
351            } => write!(
352                f,
353                "Strict mode violation at line {}, column {}: {}",
354                line, column, reason
355            ),
356            LnmpError::TypeHintMismatch {
357                field_id,
358                expected_type,
359                actual_value,
360                line,
361                column,
362            } => write!(
363                f,
364                "Type hint mismatch for field {} at line {}, column {}: expected type '{}', got {}",
365                field_id, line, column, expected_type, actual_value
366            ),
367            LnmpError::InvalidTypeHint { hint, line, column } => write!(
368                f,
369                "Invalid type hint '{}' at line {}, column {}",
370                hint, line, column
371            ),
372            LnmpError::ChecksumMismatch {
373                field_id,
374                expected,
375                found,
376                line,
377                column,
378            } => write!(
379                f,
380                "Checksum mismatch for field {} at line {}, column {}: expected {}, found {}",
381                field_id, line, column, expected, found
382            ),
383            LnmpError::NestingTooDeep {
384                max_depth,
385                actual_depth,
386                line,
387                column,
388            } => write!(
389                f,
390                "Nesting too deep (NestingTooDeep). Maximum nesting depth exceeded at line {}, column {}: maximum depth is {}, but reached {}",
391                line, column, max_depth, actual_depth
392            ),
393            LnmpError::InvalidNestedStructure {
394                reason,
395                line,
396                column,
397            } => write!(
398                f,
399                "Invalid nested structure at line {}, column {}: {}",
400                line, column, reason
401            ),
402            LnmpError::InvalidChecksum { field_id, reason, line, column } => write!(
403                f,
404                "Invalid checksum for field {} at line {}, column {}: {}",
405                field_id, line, column, reason
406            ),
407            LnmpError::UnclosedNestedStructure {
408                structure_type,
409                opened_at_line,
410                opened_at_column,
411                line,
412                column,
413            } => write!(
414                f,
415                "Unclosed {} at line {}, column {} (opened at line {}, column {})",
416                structure_type, line, column, opened_at_line, opened_at_column
417            ),
418            LnmpError::DuplicateFieldId { field_id, line, column } => write!(
419                f,
420                "DuplicateFieldId: Field ID {} appears multiple times at line {}, column {}",
421                field_id, line, column
422            ),
423            LnmpError::ValidationError(msg) => write!(f, "Validation Error: {}", msg),
424        }
425    }
426}
427
428impl std::error::Error for LnmpError {}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_error_context_creation() {
436        let ctx = ErrorContext::new(1, 5);
437        assert_eq!(ctx.line, 1);
438        assert_eq!(ctx.column, 5);
439        assert!(ctx.source_snippet.is_empty());
440        assert!(ctx.source_file.is_none());
441    }
442
443    #[test]
444    fn test_error_context_with_snippet() {
445        let source = "F7:b=1\nF12:i=14532#DEADBEEF\nF23:sa=[admin,dev]";
446        let ctx = ErrorContext::with_snippet(2, 15, source);
447        assert_eq!(ctx.line, 2);
448        assert_eq!(ctx.column, 15);
449        assert!(ctx.source_snippet.contains("F12:i=14532"));
450    }
451
452    #[test]
453    fn test_error_context_with_file() {
454        let source = "F7:b=1\nF12:i=14532";
455        let ctx = ErrorContext::with_file(1, 5, source, "test.lnmp".to_string());
456        assert_eq!(ctx.line, 1);
457        assert_eq!(ctx.source_file, Some("test.lnmp".to_string()));
458    }
459
460    #[test]
461    fn test_invalid_character_display() {
462        let error = LnmpError::InvalidCharacter {
463            char: '@',
464            line: 1,
465            column: 5,
466        };
467        let msg = format!("{}", error);
468        assert!(msg.contains("line 1"));
469        assert!(msg.contains("column 5"));
470        assert!(msg.contains("'@'"));
471    }
472
473    #[test]
474    fn test_unterminated_string_display() {
475        let error = LnmpError::UnterminatedString {
476            line: 1,
477            column: 10,
478        };
479        let msg = format!("{}", error);
480        assert!(msg.contains("line 1"));
481        assert!(msg.contains("column 10"));
482        assert!(msg.contains("Unterminated string"));
483    }
484
485    #[test]
486    fn test_unexpected_token_display() {
487        let error = LnmpError::UnexpectedToken {
488            expected: "equals sign".to_string(),
489            found: Token::Semicolon,
490            line: 1,
491            column: 5,
492        };
493        let msg = format!("{}", error);
494        assert!(msg.contains("line 1"));
495        assert!(msg.contains("column 5"));
496        assert!(msg.contains("expected equals sign"));
497    }
498
499    #[test]
500    fn test_invalid_field_id_display() {
501        let error = LnmpError::InvalidFieldId {
502            value: "99999".to_string(),
503            line: 2,
504            column: 3,
505        };
506        let msg = format!("{}", error);
507        assert!(msg.contains("line 2"));
508        assert!(msg.contains("column 3"));
509        assert!(msg.contains("99999"));
510    }
511
512    #[test]
513    fn test_invalid_value_display() {
514        let error = LnmpError::InvalidValue {
515            field_id: 12,
516            reason: "not a valid integer".to_string(),
517            line: 3,
518            column: 10,
519        };
520        let msg = format!("{}", error);
521        assert!(msg.contains("field 12"));
522        assert!(msg.contains("line 3"));
523        assert!(msg.contains("column 10"));
524        assert!(msg.contains("not a valid integer"));
525    }
526
527    #[test]
528    fn test_unexpected_eof_display() {
529        let error = LnmpError::UnexpectedEof { line: 5, column: 1 };
530        let msg = format!("{}", error);
531        assert!(msg.contains("line 5"));
532        assert!(msg.contains("column 1"));
533        assert!(msg.contains("end of file"));
534    }
535
536    #[test]
537    fn test_invalid_escape_sequence_display() {
538        let error = LnmpError::InvalidEscapeSequence {
539            sequence: "\\x".to_string(),
540            line: 1,
541            column: 15,
542        };
543        let msg = format!("{}", error);
544        assert!(msg.contains("line 1"));
545        assert!(msg.contains("column 15"));
546        assert!(msg.contains("\\x"));
547    }
548
549    #[test]
550    fn test_checksum_mismatch_display() {
551        let error = LnmpError::ChecksumMismatch {
552            field_id: 12,
553            expected: "36AAE667".to_string(),
554            found: "DEADBEEF".to_string(),
555            line: 2,
556            column: 15,
557        };
558        let msg = format!("{}", error);
559        assert!(msg.contains("field 12"));
560        assert!(msg.contains("line 2"));
561        assert!(msg.contains("column 15"));
562        assert!(msg.contains("36AAE667"));
563        assert!(msg.contains("DEADBEEF"));
564    }
565
566    #[test]
567    fn test_nesting_too_deep_display() {
568        let error = LnmpError::NestingTooDeep {
569            max_depth: 10,
570            actual_depth: 15,
571            line: 5,
572            column: 20,
573        };
574        let msg = format!("{}", error);
575        assert!(msg.contains("line 5"));
576        assert!(msg.contains("column 20"));
577        assert!(msg.contains("10"));
578        assert!(msg.contains("15"));
579        assert!(msg.contains("Nesting too deep"));
580    }
581
582    #[test]
583    fn test_invalid_nested_structure_display() {
584        let error = LnmpError::InvalidNestedStructure {
585            reason: "mismatched braces".to_string(),
586            line: 3,
587            column: 12,
588        };
589        let msg = format!("{}", error);
590        assert!(msg.contains("line 3"));
591        assert!(msg.contains("column 12"));
592        assert!(msg.contains("mismatched braces"));
593    }
594
595    #[test]
596    fn test_unclosed_nested_structure_display() {
597        let error = LnmpError::UnclosedNestedStructure {
598            structure_type: "record".to_string(),
599            opened_at_line: 1,
600            opened_at_column: 5,
601            line: 3,
602            column: 1,
603        };
604        let msg = format!("{}", error);
605        assert!(msg.contains("record"));
606        assert!(msg.contains("line 3"));
607        assert!(msg.contains("column 1"));
608        assert!(msg.contains("line 1"));
609        assert!(msg.contains("column 5"));
610    }
611
612    #[test]
613    fn test_format_with_source() {
614        let source = "F7:b=1\nF12:i=14532#DEADBEEF\nF23:sa=[admin,dev]";
615        let error = LnmpError::ChecksumMismatch {
616            field_id: 12,
617            expected: "36AAE667".to_string(),
618            found: "DEADBEEF".to_string(),
619            line: 2,
620            column: 15,
621        };
622        let formatted = error.format_with_source(source);
623        assert!(formatted.contains("Error:"));
624        assert!(formatted.contains("F12:i=14532#DEADBEEF"));
625        assert!(formatted.contains("^ here"));
626    }
627
628    #[test]
629    fn test_error_position() {
630        let error = LnmpError::NestingTooDeep {
631            max_depth: 10,
632            actual_depth: 15,
633            line: 5,
634            column: 20,
635        };
636        let (line, column) = error.position();
637        assert_eq!(line, 5);
638        assert_eq!(column, 20);
639    }
640
641    #[test]
642    fn test_error_equality() {
643        let error1 = LnmpError::UnexpectedEof { line: 1, column: 1 };
644        let error2 = LnmpError::UnexpectedEof { line: 1, column: 1 };
645        let error3 = LnmpError::UnexpectedEof { line: 2, column: 1 };
646
647        assert_eq!(error1, error2);
648        assert_ne!(error1, error3);
649    }
650
651    #[test]
652    fn test_error_clone() {
653        let error = LnmpError::InvalidFieldId {
654            value: "test".to_string(),
655            line: 1,
656            column: 1,
657        };
658        let cloned = error.clone();
659        assert_eq!(error, cloned);
660    }
661
662    #[test]
663    fn test_error_context_equality() {
664        let ctx1 = ErrorContext::new(1, 5);
665        let ctx2 = ErrorContext::new(1, 5);
666        let ctx3 = ErrorContext::new(2, 5);
667
668        assert_eq!(ctx1, ctx2);
669        assert_ne!(ctx1, ctx3);
670    }
671}