Skip to main content

alimentar/
error.rs

1//! Error types for alimentar.
2
3use std::path::PathBuf;
4
5/// Result type alias for alimentar operations.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors that can occur in alimentar operations.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum Error {
12    /// I/O error during file operations.
13    #[error("I/O error at {path:?}: {source}")]
14    Io {
15        /// The path where the error occurred, if known.
16        path: Option<PathBuf>,
17        /// The underlying I/O error.
18        #[source]
19        source: std::io::Error,
20    },
21
22    /// Arrow error during data processing.
23    #[error("Arrow error: {0}")]
24    Arrow(#[from] arrow::error::ArrowError),
25
26    /// Parquet error during file operations.
27    #[error("Parquet error: {0}")]
28    Parquet(#[from] parquet::errors::ParquetError),
29
30    /// Index out of bounds when accessing dataset.
31    #[error("Index {index} out of bounds for dataset with {len} rows")]
32    IndexOutOfBounds {
33        /// The requested index.
34        index: usize,
35        /// The actual length of the dataset.
36        len: usize,
37    },
38
39    /// Column not found in schema.
40    #[error("Column '{name}' not found in schema")]
41    ColumnNotFound {
42        /// The name of the missing column.
43        name: String,
44    },
45
46    /// Invalid configuration.
47    #[error("Invalid configuration: {message}")]
48    InvalidConfig {
49        /// Description of the configuration error.
50        message: String,
51    },
52
53    /// Unsupported file format.
54    #[error("Unsupported format: {format}")]
55    UnsupportedFormat {
56        /// The unsupported format name or extension.
57        format: String,
58    },
59
60    /// Empty dataset error.
61    #[error("Dataset is empty")]
62    EmptyDataset,
63
64    /// Schema mismatch between datasets or batches.
65    #[error("Schema mismatch: {message}")]
66    SchemaMismatch {
67        /// Description of the schema mismatch.
68        message: String,
69    },
70
71    /// Backend storage error.
72    #[error("Storage backend error: {message}")]
73    Storage {
74        /// Description of the storage error.
75        message: String,
76    },
77
78    /// Transform error.
79    #[error("Transform error: {message}")]
80    Transform {
81        /// Description of the transform error.
82        message: String,
83    },
84
85    /// Parse error.
86    #[error("Parse error: {message}")]
87    Parse {
88        /// Description of the parse error.
89        message: String,
90    },
91
92    /// Data error.
93    #[error("Data error: {message}")]
94    Data {
95        /// Description of the data error.
96        message: String,
97    },
98
99    /// Format error (header, checksum, etc.).
100    #[error("Format error: {0}")]
101    Format(String),
102
103    /// Checksum mismatch.
104    #[error("Checksum mismatch: expected {expected:08X}, got {actual:08X}")]
105    ChecksumMismatch {
106        /// Expected checksum.
107        expected: u32,
108        /// Actual checksum.
109        actual: u32,
110    },
111
112    /// License has expired.
113    #[error("License expired at {expired_at} (current time: {current_time})")]
114    LicenseExpired {
115        /// Expiration timestamp (Unix epoch seconds).
116        expired_at: u64,
117        /// Current timestamp (Unix epoch seconds).
118        current_time: u64,
119    },
120
121    /// Signature verification failed.
122    #[error("Signature verification failed")]
123    SignatureInvalid,
124
125    /// Decryption failed.
126    #[error("Decryption failed: wrong password or corrupted data")]
127    DecryptionFailed,
128
129    /// Resource not found.
130    #[error("Not found: {0}")]
131    NotFound(String),
132
133    /// Invalid format for output/conversion.
134    #[error("Invalid format: {0}")]
135    InvalidFormat(String),
136}
137
138impl Error {
139    /// Create an I/O error with a path context.
140    pub fn io(source: std::io::Error, path: impl Into<PathBuf>) -> Self {
141        Self::Io {
142            path: Some(path.into()),
143            source,
144        }
145    }
146
147    /// Create an I/O error without path context.
148    pub fn io_no_path(source: std::io::Error) -> Self {
149        Self::Io { path: None, source }
150    }
151
152    /// Create a column not found error.
153    pub fn column_not_found(name: impl Into<String>) -> Self {
154        Self::ColumnNotFound { name: name.into() }
155    }
156
157    /// Create an invalid configuration error.
158    pub fn invalid_config(message: impl Into<String>) -> Self {
159        Self::InvalidConfig {
160            message: message.into(),
161        }
162    }
163
164    /// Create an unsupported format error.
165    pub fn unsupported_format(format: impl Into<String>) -> Self {
166        Self::UnsupportedFormat {
167            format: format.into(),
168        }
169    }
170
171    /// Create a schema mismatch error.
172    pub fn schema_mismatch(message: impl Into<String>) -> Self {
173        Self::SchemaMismatch {
174            message: message.into(),
175        }
176    }
177
178    /// Create a storage error.
179    pub fn storage(message: impl Into<String>) -> Self {
180        Self::Storage {
181            message: message.into(),
182        }
183    }
184
185    /// Create a transform error.
186    pub fn transform(message: impl Into<String>) -> Self {
187        Self::Transform {
188            message: message.into(),
189        }
190    }
191
192    /// Create a parse error.
193    pub fn parse(message: impl Into<String>) -> Self {
194        Self::Parse {
195            message: message.into(),
196        }
197    }
198
199    /// Create a data error.
200    pub fn data(message: impl Into<String>) -> Self {
201        Self::Data {
202            message: message.into(),
203        }
204    }
205
206    /// Create an empty dataset error.
207    #[must_use]
208    pub fn empty_dataset(_name: &str) -> Self {
209        Self::EmptyDataset
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_io_error_with_path() {
219        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
220        let err = Error::io(io_err, "/path/to/file");
221        assert!(err.to_string().contains("/path/to/file"));
222        assert!(err.to_string().contains("file not found"));
223    }
224
225    #[test]
226    fn test_io_error_without_path() {
227        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
228        let err = Error::io_no_path(io_err);
229        assert!(err.to_string().contains("None"));
230    }
231
232    #[test]
233    fn test_index_out_of_bounds() {
234        let err = Error::IndexOutOfBounds { index: 10, len: 5 };
235        assert!(err.to_string().contains("10"));
236        assert!(err.to_string().contains('5'));
237    }
238
239    #[test]
240    fn test_column_not_found() {
241        let err = Error::column_not_found("my_column");
242        assert!(err.to_string().contains("my_column"));
243    }
244
245    #[test]
246    fn test_invalid_config() {
247        let err = Error::invalid_config("batch_size must be positive");
248        assert!(err.to_string().contains("batch_size must be positive"));
249    }
250
251    #[test]
252    fn test_unsupported_format() {
253        let err = Error::unsupported_format("xlsx");
254        assert!(err.to_string().contains("xlsx"));
255    }
256
257    #[test]
258    fn test_schema_mismatch() {
259        let err = Error::schema_mismatch("expected Int64, got Utf8");
260        assert!(err.to_string().contains("expected Int64, got Utf8"));
261    }
262
263    #[test]
264    fn test_storage_error() {
265        let err = Error::storage("connection refused");
266        assert!(err.to_string().contains("connection refused"));
267    }
268
269    #[test]
270    fn test_transform_error() {
271        let err = Error::transform("filter predicate failed");
272        assert!(err.to_string().contains("filter predicate failed"));
273    }
274
275    #[test]
276    fn test_empty_dataset() {
277        let err = Error::EmptyDataset;
278        assert!(err.to_string().contains("empty"));
279    }
280
281    #[test]
282    fn test_empty_dataset_constructor() {
283        let err = Error::empty_dataset("test");
284        assert!(err.to_string().contains("empty"));
285    }
286
287    #[test]
288    fn test_parse_error() {
289        let err = Error::parse("invalid JSON syntax");
290        assert!(err.to_string().contains("invalid JSON syntax"));
291    }
292
293    #[test]
294    fn test_data_error() {
295        let err = Error::data("corrupted record");
296        assert!(err.to_string().contains("corrupted record"));
297    }
298
299    #[test]
300    fn test_format_error() {
301        let err = Error::Format("invalid magic bytes".to_string());
302        assert!(err.to_string().contains("invalid magic bytes"));
303    }
304
305    #[test]
306    fn test_checksum_mismatch() {
307        let err = Error::ChecksumMismatch {
308            expected: 0xDEADBEEF,
309            actual: 0xCAFEBABE,
310        };
311        let msg = err.to_string();
312        assert!(msg.contains("DEADBEEF"));
313        assert!(msg.contains("CAFEBABE"));
314    }
315
316    #[test]
317    fn test_license_expired() {
318        let err = Error::LicenseExpired {
319            expired_at: 1700000000,
320            current_time: 1700100000,
321        };
322        let msg = err.to_string();
323        assert!(msg.contains("expired"));
324        assert!(msg.contains("1700000000"));
325        assert!(msg.contains("1700100000"));
326    }
327
328    #[test]
329    fn test_signature_invalid() {
330        let err = Error::SignatureInvalid;
331        assert!(err.to_string().contains("Signature verification failed"));
332    }
333
334    #[test]
335    fn test_decryption_failed() {
336        let err = Error::DecryptionFailed;
337        assert!(err.to_string().contains("Decryption failed"));
338    }
339}