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}