Skip to main content

hedl_core/
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 HEDL parsing.
19
20use std::fmt;
21use thiserror::Error;
22
23/// The kind of error that occurred during parsing.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum HedlErrorKind {
26    /// Lexical or structural violation.
27    Syntax,
28    /// Unsupported version.
29    Version,
30    /// Schema violation or mismatch.
31    Schema,
32    /// Duplicate or invalid alias.
33    Alias,
34    /// Wrong number of cells in row.
35    Shape,
36    /// Logical error (ditto in ID, null in ID, etc).
37    Semantic,
38    /// Child row without NEST rule.
39    OrphanRow,
40    /// Duplicate ID within type.
41    Collision,
42    /// Unresolved reference in strict mode.
43    Reference,
44    /// Security limit exceeded.
45    Security,
46    /// Error during format conversion (JSON, YAML, XML, etc.).
47    Conversion,
48    /// I/O error (file operations, network, etc.).
49    IO,
50}
51
52impl fmt::Display for HedlErrorKind {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::Syntax => write!(f, "SyntaxError"),
56            Self::Version => write!(f, "VersionError"),
57            Self::Schema => write!(f, "SchemaError"),
58            Self::Alias => write!(f, "AliasError"),
59            Self::Shape => write!(f, "ShapeError"),
60            Self::Semantic => write!(f, "SemanticError"),
61            Self::OrphanRow => write!(f, "OrphanRowError"),
62            Self::Collision => write!(f, "CollisionError"),
63            Self::Reference => write!(f, "ReferenceError"),
64            Self::Security => write!(f, "SecurityError"),
65            Self::Conversion => write!(f, "ConversionError"),
66            Self::IO => write!(f, "IOError"),
67        }
68    }
69}
70
71/// An error that occurred during HEDL parsing.
72#[derive(Debug, Clone, Error)]
73#[error("{kind} at line {line}: {message}")]
74pub struct HedlError {
75    /// The kind of error.
76    pub kind: HedlErrorKind,
77    /// Human-readable error message.
78    pub message: String,
79    /// Line number (1-based).
80    pub line: usize,
81    /// Column number (1-based, optional).
82    pub column: Option<usize>,
83    /// Additional context (e.g., "in list User started at line 5").
84    pub context: Option<String>,
85}
86
87impl HedlError {
88    /// Create a new error.
89    pub fn new(kind: HedlErrorKind, message: impl Into<String>, line: usize) -> Self {
90        Self {
91            kind,
92            message: message.into(),
93            line,
94            column: None,
95            context: None,
96        }
97    }
98
99    /// Set line number.
100    pub fn with_line(mut self, line: usize) -> Self {
101        self.line = line;
102        self
103    }
104
105    /// Add column information.
106    pub fn with_column(mut self, column: usize) -> Self {
107        self.column = Some(column);
108        self
109    }
110
111    /// Add context information.
112    pub fn with_context(mut self, context: impl Into<String>) -> Self {
113        self.context = Some(context.into());
114        self
115    }
116
117    // Convenience constructors for each error kind
118
119    /// Creates a syntax error at the given line.
120    pub fn syntax(message: impl Into<String>, line: usize) -> Self {
121        Self::new(HedlErrorKind::Syntax, message, line)
122    }
123
124    /// Creates a version error at the given line.
125    pub fn version(message: impl Into<String>, line: usize) -> Self {
126        Self::new(HedlErrorKind::Version, message, line)
127    }
128
129    /// Creates a schema error at the given line.
130    pub fn schema(message: impl Into<String>, line: usize) -> Self {
131        Self::new(HedlErrorKind::Schema, message, line)
132    }
133
134    /// Creates an alias error at the given line.
135    pub fn alias(message: impl Into<String>, line: usize) -> Self {
136        Self::new(HedlErrorKind::Alias, message, line)
137    }
138
139    /// Creates a shape error at the given line.
140    pub fn shape(message: impl Into<String>, line: usize) -> Self {
141        Self::new(HedlErrorKind::Shape, message, line)
142    }
143
144    /// Creates a semantic error at the given line.
145    pub fn semantic(message: impl Into<String>, line: usize) -> Self {
146        Self::new(HedlErrorKind::Semantic, message, line)
147    }
148
149    /// Creates an orphan row error at the given line.
150    pub fn orphan_row(message: impl Into<String>, line: usize) -> Self {
151        Self::new(HedlErrorKind::OrphanRow, message, line)
152    }
153
154    /// Creates a collision error at the given line.
155    pub fn collision(message: impl Into<String>, line: usize) -> Self {
156        Self::new(HedlErrorKind::Collision, message, line)
157    }
158
159    /// Creates a reference error at the given line.
160    pub fn reference(message: impl Into<String>, line: usize) -> Self {
161        Self::new(HedlErrorKind::Reference, message, line)
162    }
163
164    /// Creates a security error at the given line.
165    pub fn security(message: impl Into<String>, line: usize) -> Self {
166        Self::new(HedlErrorKind::Security, message, line)
167    }
168
169    /// Creates a conversion error (no line context).
170    pub fn conversion(message: impl Into<String>) -> Self {
171        Self::new(HedlErrorKind::Conversion, message, 0)
172    }
173
174    /// Creates an I/O error (no line context).
175    pub fn io(message: impl Into<String>) -> Self {
176        Self::new(HedlErrorKind::IO, message, 0)
177    }
178
179    /// Create a type mismatch error (semantic error kind).
180    pub fn type_mismatch(message: impl Into<String>, line: usize) -> Self {
181        Self::new(HedlErrorKind::Semantic, message, line)
182    }
183}
184
185/// Result type for HEDL operations.
186pub type HedlResult<T> = Result<T, HedlError>;
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    // ==================== HedlErrorKind Display tests ====================
193
194    #[test]
195    fn test_error_kind_display_syntax() {
196        assert_eq!(format!("{}", HedlErrorKind::Syntax), "SyntaxError");
197    }
198
199    #[test]
200    fn test_error_kind_display_version() {
201        assert_eq!(format!("{}", HedlErrorKind::Version), "VersionError");
202    }
203
204    #[test]
205    fn test_error_kind_display_schema() {
206        assert_eq!(format!("{}", HedlErrorKind::Schema), "SchemaError");
207    }
208
209    #[test]
210    fn test_error_kind_display_alias() {
211        assert_eq!(format!("{}", HedlErrorKind::Alias), "AliasError");
212    }
213
214    #[test]
215    fn test_error_kind_display_shape() {
216        assert_eq!(format!("{}", HedlErrorKind::Shape), "ShapeError");
217    }
218
219    #[test]
220    fn test_error_kind_display_semantic() {
221        assert_eq!(format!("{}", HedlErrorKind::Semantic), "SemanticError");
222    }
223
224    #[test]
225    fn test_error_kind_display_orphan_row() {
226        assert_eq!(format!("{}", HedlErrorKind::OrphanRow), "OrphanRowError");
227    }
228
229    #[test]
230    fn test_error_kind_display_collision() {
231        assert_eq!(format!("{}", HedlErrorKind::Collision), "CollisionError");
232    }
233
234    #[test]
235    fn test_error_kind_display_reference() {
236        assert_eq!(format!("{}", HedlErrorKind::Reference), "ReferenceError");
237    }
238
239    #[test]
240    fn test_error_kind_display_security() {
241        assert_eq!(format!("{}", HedlErrorKind::Security), "SecurityError");
242    }
243
244    // ==================== HedlErrorKind equality tests ====================
245
246    #[test]
247    fn test_error_kind_equality() {
248        assert_eq!(HedlErrorKind::Syntax, HedlErrorKind::Syntax);
249        assert_ne!(HedlErrorKind::Syntax, HedlErrorKind::Schema);
250    }
251
252    #[test]
253    fn test_error_kind_clone() {
254        let kind = HedlErrorKind::Reference;
255        let cloned = kind.clone();
256        assert_eq!(kind, cloned);
257    }
258
259    // ==================== HedlError Display tests ====================
260
261    #[test]
262    fn test_error_display() {
263        let err = HedlError::new(HedlErrorKind::Syntax, "unexpected token", 42);
264        let msg = format!("{}", err);
265        assert!(msg.contains("SyntaxError"));
266        assert!(msg.contains("line 42"));
267        assert!(msg.contains("unexpected token"));
268    }
269
270    #[test]
271    fn test_error_with_column() {
272        let err = HedlError::syntax("error", 5).with_column(10);
273        assert_eq!(err.column, Some(10));
274    }
275
276    #[test]
277    fn test_error_with_context() {
278        let err = HedlError::syntax("error", 5).with_context("in struct User");
279        assert_eq!(err.context, Some("in struct User".to_string()));
280    }
281
282    // ==================== Convenience constructor tests ====================
283
284    #[test]
285    fn test_error_syntax() {
286        let err = HedlError::syntax("test", 1);
287        assert_eq!(err.kind, HedlErrorKind::Syntax);
288        assert_eq!(err.line, 1);
289    }
290
291    #[test]
292    fn test_error_version() {
293        let err = HedlError::version("test", 2);
294        assert_eq!(err.kind, HedlErrorKind::Version);
295    }
296
297    #[test]
298    fn test_error_schema() {
299        let err = HedlError::schema("test", 3);
300        assert_eq!(err.kind, HedlErrorKind::Schema);
301    }
302
303    #[test]
304    fn test_error_alias() {
305        let err = HedlError::alias("test", 4);
306        assert_eq!(err.kind, HedlErrorKind::Alias);
307    }
308
309    #[test]
310    fn test_error_shape() {
311        let err = HedlError::shape("test", 5);
312        assert_eq!(err.kind, HedlErrorKind::Shape);
313    }
314
315    #[test]
316    fn test_error_semantic() {
317        let err = HedlError::semantic("test", 6);
318        assert_eq!(err.kind, HedlErrorKind::Semantic);
319    }
320
321    #[test]
322    fn test_error_orphan_row() {
323        let err = HedlError::orphan_row("test", 7);
324        assert_eq!(err.kind, HedlErrorKind::OrphanRow);
325    }
326
327    #[test]
328    fn test_error_collision() {
329        let err = HedlError::collision("test", 8);
330        assert_eq!(err.kind, HedlErrorKind::Collision);
331    }
332
333    #[test]
334    fn test_error_reference() {
335        let err = HedlError::reference("test", 9);
336        assert_eq!(err.kind, HedlErrorKind::Reference);
337    }
338
339    #[test]
340    fn test_error_security() {
341        let err = HedlError::security("test", 10);
342        assert_eq!(err.kind, HedlErrorKind::Security);
343    }
344
345    #[test]
346    fn test_error_conversion() {
347        let err = HedlError::conversion("JSON serialization failed");
348        assert_eq!(err.kind, HedlErrorKind::Conversion);
349        assert_eq!(err.line, 0);
350    }
351
352    #[test]
353    fn test_error_io() {
354        let err = HedlError::io("Failed to read file");
355        assert_eq!(err.kind, HedlErrorKind::IO);
356        assert_eq!(err.line, 0);
357    }
358
359    #[test]
360    fn test_error_kind_display_conversion() {
361        assert_eq!(format!("{}", HedlErrorKind::Conversion), "ConversionError");
362    }
363
364    #[test]
365    fn test_error_kind_display_io() {
366        assert_eq!(format!("{}", HedlErrorKind::IO), "IOError");
367    }
368
369    // ==================== Error trait tests ====================
370
371    #[test]
372    fn test_error_is_std_error() {
373        fn accepts_error<E: std::error::Error>(_: E) {}
374        accepts_error(HedlError::syntax("test", 1));
375    }
376
377    #[test]
378    fn test_error_clone() {
379        let original = HedlError::syntax("message", 5).with_column(10);
380        let cloned = original.clone();
381        assert_eq!(original.kind, cloned.kind);
382        assert_eq!(original.message, cloned.message);
383        assert_eq!(original.line, cloned.line);
384        assert_eq!(original.column, cloned.column);
385    }
386
387    // ==================== Edge cases ====================
388
389    #[test]
390    fn test_error_with_empty_message() {
391        let err = HedlError::syntax("", 1);
392        assert_eq!(err.message, "");
393    }
394
395    #[test]
396    fn test_error_with_unicode_message() {
397        let err = HedlError::syntax("日本語エラー 🎉", 1);
398        assert!(err.message.contains("🎉"));
399    }
400
401    #[test]
402    fn test_error_line_zero() {
403        // Line 0 is technically invalid but should still work
404        let err = HedlError::syntax("test", 0);
405        assert_eq!(err.line, 0);
406    }
407
408    #[test]
409    fn test_error_large_line() {
410        let err = HedlError::syntax("test", usize::MAX);
411        assert_eq!(err.line, usize::MAX);
412    }
413
414    #[test]
415    fn test_error_chained_builders() {
416        let err = HedlError::syntax("error", 5)
417            .with_column(10)
418            .with_context("in list");
419        assert_eq!(err.column, Some(10));
420        assert_eq!(err.context, Some("in list".to_string()));
421    }
422
423    #[test]
424    fn test_error_debug() {
425        let err = HedlError::syntax("test", 1);
426        let debug = format!("{:?}", err);
427        assert!(debug.contains("Syntax"));
428        assert!(debug.contains("test"));
429    }
430}