Skip to main content

hedl_stream/
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 in 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 streaming parser.
19//!
20//! This module defines all error types that can occur during HEDL streaming
21//! parsing. All errors include contextual information (like line numbers)
22//! to aid debugging.
23//!
24//! # Error Categories
25//!
26//! - **I/O Errors**: Problems reading the input stream
27//! - **Syntax Errors**: Malformed HEDL syntax
28//! - **Schema Errors**: Type/schema definition issues
29//! - **Validation Errors**: Data doesn't match schema
30//! - **Timeout Errors**: Parsing exceeded time limit
31//!
32//! # Error Handling Examples
33//!
34//! ## Basic Error Handling
35//!
36//! ```rust
37//! use hedl_stream::{StreamingParser, StreamError};
38//! use std::io::Cursor;
39//!
40//! let bad_input = r#"
41//! %VERSION: 1.0
42//! ---
43//! invalid line without colon
44//! "#;
45//!
46//! let parser = StreamingParser::new(Cursor::new(bad_input)).unwrap();
47//!
48//! for event in parser {
49//!     if let Err(e) = event {
50//!         eprintln!("Error: {}", e);
51//!         if let Some(line) = e.line() {
52//!             eprintln!("  at line {}", line);
53//!         }
54//!     }
55//! }
56//! ```
57//!
58//! ## Match on Error Type
59//!
60//! ```rust
61//! use hedl_stream::{StreamingParser, StreamError};
62//! use std::io::Cursor;
63//!
64//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
65//! let parser = StreamingParser::new(Cursor::new("..."))?;
66//!
67//! for event in parser {
68//!     match event {
69//!         Ok(event) => { /* process */ }
70//!         Err(StreamError::Timeout { elapsed, limit }) => {
71//!             eprintln!("Timeout: took {:?}, limit {:?}", elapsed, limit);
72//!             break;
73//!         }
74//!         Err(StreamError::ShapeMismatch { line, expected, got }) => {
75//!             eprintln!("Line {}: column mismatch (expected {}, got {})",
76//!                 line, expected, got);
77//!         }
78//!         Err(e) => {
79//!             eprintln!("Other error: {}", e);
80//!         }
81//!     }
82//! }
83//! # Ok(())
84//! # }
85//! ```
86
87use thiserror::Error;
88
89/// Errors that can occur during streaming parsing.
90///
91/// All variants include contextual information to help diagnose and fix issues.
92/// Most errors include line numbers; use the [`line()`](Self::line) method to
93/// extract them uniformly.
94///
95/// # Examples
96///
97/// ## Creating Errors
98///
99/// ```rust
100/// use hedl_stream::StreamError;
101///
102/// let err = StreamError::syntax(42, "unexpected token");
103/// assert_eq!(err.line(), Some(42));
104///
105/// let schema_err = StreamError::schema(10, "type not found");
106/// assert_eq!(schema_err.line(), Some(10));
107/// ```
108///
109/// ## Error Display
110///
111/// ```rust
112/// use hedl_stream::StreamError;
113///
114/// let err = StreamError::syntax(5, "missing colon");
115/// let msg = format!("{}", err);
116/// assert!(msg.contains("line 5"));
117/// assert!(msg.contains("missing colon"));
118/// ```
119#[derive(Error, Debug)]
120pub enum StreamError {
121    /// IO error.
122    #[error("IO error: {0}")]
123    Io(#[from] std::io::Error),
124
125    /// Invalid UTF-8 encoding.
126    #[error("Invalid UTF-8 at line {line}: {message}")]
127    Utf8 {
128        /// Line number where the invalid encoding was found.
129        line: usize,
130        /// Description of the encoding error.
131        message: String,
132    },
133
134    /// Syntax error.
135    #[error("Syntax error at line {line}: {message}")]
136    Syntax {
137        /// Line number where the syntax error occurred.
138        line: usize,
139        /// Description of the syntax error.
140        message: String,
141    },
142
143    /// Schema error.
144    #[error("Schema error at line {line}: {message}")]
145    Schema {
146        /// Line number where the schema error occurred.
147        line: usize,
148        /// Description of the schema error.
149        message: String,
150    },
151
152    /// Invalid header.
153    #[error("Invalid header: {0}")]
154    Header(String),
155
156    /// Missing version directive.
157    #[error("Missing %VERSION directive")]
158    MissingVersion,
159
160    /// Invalid version.
161    #[error("Invalid version: {0}")]
162    InvalidVersion(String),
163
164    /// Orphan row (child without parent).
165    #[error("Orphan row at line {line}: {message}")]
166    OrphanRow {
167        /// Line number of the orphan row.
168        line: usize,
169        /// Description of the orphan row error.
170        message: String,
171    },
172
173    /// Shape mismatch.
174    #[error("Shape mismatch at line {line}: expected {expected} columns, got {got}")]
175    ShapeMismatch {
176        /// Line number where the mismatch occurred.
177        line: usize,
178        /// Expected number of columns.
179        expected: usize,
180        /// Actual number of columns found.
181        got: usize,
182    },
183
184    /// Timeout exceeded during parsing.
185    #[error("Parsing timeout: elapsed {elapsed:?} exceeded limit {limit:?}")]
186    Timeout {
187        /// Time elapsed before timeout.
188        elapsed: std::time::Duration,
189        /// Configured timeout limit.
190        limit: std::time::Duration,
191    },
192
193    /// Line length exceeds configured maximum.
194    #[error("Line {line} exceeds maximum length: {length} bytes > {limit} bytes")]
195    LineTooLong {
196        /// Line number that exceeded the limit.
197        line: usize,
198        /// Actual length in bytes.
199        length: usize,
200        /// Maximum allowed length in bytes.
201        limit: usize,
202    },
203
204    /// Invalid UTF-8 encoding in input.
205    #[error("Invalid UTF-8 encoding at line {line}: {error}")]
206    InvalidUtf8 {
207        /// Line number with invalid encoding.
208        line: usize,
209        /// The underlying UTF-8 decoding error.
210        error: std::str::Utf8Error,
211    },
212}
213
214impl StreamError {
215    /// Create a syntax error.
216    #[inline]
217    pub fn syntax(line: usize, message: impl Into<String>) -> Self {
218        Self::Syntax {
219            line,
220            message: message.into(),
221        }
222    }
223
224    /// Create a schema error.
225    #[inline]
226    pub fn schema(line: usize, message: impl Into<String>) -> Self {
227        Self::Schema {
228            line,
229            message: message.into(),
230        }
231    }
232
233    /// Create an orphan row error.
234    #[inline]
235    pub fn orphan_row(line: usize, message: impl Into<String>) -> Self {
236        Self::OrphanRow {
237            line,
238            message: message.into(),
239        }
240    }
241
242    /// Get the line number if available.
243    #[inline]
244    #[must_use]
245    pub fn line(&self) -> Option<usize> {
246        match self {
247            Self::Utf8 { line, .. }
248            | Self::Syntax { line, .. }
249            | Self::Schema { line, .. }
250            | Self::OrphanRow { line, .. }
251            | Self::ShapeMismatch { line, .. }
252            | Self::LineTooLong { line, .. }
253            | Self::InvalidUtf8 { line, .. } => Some(*line),
254            _ => None,
255        }
256    }
257}
258
259/// Result type for streaming operations.
260pub type StreamResult<T> = Result<T, StreamError>;
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use std::io;
266
267    // ==================== StreamError variant tests ====================
268
269    #[test]
270    fn test_stream_error_io() {
271        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
272        let err = StreamError::Io(io_err);
273        let display = format!("{err}");
274        assert!(display.contains("IO error"));
275        assert!(display.contains("file not found"));
276    }
277
278    #[test]
279    fn test_stream_error_utf8() {
280        let err = StreamError::Utf8 {
281            line: 42,
282            message: "invalid byte sequence".to_string(),
283        };
284        let display = format!("{err}");
285        assert!(display.contains("Invalid UTF-8"));
286        assert!(display.contains("42"));
287        assert!(display.contains("invalid byte sequence"));
288    }
289
290    #[test]
291    fn test_stream_error_syntax() {
292        let err = StreamError::Syntax {
293            line: 10,
294            message: "unexpected token".to_string(),
295        };
296        let display = format!("{err}");
297        assert!(display.contains("Syntax error"));
298        assert!(display.contains("10"));
299        assert!(display.contains("unexpected token"));
300    }
301
302    #[test]
303    fn test_stream_error_schema() {
304        let err = StreamError::Schema {
305            line: 5,
306            message: "undefined type".to_string(),
307        };
308        let display = format!("{err}");
309        assert!(display.contains("Schema error"));
310        assert!(display.contains('5'));
311        assert!(display.contains("undefined type"));
312    }
313
314    #[test]
315    fn test_stream_error_header() {
316        let err = StreamError::Header("invalid header format".to_string());
317        let display = format!("{err}");
318        assert!(display.contains("Invalid header"));
319        assert!(display.contains("invalid header format"));
320    }
321
322    #[test]
323    fn test_stream_error_missing_version() {
324        let err = StreamError::MissingVersion;
325        let display = format!("{err}");
326        assert!(display.contains("Missing %VERSION"));
327    }
328
329    #[test]
330    fn test_stream_error_invalid_version() {
331        let err = StreamError::InvalidVersion("abc".to_string());
332        let display = format!("{err}");
333        assert!(display.contains("Invalid version"));
334        assert!(display.contains("abc"));
335    }
336
337    #[test]
338    fn test_stream_error_orphan_row() {
339        let err = StreamError::OrphanRow {
340            line: 25,
341            message: "child without parent".to_string(),
342        };
343        let display = format!("{err}");
344        assert!(display.contains("Orphan row"));
345        assert!(display.contains("25"));
346        assert!(display.contains("child without parent"));
347    }
348
349    #[test]
350    fn test_stream_error_shape_mismatch() {
351        let err = StreamError::ShapeMismatch {
352            line: 100,
353            expected: 5,
354            got: 3,
355        };
356        let display = format!("{err}");
357        assert!(display.contains("Shape mismatch"));
358        assert!(display.contains("100"));
359        assert!(display.contains('5'));
360        assert!(display.contains('3'));
361    }
362
363    // ==================== Constructor tests ====================
364
365    #[test]
366    fn test_syntax_constructor() {
367        let err = StreamError::syntax(15, "invalid syntax");
368        if let StreamError::Syntax { line, message } = err {
369            assert_eq!(line, 15);
370            assert_eq!(message, "invalid syntax");
371        } else {
372            panic!("Expected Syntax variant");
373        }
374    }
375
376    #[test]
377    fn test_syntax_constructor_string() {
378        let err = StreamError::syntax(20, String::from("detailed error"));
379        if let StreamError::Syntax { line, message } = err {
380            assert_eq!(line, 20);
381            assert_eq!(message, "detailed error");
382        } else {
383            panic!("Expected Syntax variant");
384        }
385    }
386
387    #[test]
388    fn test_schema_constructor() {
389        let err = StreamError::schema(30, "type not found");
390        if let StreamError::Schema { line, message } = err {
391            assert_eq!(line, 30);
392            assert_eq!(message, "type not found");
393        } else {
394            panic!("Expected Schema variant");
395        }
396    }
397
398    #[test]
399    fn test_schema_constructor_string() {
400        let err = StreamError::schema(35, String::from("schema validation failed"));
401        if let StreamError::Schema { line, message } = err {
402            assert_eq!(line, 35);
403            assert_eq!(message, "schema validation failed");
404        } else {
405            panic!("Expected Schema variant");
406        }
407    }
408
409    #[test]
410    fn test_orphan_row_constructor() {
411        let err = StreamError::orphan_row(50, "no parent context");
412        if let StreamError::OrphanRow { line, message } = err {
413            assert_eq!(line, 50);
414            assert_eq!(message, "no parent context");
415        } else {
416            panic!("Expected OrphanRow variant");
417        }
418    }
419
420    #[test]
421    fn test_orphan_row_constructor_string() {
422        let err = StreamError::orphan_row(55, String::from("orphan details"));
423        if let StreamError::OrphanRow { line, message } = err {
424            assert_eq!(line, 55);
425            assert_eq!(message, "orphan details");
426        } else {
427            panic!("Expected OrphanRow variant");
428        }
429    }
430
431    // ==================== line() method tests ====================
432
433    #[test]
434    fn test_line_utf8() {
435        let err = StreamError::Utf8 {
436            line: 10,
437            message: "test".to_string(),
438        };
439        assert_eq!(err.line(), Some(10));
440    }
441
442    #[test]
443    fn test_line_syntax() {
444        let err = StreamError::syntax(20, "test");
445        assert_eq!(err.line(), Some(20));
446    }
447
448    #[test]
449    fn test_line_schema() {
450        let err = StreamError::schema(30, "test");
451        assert_eq!(err.line(), Some(30));
452    }
453
454    #[test]
455    fn test_line_orphan_row() {
456        let err = StreamError::orphan_row(40, "test");
457        assert_eq!(err.line(), Some(40));
458    }
459
460    #[test]
461    fn test_line_shape_mismatch() {
462        let err = StreamError::ShapeMismatch {
463            line: 50,
464            expected: 3,
465            got: 2,
466        };
467        assert_eq!(err.line(), Some(50));
468    }
469
470    #[test]
471    fn test_line_io_none() {
472        let io_err = io::Error::other("test");
473        let err = StreamError::Io(io_err);
474        assert_eq!(err.line(), None);
475    }
476
477    #[test]
478    fn test_line_header_none() {
479        let err = StreamError::Header("test".to_string());
480        assert_eq!(err.line(), None);
481    }
482
483    #[test]
484    fn test_line_missing_version_none() {
485        let err = StreamError::MissingVersion;
486        assert_eq!(err.line(), None);
487    }
488
489    #[test]
490    fn test_line_invalid_version_none() {
491        let err = StreamError::InvalidVersion("1.x".to_string());
492        assert_eq!(err.line(), None);
493    }
494
495    // ==================== From<io::Error> tests ====================
496
497    #[test]
498    fn test_from_io_error() {
499        let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
500        let stream_err: StreamError = io_err.into();
501        assert!(matches!(stream_err, StreamError::Io(_)));
502        let display = format!("{stream_err}");
503        assert!(display.contains("access denied"));
504    }
505
506    #[test]
507    fn test_from_io_error_not_found() {
508        let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing");
509        let stream_err: StreamError = io_err.into();
510        assert!(matches!(stream_err, StreamError::Io(_)));
511    }
512
513    // ==================== Debug tests ====================
514
515    #[test]
516    fn test_debug_syntax() {
517        let err = StreamError::syntax(10, "test error");
518        let debug = format!("{err:?}");
519        assert!(debug.contains("Syntax"));
520        assert!(debug.contains("10"));
521    }
522
523    #[test]
524    fn test_debug_schema() {
525        let err = StreamError::schema(20, "schema issue");
526        let debug = format!("{err:?}");
527        assert!(debug.contains("Schema"));
528    }
529
530    #[test]
531    fn test_debug_missing_version() {
532        let err = StreamError::MissingVersion;
533        let debug = format!("{err:?}");
534        assert!(debug.contains("MissingVersion"));
535    }
536
537    // ==================== Edge case tests ====================
538
539    #[test]
540    fn test_line_zero() {
541        let err = StreamError::syntax(0, "at start");
542        assert_eq!(err.line(), Some(0));
543    }
544
545    #[test]
546    fn test_line_max() {
547        let err = StreamError::syntax(usize::MAX, "at end");
548        assert_eq!(err.line(), Some(usize::MAX));
549    }
550
551    #[test]
552    fn test_empty_message() {
553        let err = StreamError::syntax(1, "");
554        if let StreamError::Syntax { message, .. } = err {
555            assert!(message.is_empty());
556        }
557    }
558
559    #[test]
560    fn test_unicode_message() {
561        let err = StreamError::syntax(1, "错误信息 🚫");
562        let display = format!("{err}");
563        assert!(display.contains("错误信息"));
564        assert!(display.contains("🚫"));
565    }
566
567    #[test]
568    fn test_multiline_message() {
569        let err = StreamError::syntax(1, "line1\nline2\nline3");
570        let display = format!("{err}");
571        assert!(display.contains("line1"));
572    }
573
574    #[test]
575    fn test_shape_mismatch_zero_columns() {
576        let err = StreamError::ShapeMismatch {
577            line: 1,
578            expected: 0,
579            got: 0,
580        };
581        assert_eq!(err.line(), Some(1));
582    }
583
584    #[test]
585    fn test_shape_mismatch_large_numbers() {
586        let err = StreamError::ShapeMismatch {
587            line: 1000000,
588            expected: 1000,
589            got: 999,
590        };
591        let display = format!("{err}");
592        assert!(display.contains("1000000"));
593        assert!(display.contains("1000"));
594        assert!(display.contains("999"));
595    }
596
597    // ==================== Timeout error tests ====================
598
599    #[test]
600    fn test_stream_error_timeout() {
601        use std::time::Duration;
602
603        let elapsed = Duration::from_millis(150);
604        let limit = Duration::from_millis(100);
605        let err = StreamError::Timeout { elapsed, limit };
606
607        let display = format!("{err}");
608        assert!(display.contains("timeout"));
609        assert!(display.contains("150ms"));
610        assert!(display.contains("100ms"));
611    }
612
613    #[test]
614    fn test_timeout_error_debug() {
615        use std::time::Duration;
616
617        let err = StreamError::Timeout {
618            elapsed: Duration::from_secs(1),
619            limit: Duration::from_millis(500),
620        };
621
622        let debug = format!("{err:?}");
623        assert!(debug.contains("Timeout"));
624    }
625
626    #[test]
627    fn test_timeout_error_no_line() {
628        use std::time::Duration;
629
630        let err = StreamError::Timeout {
631            elapsed: Duration::from_millis(200),
632            limit: Duration::from_millis(100),
633        };
634
635        // Timeout errors don't have line numbers
636        assert_eq!(err.line(), None);
637    }
638
639    #[test]
640    fn test_timeout_elapsed_greater_than_limit() {
641        use std::time::Duration;
642
643        let elapsed = Duration::from_secs(5);
644        let limit = Duration::from_secs(1);
645        let err = StreamError::Timeout { elapsed, limit };
646
647        let display = format!("{err}");
648        assert!(display.contains("5s"));
649        assert!(display.contains("1s"));
650    }
651
652    #[test]
653    fn test_timeout_with_nanoseconds() {
654        use std::time::Duration;
655
656        let elapsed = Duration::from_nanos(1500);
657        let limit = Duration::from_nanos(1000);
658        let err = StreamError::Timeout { elapsed, limit };
659
660        let display = format!("{err}");
661        assert!(display.contains("timeout"));
662    }
663}