hedl_csv/
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 CSV conversion operations.
19
20use thiserror::Error;
21
22/// CSV conversion error types.
23///
24/// This enum provides structured error handling for CSV parsing and generation,
25/// with contextual information to help diagnose issues.
26///
27/// # Examples
28///
29/// ```
30/// use hedl_csv::CsvError;
31///
32/// let err = CsvError::TypeMismatch {
33///     column: "age".to_string(),
34///     expected: "integer".to_string(),
35///     value: "abc".to_string(),
36/// };
37///
38/// assert_eq!(
39///     err.to_string(),
40///     "Type mismatch in column 'age': expected integer, got 'abc'"
41/// );
42/// ```
43#[derive(Debug, Error)]
44pub enum CsvError {
45    /// CSV parsing error at a specific line.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use hedl_csv::CsvError;
51    ///
52    /// let err = CsvError::ParseError {
53    ///     line: 42,
54    ///     message: "Invalid escape sequence".to_string(),
55    /// };
56    /// assert!(err.to_string().contains("line 42"));
57    /// ```
58    #[error("CSV parse error at line {line}: {message}")]
59    ParseError {
60        /// Line number where the error occurred (1-based).
61        line: usize,
62        /// Detailed error message.
63        message: String,
64    },
65
66    /// Type mismatch when converting values.
67    ///
68    /// This error occurs when a CSV field value cannot be converted to the expected type.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use hedl_csv::CsvError;
74    ///
75    /// let err = CsvError::TypeMismatch {
76    ///     column: "price".to_string(),
77    ///     expected: "float".to_string(),
78    ///     value: "not-a-number".to_string(),
79    /// };
80    /// ```
81    #[error("Type mismatch in column '{column}': expected {expected}, got '{value}'")]
82    TypeMismatch {
83        /// Column name where the mismatch occurred.
84        column: String,
85        /// Expected type description.
86        expected: String,
87        /// Actual value that failed to convert.
88        value: String,
89    },
90
91    /// Missing required column in CSV data.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use hedl_csv::CsvError;
97    ///
98    /// let err = CsvError::MissingColumn("id".to_string());
99    /// assert_eq!(err.to_string(), "Missing required column: id");
100    /// ```
101    #[error("Missing required column: {0}")]
102    MissingColumn(String),
103
104    /// Invalid header format or content.
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use hedl_csv::CsvError;
110    ///
111    /// let err = CsvError::InvalidHeader {
112    ///     position: 0,
113    ///     reason: "Empty column name".to_string(),
114    /// };
115    /// ```
116    #[error("Invalid header at position {position}: {reason}")]
117    InvalidHeader {
118        /// Position of the invalid header (0-based).
119        position: usize,
120        /// Reason the header is invalid.
121        reason: String,
122    },
123
124    /// Row has wrong number of columns.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use hedl_csv::CsvError;
130    ///
131    /// let err = CsvError::WidthMismatch {
132    ///     expected: 5,
133    ///     actual: 3,
134    ///     row: 10,
135    /// };
136    /// assert!(err.to_string().contains("expected 5 columns"));
137    /// assert!(err.to_string().contains("got 3"));
138    /// ```
139    #[error("Row width mismatch: expected {expected} columns, got {actual} in row {row}")]
140    WidthMismatch {
141        /// Expected number of columns.
142        expected: usize,
143        /// Actual number of columns in the row.
144        actual: usize,
145        /// Row number where the mismatch occurred (1-based).
146        row: usize,
147    },
148
149    /// I/O error during CSV reading or writing.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use hedl_csv::CsvError;
155    /// use std::io;
156    ///
157    /// let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
158    /// let csv_err = CsvError::from(io_err);
159    /// ```
160    #[error("I/O error: {0}")]
161    Io(#[from] std::io::Error),
162
163    /// Error from underlying CSV library.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// use hedl_csv::CsvError;
169    ///
170    /// // This error type wraps csv::Error transparently
171    /// ```
172    #[error("CSV library error: {0}")]
173    CsvLib(#[from] csv::Error),
174
175    /// HEDL core error during conversion.
176    ///
177    /// This wraps errors from the `hedl_core` crate when they occur during
178    /// CSV conversion operations.
179    #[error("HEDL core error: {0}")]
180    HedlCore(String),
181
182    /// Row count exceeded security limit.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use hedl_csv::CsvError;
188    ///
189    /// let err = CsvError::SecurityLimit {
190    ///     limit: 1_000_000,
191    ///     actual: 1_000_001,
192    /// };
193    /// assert!(err.to_string().contains("Security limit"));
194    /// ```
195    #[error("Security limit exceeded: row count {actual} exceeds maximum {limit}")]
196    SecurityLimit {
197        /// Maximum allowed rows.
198        limit: usize,
199        /// Actual row count encountered.
200        actual: usize,
201    },
202
203    /// Empty ID field in CSV data.
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use hedl_csv::CsvError;
209    ///
210    /// let err = CsvError::EmptyId { row: 5 };
211    /// assert_eq!(err.to_string(), "Empty 'id' field at row 5");
212    /// ```
213    #[error("Empty 'id' field at row {row}")]
214    EmptyId {
215        /// Row number with empty ID (1-based).
216        row: usize,
217    },
218
219    /// Matrix list not found in document.
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use hedl_csv::CsvError;
225    ///
226    /// let err = CsvError::ListNotFound {
227    ///     name: "people".to_string(),
228    ///     available: "users, items".to_string(),
229    /// };
230    /// assert!(err.to_string().contains("not found"));
231    /// ```
232    #[error("Matrix list '{name}' not found in document (available: {available})")]
233    ListNotFound {
234        /// Name of the list that was not found.
235        name: String,
236        /// Available list names in the document.
237        available: String,
238    },
239
240    /// Item is not a matrix list.
241    ///
242    /// # Examples
243    ///
244    /// ```
245    /// use hedl_csv::CsvError;
246    ///
247    /// let err = CsvError::NotAList {
248    ///     name: "value".to_string(),
249    ///     actual_type: "scalar".to_string(),
250    /// };
251    /// ```
252    #[error("Item '{name}' is not a matrix list (found: {actual_type})")]
253    NotAList {
254        /// Name of the item.
255        name: String,
256        /// Actual type of the item.
257        actual_type: String,
258    },
259
260    /// No matrix lists found in document.
261    #[error("No matrix lists found in document")]
262    NoLists,
263
264    /// Invalid UTF-8 in CSV output.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use hedl_csv::CsvError;
270    ///
271    /// let err = CsvError::InvalidUtf8 {
272    ///     context: "CSV serialization".to_string(),
273    /// };
274    /// ```
275    #[error("Invalid UTF-8 in {context}")]
276    InvalidUtf8 {
277        /// Context where the invalid UTF-8 was encountered.
278        context: String,
279    },
280
281    /// Generic error with custom message.
282    ///
283    /// This is a catch-all for errors that don't fit other categories.
284    #[error("{0}")]
285    Other(String),
286}
287
288/// Convenience type alias for `Result` with `CsvError`.
289pub type Result<T> = std::result::Result<T, CsvError>;
290
291impl CsvError {
292    /// Add context to an error message.
293    ///
294    /// This is useful for providing additional information about where an error occurred.
295    ///
296    /// # Examples
297    ///
298    /// ```
299    /// use hedl_csv::CsvError;
300    ///
301    /// let err = CsvError::ParseError {
302    ///     line: 5,
303    ///     message: "Invalid value".to_string(),
304    /// };
305    /// let with_context = err.with_context("in column 'age' at line 10".to_string());
306    /// ```
307    pub fn with_context(self, context: String) -> Self {
308        match self {
309            CsvError::ParseError { line, message } => CsvError::ParseError {
310                line,
311                message: format!("{} ({})", message, context),
312            },
313            CsvError::HedlCore(msg) => CsvError::HedlCore(format!("{} ({})", msg, context)),
314            CsvError::Other(msg) => CsvError::Other(format!("{} ({})", msg, context)),
315            // For other variants, wrap in Other with context
316            other => CsvError::Other(format!("{} ({})", other, context)),
317        }
318    }
319}
320
321impl From<hedl_core::HedlError> for CsvError {
322    fn from(err: hedl_core::HedlError) -> Self {
323        CsvError::HedlCore(err.to_string())
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_parse_error_display() {
333        let err = CsvError::ParseError {
334            line: 42,
335            message: "Invalid escape sequence".to_string(),
336        };
337        assert_eq!(
338            err.to_string(),
339            "CSV parse error at line 42: Invalid escape sequence"
340        );
341    }
342
343    #[test]
344    fn test_type_mismatch_display() {
345        let err = CsvError::TypeMismatch {
346            column: "age".to_string(),
347            expected: "integer".to_string(),
348            value: "abc".to_string(),
349        };
350        assert_eq!(
351            err.to_string(),
352            "Type mismatch in column 'age': expected integer, got 'abc'"
353        );
354    }
355
356    #[test]
357    fn test_missing_column_display() {
358        let err = CsvError::MissingColumn("id".to_string());
359        assert_eq!(err.to_string(), "Missing required column: id");
360    }
361
362    #[test]
363    fn test_invalid_header_display() {
364        let err = CsvError::InvalidHeader {
365            position: 3,
366            reason: "Empty column name".to_string(),
367        };
368        assert_eq!(
369            err.to_string(),
370            "Invalid header at position 3: Empty column name"
371        );
372    }
373
374    #[test]
375    fn test_width_mismatch_display() {
376        let err = CsvError::WidthMismatch {
377            expected: 5,
378            actual: 3,
379            row: 10,
380        };
381        assert_eq!(
382            err.to_string(),
383            "Row width mismatch: expected 5 columns, got 3 in row 10"
384        );
385    }
386
387    #[test]
388    fn test_security_limit_display() {
389        let err = CsvError::SecurityLimit {
390            limit: 1_000_000,
391            actual: 1_500_000,
392        };
393        assert_eq!(
394            err.to_string(),
395            "Security limit exceeded: row count 1500000 exceeds maximum 1000000"
396        );
397    }
398
399    #[test]
400    fn test_empty_id_display() {
401        let err = CsvError::EmptyId { row: 5 };
402        assert_eq!(err.to_string(), "Empty 'id' field at row 5");
403    }
404
405    #[test]
406    fn test_list_not_found_display() {
407        let err = CsvError::ListNotFound {
408            name: "people".to_string(),
409            available: "users, items".to_string(),
410        };
411        assert_eq!(
412            err.to_string(),
413            "Matrix list 'people' not found in document (available: users, items)"
414        );
415    }
416
417    #[test]
418    fn test_not_a_list_display() {
419        let err = CsvError::NotAList {
420            name: "value".to_string(),
421            actual_type: "scalar".to_string(),
422        };
423        assert_eq!(
424            err.to_string(),
425            "Item 'value' is not a matrix list (found: scalar)"
426        );
427    }
428
429    #[test]
430    fn test_no_lists_display() {
431        let err = CsvError::NoLists;
432        assert_eq!(err.to_string(), "No matrix lists found in document");
433    }
434
435    #[test]
436    fn test_invalid_utf8_display() {
437        let err = CsvError::InvalidUtf8 {
438            context: "CSV output".to_string(),
439        };
440        assert_eq!(err.to_string(), "Invalid UTF-8 in CSV output");
441    }
442
443    #[test]
444    fn test_other_display() {
445        let err = CsvError::Other("Custom error message".to_string());
446        assert_eq!(err.to_string(), "Custom error message");
447    }
448
449    #[test]
450    fn test_io_error_conversion() {
451        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
452        let csv_err = CsvError::from(io_err);
453        assert!(csv_err.to_string().contains("I/O error"));
454    }
455
456    #[test]
457    fn test_hedl_error_conversion() {
458        let hedl_err = hedl_core::HedlError::new(
459            hedl_core::HedlErrorKind::Syntax,
460            "Syntax error".to_string(),
461            1,
462        );
463        let csv_err = CsvError::from(hedl_err);
464        assert!(csv_err.to_string().contains("HEDL core error"));
465    }
466
467    #[test]
468    fn test_error_is_send_sync() {
469        fn assert_send_sync<T: Send + Sync>() {}
470        assert_send_sync::<CsvError>();
471    }
472
473    #[test]
474    fn test_error_debug() {
475        let err = CsvError::MissingColumn("id".to_string());
476        let debug = format!("{:?}", err);
477        assert!(debug.contains("MissingColumn"));
478        assert!(debug.contains("id"));
479    }
480
481    #[test]
482    fn test_error_messages() {
483        let err = CsvError::TypeMismatch {
484            column: "age".to_string(),
485            expected: "integer".to_string(),
486            value: "abc".to_string(),
487        };
488        assert_eq!(
489            err.to_string(),
490            "Type mismatch in column 'age': expected integer, got 'abc'"
491        );
492    }
493
494    #[test]
495    fn test_with_context() {
496        let err = CsvError::ParseError {
497            line: 10,
498            message: "Invalid value".to_string(),
499        };
500        let with_ctx = err.with_context("in field 'name'".to_string());
501        assert_eq!(
502            with_ctx.to_string(),
503            "CSV parse error at line 10: Invalid value (in field 'name')"
504        );
505    }
506}