Skip to main content

plissken_core/
error.rs

1//! Unified error types for plissken-core.
2//!
3//! This module provides a single error enum that covers all error cases
4//! in the library, replacing the previous mix of `anyhow::Error`,
5//! `tera::Error`, and other error types.
6
7use std::path::PathBuf;
8use thiserror::Error;
9
10/// The primary error type for plissken-core operations.
11///
12/// This enum covers all error categories that can occur during parsing,
13/// rendering, and configuration handling.
14#[derive(Debug, Error)]
15pub enum PlisskenError {
16    // =========================================================================
17    // Configuration Errors
18    // =========================================================================
19    /// Configuration file not found at the expected path.
20    #[error("config not found: {path}")]
21    ConfigNotFound {
22        /// Path where config was expected
23        path: PathBuf,
24    },
25
26    /// Failed to parse configuration file.
27    #[error("config parse error: {message}")]
28    ConfigParse {
29        /// Description of the parse error
30        message: String,
31        /// The underlying TOML parse error, if available
32        #[source]
33        source: Option<toml::de::Error>,
34    },
35
36    /// Configuration validation failed.
37    #[error("config validation failed: {message}")]
38    ConfigValidation {
39        /// Description of the validation error
40        message: String,
41    },
42
43    // =========================================================================
44    // Parse Errors
45    // =========================================================================
46    /// Failed to parse a source file.
47    #[error("failed to parse {language} file '{path}': {message}")]
48    Parse {
49        /// The language being parsed (e.g., "Rust", "Python")
50        language: String,
51        /// Path to the file that failed to parse
52        path: PathBuf,
53        /// Line number where error occurred, if known
54        line: Option<usize>,
55        /// Description of the parse error
56        message: String,
57    },
58
59    /// Failed to read a source file.
60    #[error("failed to read file '{path}': {message}")]
61    FileRead {
62        /// Path to the file that couldn't be read
63        path: PathBuf,
64        /// Description of the error
65        message: String,
66        /// The underlying IO error
67        #[source]
68        source: std::io::Error,
69    },
70
71    // =========================================================================
72    // Render Errors
73    // =========================================================================
74    /// Template rendering failed.
75    #[error("template error: {message}")]
76    Template {
77        /// Description of the template error
78        message: String,
79        /// The underlying Tera error
80        #[source]
81        source: tera::Error,
82    },
83
84    /// Failed to write output file.
85    #[error("failed to write output '{path}': {message}")]
86    OutputWrite {
87        /// Path to the output file
88        path: PathBuf,
89        /// Description of the error
90        message: String,
91        /// The underlying IO error
92        #[source]
93        source: std::io::Error,
94    },
95
96    // =========================================================================
97    // Cross-Reference Errors
98    // =========================================================================
99    /// Cross-reference resolution failed.
100    #[error("cross-reference error: {message}")]
101    CrossRef {
102        /// Description of the cross-reference error
103        message: String,
104    },
105
106    // =========================================================================
107    // IO Errors
108    // =========================================================================
109    /// Generic IO error with context.
110    #[error("{context}: {source}")]
111    Io {
112        /// Context describing what operation failed
113        context: String,
114        /// The underlying IO error
115        #[source]
116        source: std::io::Error,
117    },
118
119    // =========================================================================
120    // Discovery Errors
121    // =========================================================================
122    /// Module discovery failed.
123    #[error("module discovery failed in '{path}': {message}")]
124    Discovery {
125        /// Directory being scanned
126        path: PathBuf,
127        /// Description of the error
128        message: String,
129    },
130
131    // =========================================================================
132    // Manifest Errors
133    // =========================================================================
134    /// Failed to parse manifest file (Cargo.toml or pyproject.toml).
135    #[error("failed to parse manifest '{path}': {message}")]
136    ManifestParse {
137        /// Path to the manifest file
138        path: PathBuf,
139        /// Description of the error
140        message: String,
141    },
142}
143
144/// A specialized Result type for plissken operations.
145pub type Result<T> = std::result::Result<T, PlisskenError>;
146
147// =============================================================================
148// From implementations for automatic error conversion
149// =============================================================================
150
151impl From<std::io::Error> for PlisskenError {
152    fn from(err: std::io::Error) -> Self {
153        PlisskenError::Io {
154            context: "IO operation failed".into(),
155            source: err,
156        }
157    }
158}
159
160impl From<tera::Error> for PlisskenError {
161    fn from(err: tera::Error) -> Self {
162        PlisskenError::Template {
163            message: err.to_string(),
164            source: err,
165        }
166    }
167}
168
169impl From<toml::de::Error> for PlisskenError {
170    fn from(err: toml::de::Error) -> Self {
171        PlisskenError::ConfigParse {
172            message: err.to_string(),
173            source: Some(err),
174        }
175    }
176}
177
178// Convert from our existing ConfigError
179impl From<crate::config::ConfigError> for PlisskenError {
180    fn from(err: crate::config::ConfigError) -> Self {
181        PlisskenError::ConfigValidation {
182            message: err.to_string(),
183        }
184    }
185}
186
187// Convert from ManifestError
188impl From<crate::manifest::ManifestError> for PlisskenError {
189    fn from(err: crate::manifest::ManifestError) -> Self {
190        PlisskenError::ManifestParse {
191            path: PathBuf::new(),
192            message: err.to_string(),
193        }
194    }
195}
196
197// =============================================================================
198// Helper constructors for common error patterns
199// =============================================================================
200
201impl PlisskenError {
202    /// Create a parse error for a Rust file.
203    pub fn rust_parse(path: impl Into<PathBuf>, message: impl Into<String>) -> Self {
204        PlisskenError::Parse {
205            language: "Rust".into(),
206            path: path.into(),
207            line: None,
208            message: message.into(),
209        }
210    }
211
212    /// Create a parse error for a Rust file with line number.
213    pub fn rust_parse_at(
214        path: impl Into<PathBuf>,
215        line: usize,
216        message: impl Into<String>,
217    ) -> Self {
218        PlisskenError::Parse {
219            language: "Rust".into(),
220            path: path.into(),
221            line: Some(line),
222            message: message.into(),
223        }
224    }
225
226    /// Create a parse error for a Python file.
227    pub fn python_parse(path: impl Into<PathBuf>, message: impl Into<String>) -> Self {
228        PlisskenError::Parse {
229            language: "Python".into(),
230            path: path.into(),
231            line: None,
232            message: message.into(),
233        }
234    }
235
236    /// Create a parse error for a Python file with line number.
237    pub fn python_parse_at(
238        path: impl Into<PathBuf>,
239        line: usize,
240        message: impl Into<String>,
241    ) -> Self {
242        PlisskenError::Parse {
243            language: "Python".into(),
244            path: path.into(),
245            line: Some(line),
246            message: message.into(),
247        }
248    }
249
250    /// Create a file read error.
251    pub fn file_read(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
252        let path = path.into();
253        PlisskenError::FileRead {
254            message: source.to_string(),
255            path,
256            source,
257        }
258    }
259
260    /// Create an IO error with context.
261    pub fn io(context: impl Into<String>, source: std::io::Error) -> Self {
262        PlisskenError::Io {
263            context: context.into(),
264            source,
265        }
266    }
267
268    /// Create a config not found error.
269    pub fn config_not_found(path: impl Into<PathBuf>) -> Self {
270        PlisskenError::ConfigNotFound { path: path.into() }
271    }
272
273    /// Create a discovery error.
274    pub fn discovery(path: impl Into<PathBuf>, message: impl Into<String>) -> Self {
275        PlisskenError::Discovery {
276            path: path.into(),
277            message: message.into(),
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_parse_error_display() {
288        let err = PlisskenError::rust_parse("/path/to/file.rs", "unexpected token");
289        assert!(err.to_string().contains("Rust"));
290        assert!(err.to_string().contains("/path/to/file.rs"));
291        assert!(err.to_string().contains("unexpected token"));
292    }
293
294    #[test]
295    fn test_parse_error_with_line() {
296        let err = PlisskenError::python_parse_at("/path/to/file.py", 42, "syntax error");
297        assert!(err.to_string().contains("Python"));
298        assert!(err.to_string().contains("/path/to/file.py"));
299    }
300
301    #[test]
302    fn test_config_not_found_display() {
303        let err = PlisskenError::config_not_found("/project/plissken.toml");
304        assert!(err.to_string().contains("config not found"));
305        assert!(err.to_string().contains("plissken.toml"));
306    }
307
308    #[test]
309    fn test_config_validation_error() {
310        let err = PlisskenError::ConfigValidation {
311            message: "no language configured".into(),
312        };
313        assert!(err.to_string().contains("config validation failed"));
314        assert!(err.to_string().contains("no language configured"));
315    }
316
317    #[test]
318    fn test_io_error_conversion() {
319        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
320        let err: PlisskenError = io_err.into();
321        assert!(err.to_string().contains("IO operation failed"));
322    }
323
324    #[test]
325    fn test_template_error_conversion() {
326        // Create a tera error by trying to parse invalid template
327        let tera_result = tera::Tera::one_off("{{ invalid", &tera::Context::new(), false);
328        if let Err(tera_err) = tera_result {
329            let err: PlisskenError = tera_err.into();
330            assert!(err.to_string().contains("template error"));
331        }
332    }
333
334    #[test]
335    fn test_discovery_error() {
336        let err = PlisskenError::discovery("/src/python", "permission denied");
337        assert!(err.to_string().contains("module discovery failed"));
338        assert!(err.to_string().contains("/src/python"));
339    }
340
341    #[test]
342    fn test_file_read_error() {
343        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
344        let err = PlisskenError::file_read("/path/to/file.rs", io_err);
345        assert!(err.to_string().contains("failed to read file"));
346        assert!(err.to_string().contains("/path/to/file.rs"));
347    }
348
349    #[test]
350    fn test_crossref_error() {
351        let err = PlisskenError::CrossRef {
352            message: "unresolved reference".into(),
353        };
354        assert!(err.to_string().contains("cross-reference error"));
355    }
356}