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}