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 { line: usize, message: String },
128
129    /// Syntax error.
130    #[error("Syntax error at line {line}: {message}")]
131    Syntax { line: usize, message: String },
132
133    /// Schema error.
134    #[error("Schema error at line {line}: {message}")]
135    Schema { line: usize, message: String },
136
137    /// Invalid header.
138    #[error("Invalid header: {0}")]
139    Header(String),
140
141    /// Missing version directive.
142    #[error("Missing %VERSION directive")]
143    MissingVersion,
144
145    /// Invalid version.
146    #[error("Invalid version: {0}")]
147    InvalidVersion(String),
148
149    /// Orphan row (child without parent).
150    #[error("Orphan row at line {line}: {message}")]
151    OrphanRow { line: usize, message: String },
152
153    /// Shape mismatch.
154    #[error("Shape mismatch at line {line}: expected {expected} columns, got {got}")]
155    ShapeMismatch {
156        line: usize,
157        expected: usize,
158        got: usize,
159    },
160
161    /// Timeout exceeded during parsing.
162    #[error("Parsing timeout: elapsed {elapsed:?} exceeded limit {limit:?}")]
163    Timeout {
164        elapsed: std::time::Duration,
165        limit: std::time::Duration,
166    },
167}
168
169impl StreamError {
170    /// Create a syntax error.
171    #[inline]
172    pub fn syntax(line: usize, message: impl Into<String>) -> Self {
173        Self::Syntax {
174            line,
175            message: message.into(),
176        }
177    }
178
179    /// Create a schema error.
180    #[inline]
181    pub fn schema(line: usize, message: impl Into<String>) -> Self {
182        Self::Schema {
183            line,
184            message: message.into(),
185        }
186    }
187
188    /// Create an orphan row error.
189    #[inline]
190    pub fn orphan_row(line: usize, message: impl Into<String>) -> Self {
191        Self::OrphanRow {
192            line,
193            message: message.into(),
194        }
195    }
196
197    /// Get the line number if available.
198    #[inline]
199    pub fn line(&self) -> Option<usize> {
200        match self {
201            Self::Utf8 { line, .. }
202            | Self::Syntax { line, .. }
203            | Self::Schema { line, .. }
204            | Self::OrphanRow { line, .. }
205            | Self::ShapeMismatch { line, .. } => Some(*line),
206            _ => None,
207        }
208    }
209}
210
211/// Result type for streaming operations.
212pub type StreamResult<T> = Result<T, StreamError>;
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use std::io;
218
219    // ==================== StreamError variant tests ====================
220
221    #[test]
222    fn test_stream_error_io() {
223        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
224        let err = StreamError::Io(io_err);
225        let display = format!("{}", err);
226        assert!(display.contains("IO error"));
227        assert!(display.contains("file not found"));
228    }
229
230    #[test]
231    fn test_stream_error_utf8() {
232        let err = StreamError::Utf8 {
233            line: 42,
234            message: "invalid byte sequence".to_string(),
235        };
236        let display = format!("{}", err);
237        assert!(display.contains("Invalid UTF-8"));
238        assert!(display.contains("42"));
239        assert!(display.contains("invalid byte sequence"));
240    }
241
242    #[test]
243    fn test_stream_error_syntax() {
244        let err = StreamError::Syntax {
245            line: 10,
246            message: "unexpected token".to_string(),
247        };
248        let display = format!("{}", err);
249        assert!(display.contains("Syntax error"));
250        assert!(display.contains("10"));
251        assert!(display.contains("unexpected token"));
252    }
253
254    #[test]
255    fn test_stream_error_schema() {
256        let err = StreamError::Schema {
257            line: 5,
258            message: "undefined type".to_string(),
259        };
260        let display = format!("{}", err);
261        assert!(display.contains("Schema error"));
262        assert!(display.contains("5"));
263        assert!(display.contains("undefined type"));
264    }
265
266    #[test]
267    fn test_stream_error_header() {
268        let err = StreamError::Header("invalid header format".to_string());
269        let display = format!("{}", err);
270        assert!(display.contains("Invalid header"));
271        assert!(display.contains("invalid header format"));
272    }
273
274    #[test]
275    fn test_stream_error_missing_version() {
276        let err = StreamError::MissingVersion;
277        let display = format!("{}", err);
278        assert!(display.contains("Missing %VERSION"));
279    }
280
281    #[test]
282    fn test_stream_error_invalid_version() {
283        let err = StreamError::InvalidVersion("abc".to_string());
284        let display = format!("{}", err);
285        assert!(display.contains("Invalid version"));
286        assert!(display.contains("abc"));
287    }
288
289    #[test]
290    fn test_stream_error_orphan_row() {
291        let err = StreamError::OrphanRow {
292            line: 25,
293            message: "child without parent".to_string(),
294        };
295        let display = format!("{}", err);
296        assert!(display.contains("Orphan row"));
297        assert!(display.contains("25"));
298        assert!(display.contains("child without parent"));
299    }
300
301    #[test]
302    fn test_stream_error_shape_mismatch() {
303        let err = StreamError::ShapeMismatch {
304            line: 100,
305            expected: 5,
306            got: 3,
307        };
308        let display = format!("{}", err);
309        assert!(display.contains("Shape mismatch"));
310        assert!(display.contains("100"));
311        assert!(display.contains("5"));
312        assert!(display.contains("3"));
313    }
314
315    // ==================== Constructor tests ====================
316
317    #[test]
318    fn test_syntax_constructor() {
319        let err = StreamError::syntax(15, "invalid syntax");
320        if let StreamError::Syntax { line, message } = err {
321            assert_eq!(line, 15);
322            assert_eq!(message, "invalid syntax");
323        } else {
324            panic!("Expected Syntax variant");
325        }
326    }
327
328    #[test]
329    fn test_syntax_constructor_string() {
330        let err = StreamError::syntax(20, String::from("detailed error"));
331        if let StreamError::Syntax { line, message } = err {
332            assert_eq!(line, 20);
333            assert_eq!(message, "detailed error");
334        } else {
335            panic!("Expected Syntax variant");
336        }
337    }
338
339    #[test]
340    fn test_schema_constructor() {
341        let err = StreamError::schema(30, "type not found");
342        if let StreamError::Schema { line, message } = err {
343            assert_eq!(line, 30);
344            assert_eq!(message, "type not found");
345        } else {
346            panic!("Expected Schema variant");
347        }
348    }
349
350    #[test]
351    fn test_schema_constructor_string() {
352        let err = StreamError::schema(35, String::from("schema validation failed"));
353        if let StreamError::Schema { line, message } = err {
354            assert_eq!(line, 35);
355            assert_eq!(message, "schema validation failed");
356        } else {
357            panic!("Expected Schema variant");
358        }
359    }
360
361    #[test]
362    fn test_orphan_row_constructor() {
363        let err = StreamError::orphan_row(50, "no parent context");
364        if let StreamError::OrphanRow { line, message } = err {
365            assert_eq!(line, 50);
366            assert_eq!(message, "no parent context");
367        } else {
368            panic!("Expected OrphanRow variant");
369        }
370    }
371
372    #[test]
373    fn test_orphan_row_constructor_string() {
374        let err = StreamError::orphan_row(55, String::from("orphan details"));
375        if let StreamError::OrphanRow { line, message } = err {
376            assert_eq!(line, 55);
377            assert_eq!(message, "orphan details");
378        } else {
379            panic!("Expected OrphanRow variant");
380        }
381    }
382
383    // ==================== line() method tests ====================
384
385    #[test]
386    fn test_line_utf8() {
387        let err = StreamError::Utf8 {
388            line: 10,
389            message: "test".to_string(),
390        };
391        assert_eq!(err.line(), Some(10));
392    }
393
394    #[test]
395    fn test_line_syntax() {
396        let err = StreamError::syntax(20, "test");
397        assert_eq!(err.line(), Some(20));
398    }
399
400    #[test]
401    fn test_line_schema() {
402        let err = StreamError::schema(30, "test");
403        assert_eq!(err.line(), Some(30));
404    }
405
406    #[test]
407    fn test_line_orphan_row() {
408        let err = StreamError::orphan_row(40, "test");
409        assert_eq!(err.line(), Some(40));
410    }
411
412    #[test]
413    fn test_line_shape_mismatch() {
414        let err = StreamError::ShapeMismatch {
415            line: 50,
416            expected: 3,
417            got: 2,
418        };
419        assert_eq!(err.line(), Some(50));
420    }
421
422    #[test]
423    fn test_line_io_none() {
424        let io_err = io::Error::other("test");
425        let err = StreamError::Io(io_err);
426        assert_eq!(err.line(), None);
427    }
428
429    #[test]
430    fn test_line_header_none() {
431        let err = StreamError::Header("test".to_string());
432        assert_eq!(err.line(), None);
433    }
434
435    #[test]
436    fn test_line_missing_version_none() {
437        let err = StreamError::MissingVersion;
438        assert_eq!(err.line(), None);
439    }
440
441    #[test]
442    fn test_line_invalid_version_none() {
443        let err = StreamError::InvalidVersion("1.x".to_string());
444        assert_eq!(err.line(), None);
445    }
446
447    // ==================== From<io::Error> tests ====================
448
449    #[test]
450    fn test_from_io_error() {
451        let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
452        let stream_err: StreamError = io_err.into();
453        assert!(matches!(stream_err, StreamError::Io(_)));
454        let display = format!("{}", stream_err);
455        assert!(display.contains("access denied"));
456    }
457
458    #[test]
459    fn test_from_io_error_not_found() {
460        let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing");
461        let stream_err: StreamError = io_err.into();
462        assert!(matches!(stream_err, StreamError::Io(_)));
463    }
464
465    // ==================== Debug tests ====================
466
467    #[test]
468    fn test_debug_syntax() {
469        let err = StreamError::syntax(10, "test error");
470        let debug = format!("{:?}", err);
471        assert!(debug.contains("Syntax"));
472        assert!(debug.contains("10"));
473    }
474
475    #[test]
476    fn test_debug_schema() {
477        let err = StreamError::schema(20, "schema issue");
478        let debug = format!("{:?}", err);
479        assert!(debug.contains("Schema"));
480    }
481
482    #[test]
483    fn test_debug_missing_version() {
484        let err = StreamError::MissingVersion;
485        let debug = format!("{:?}", err);
486        assert!(debug.contains("MissingVersion"));
487    }
488
489    // ==================== Edge case tests ====================
490
491    #[test]
492    fn test_line_zero() {
493        let err = StreamError::syntax(0, "at start");
494        assert_eq!(err.line(), Some(0));
495    }
496
497    #[test]
498    fn test_line_max() {
499        let err = StreamError::syntax(usize::MAX, "at end");
500        assert_eq!(err.line(), Some(usize::MAX));
501    }
502
503    #[test]
504    fn test_empty_message() {
505        let err = StreamError::syntax(1, "");
506        if let StreamError::Syntax { message, .. } = err {
507            assert!(message.is_empty());
508        }
509    }
510
511    #[test]
512    fn test_unicode_message() {
513        let err = StreamError::syntax(1, "错误信息 🚫");
514        let display = format!("{}", err);
515        assert!(display.contains("错误信息"));
516        assert!(display.contains("🚫"));
517    }
518
519    #[test]
520    fn test_multiline_message() {
521        let err = StreamError::syntax(1, "line1\nline2\nline3");
522        let display = format!("{}", err);
523        assert!(display.contains("line1"));
524    }
525
526    #[test]
527    fn test_shape_mismatch_zero_columns() {
528        let err = StreamError::ShapeMismatch {
529            line: 1,
530            expected: 0,
531            got: 0,
532        };
533        assert_eq!(err.line(), Some(1));
534    }
535
536    #[test]
537    fn test_shape_mismatch_large_numbers() {
538        let err = StreamError::ShapeMismatch {
539            line: 1000000,
540            expected: 1000,
541            got: 999,
542        };
543        let display = format!("{}", err);
544        assert!(display.contains("1000000"));
545        assert!(display.contains("1000"));
546        assert!(display.contains("999"));
547    }
548
549    // ==================== Timeout error tests ====================
550
551    #[test]
552    fn test_stream_error_timeout() {
553        use std::time::Duration;
554
555        let elapsed = Duration::from_millis(150);
556        let limit = Duration::from_millis(100);
557        let err = StreamError::Timeout { elapsed, limit };
558
559        let display = format!("{}", err);
560        assert!(display.contains("timeout"));
561        assert!(display.contains("150ms"));
562        assert!(display.contains("100ms"));
563    }
564
565    #[test]
566    fn test_timeout_error_debug() {
567        use std::time::Duration;
568
569        let err = StreamError::Timeout {
570            elapsed: Duration::from_secs(1),
571            limit: Duration::from_millis(500),
572        };
573
574        let debug = format!("{:?}", err);
575        assert!(debug.contains("Timeout"));
576    }
577
578    #[test]
579    fn test_timeout_error_no_line() {
580        use std::time::Duration;
581
582        let err = StreamError::Timeout {
583            elapsed: Duration::from_millis(200),
584            limit: Duration::from_millis(100),
585        };
586
587        // Timeout errors don't have line numbers
588        assert_eq!(err.line(), None);
589    }
590
591    #[test]
592    fn test_timeout_elapsed_greater_than_limit() {
593        use std::time::Duration;
594
595        let elapsed = Duration::from_secs(5);
596        let limit = Duration::from_secs(1);
597        let err = StreamError::Timeout { elapsed, limit };
598
599        let display = format!("{}", err);
600        assert!(display.contains("5s"));
601        assert!(display.contains("1s"));
602    }
603
604    #[test]
605    fn test_timeout_with_nanoseconds() {
606        use std::time::Duration;
607
608        let elapsed = Duration::from_nanos(1500);
609        let limit = Duration::from_nanos(1000);
610        let err = StreamError::Timeout { elapsed, limit };
611
612        let display = format!("{}", err);
613        assert!(display.contains("timeout"));
614    }
615}