Skip to main content

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    /// Security limit violated.
288    ///
289    /// This error occurs when CSV data exceeds configured security limits to prevent
290    /// denial-of-service attacks.
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use hedl_csv::CsvError;
296    ///
297    /// let err = CsvError::Security {
298    ///     limit_type: "column count".to_string(),
299    ///     limit: 10_000,
300    ///     actual: 15_000,
301    ///     message: "CSV has 15000 columns, exceeds limit of 10000".to_string(),
302    /// };
303    /// assert!(err.to_string().contains("Security limit"));
304    /// ```
305    #[error("Security limit violated: {message}")]
306    Security {
307        /// Type of limit that was violated.
308        limit_type: String,
309        /// Configured limit value.
310        limit: usize,
311        /// Actual value encountered.
312        actual: usize,
313        /// Detailed error message.
314        message: String,
315    },
316}
317
318/// Convenience type alias for `Result` with `CsvError`.
319pub type Result<T> = std::result::Result<T, CsvError>;
320
321impl CsvError {
322    /// Add context to an error message.
323    ///
324    /// This is useful for providing additional information about where an error occurred.
325    ///
326    /// # Examples
327    ///
328    /// ```
329    /// use hedl_csv::CsvError;
330    ///
331    /// let err = CsvError::ParseError {
332    ///     line: 5,
333    ///     message: "Invalid value".to_string(),
334    /// };
335    /// let with_context = err.with_context("in column 'age' at line 10".to_string());
336    /// ```
337    #[must_use]
338    pub fn with_context(self, context: String) -> Self {
339        match self {
340            CsvError::ParseError { line, message } => CsvError::ParseError {
341                line,
342                message: format!("{message} ({context})"),
343            },
344            CsvError::HedlCore(msg) => CsvError::HedlCore(format!("{msg} ({context})")),
345            CsvError::Other(msg) => CsvError::Other(format!("{msg} ({context})")),
346            // For other variants, wrap in Other with context
347            other => CsvError::Other(format!("{other} ({context})")),
348        }
349    }
350
351    /// Create a security error for limit violations.
352    ///
353    /// This is a convenience method for creating Security error variants.
354    ///
355    /// # Examples
356    ///
357    /// ```
358    /// use hedl_csv::CsvError;
359    ///
360    /// let err = CsvError::security(
361    ///     "CSV has 15000 columns, exceeds limit of 10000".to_string(),
362    ///     0
363    /// );
364    /// assert!(matches!(err, CsvError::Security { .. }));
365    /// ```
366    #[must_use]
367    pub fn security(message: String, _line: usize) -> Self {
368        // Parse the message to extract limit information
369        // This is a simplified approach - the actual implementation will use structured data
370        CsvError::Security {
371            limit_type: "unknown".to_string(),
372            limit: 0,
373            actual: 0,
374            message,
375        }
376    }
377}
378
379impl From<hedl_core::HedlError> for CsvError {
380    fn from(err: hedl_core::HedlError) -> Self {
381        CsvError::HedlCore(err.to_string())
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_parse_error_display() {
391        let err = CsvError::ParseError {
392            line: 42,
393            message: "Invalid escape sequence".to_string(),
394        };
395        assert_eq!(
396            err.to_string(),
397            "CSV parse error at line 42: Invalid escape sequence"
398        );
399    }
400
401    #[test]
402    fn test_type_mismatch_display() {
403        let err = CsvError::TypeMismatch {
404            column: "age".to_string(),
405            expected: "integer".to_string(),
406            value: "abc".to_string(),
407        };
408        assert_eq!(
409            err.to_string(),
410            "Type mismatch in column 'age': expected integer, got 'abc'"
411        );
412    }
413
414    #[test]
415    fn test_missing_column_display() {
416        let err = CsvError::MissingColumn("id".to_string());
417        assert_eq!(err.to_string(), "Missing required column: id");
418    }
419
420    #[test]
421    fn test_invalid_header_display() {
422        let err = CsvError::InvalidHeader {
423            position: 3,
424            reason: "Empty column name".to_string(),
425        };
426        assert_eq!(
427            err.to_string(),
428            "Invalid header at position 3: Empty column name"
429        );
430    }
431
432    #[test]
433    fn test_width_mismatch_display() {
434        let err = CsvError::WidthMismatch {
435            expected: 5,
436            actual: 3,
437            row: 10,
438        };
439        assert_eq!(
440            err.to_string(),
441            "Row width mismatch: expected 5 columns, got 3 in row 10"
442        );
443    }
444
445    #[test]
446    fn test_security_limit_display() {
447        let err = CsvError::SecurityLimit {
448            limit: 1_000_000,
449            actual: 1_500_000,
450        };
451        assert_eq!(
452            err.to_string(),
453            "Security limit exceeded: row count 1500000 exceeds maximum 1000000"
454        );
455    }
456
457    #[test]
458    fn test_empty_id_display() {
459        let err = CsvError::EmptyId { row: 5 };
460        assert_eq!(err.to_string(), "Empty 'id' field at row 5");
461    }
462
463    #[test]
464    fn test_list_not_found_display() {
465        let err = CsvError::ListNotFound {
466            name: "people".to_string(),
467            available: "users, items".to_string(),
468        };
469        assert_eq!(
470            err.to_string(),
471            "Matrix list 'people' not found in document (available: users, items)"
472        );
473    }
474
475    #[test]
476    fn test_not_a_list_display() {
477        let err = CsvError::NotAList {
478            name: "value".to_string(),
479            actual_type: "scalar".to_string(),
480        };
481        assert_eq!(
482            err.to_string(),
483            "Item 'value' is not a matrix list (found: scalar)"
484        );
485    }
486
487    #[test]
488    fn test_no_lists_display() {
489        let err = CsvError::NoLists;
490        assert_eq!(err.to_string(), "No matrix lists found in document");
491    }
492
493    #[test]
494    fn test_invalid_utf8_display() {
495        let err = CsvError::InvalidUtf8 {
496            context: "CSV output".to_string(),
497        };
498        assert_eq!(err.to_string(), "Invalid UTF-8 in CSV output");
499    }
500
501    #[test]
502    fn test_other_display() {
503        let err = CsvError::Other("Custom error message".to_string());
504        assert_eq!(err.to_string(), "Custom error message");
505    }
506
507    #[test]
508    fn test_io_error_conversion() {
509        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
510        let csv_err = CsvError::from(io_err);
511        assert!(csv_err.to_string().contains("I/O error"));
512    }
513
514    #[test]
515    fn test_hedl_error_conversion() {
516        let hedl_err = hedl_core::HedlError::new(
517            hedl_core::HedlErrorKind::Syntax,
518            "Syntax error".to_string(),
519            1,
520        );
521        let csv_err = CsvError::from(hedl_err);
522        assert!(csv_err.to_string().contains("HEDL core error"));
523    }
524
525    #[test]
526    fn test_error_is_send_sync() {
527        fn assert_send_sync<T: Send + Sync>() {}
528        assert_send_sync::<CsvError>();
529    }
530
531    #[test]
532    fn test_error_debug() {
533        let err = CsvError::MissingColumn("id".to_string());
534        let debug = format!("{err:?}");
535        assert!(debug.contains("MissingColumn"));
536        assert!(debug.contains("id"));
537    }
538
539    #[test]
540    fn test_error_messages() {
541        let err = CsvError::TypeMismatch {
542            column: "age".to_string(),
543            expected: "integer".to_string(),
544            value: "abc".to_string(),
545        };
546        assert_eq!(
547            err.to_string(),
548            "Type mismatch in column 'age': expected integer, got 'abc'"
549        );
550    }
551
552    #[test]
553    fn test_with_context() {
554        let err = CsvError::ParseError {
555            line: 10,
556            message: "Invalid value".to_string(),
557        };
558        let with_ctx = err.with_context("in field 'name'".to_string());
559        assert_eq!(
560            with_ctx.to_string(),
561            "CSV parse error at line 10: Invalid value (in field 'name')"
562        );
563    }
564
565    #[test]
566    fn test_security_display() {
567        let err = CsvError::Security {
568            limit_type: "column count".to_string(),
569            limit: 10_000,
570            actual: 15_000,
571            message: "CSV has 15000 columns, exceeds limit of 10000".to_string(),
572        };
573        assert!(err.to_string().contains("Security limit"));
574        assert!(err.to_string().contains("15000"));
575    }
576
577    #[test]
578    fn test_security_error() {
579        let err = CsvError::security(
580            "CSV has 15000 columns, exceeds limit of 10000".to_string(),
581            0,
582        );
583        assert!(matches!(err, CsvError::Security { .. }));
584        assert!(err.to_string().contains("Security limit"));
585    }
586
587    #[test]
588    fn test_security_with_context() {
589        let err = CsvError::Security {
590            limit_type: "cell size".to_string(),
591            limit: 1_048_576,
592            actual: 2_000_000,
593            message: "Cell size exceeds limit".to_string(),
594        };
595        let with_ctx = err.with_context("at row 5, column 3".to_string());
596        assert!(with_ctx.to_string().contains("Cell size exceeds limit"));
597        assert!(with_ctx.to_string().contains("at row 5, column 3"));
598    }
599}