llm_utl/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4/// Result type alias using the library's Error type.
5pub type Result<T> = std::result::Result<T, Error>;
6
7/// Comprehensive error types for the llm-utl library.
8#[derive(Error, Debug, Clone)]
9#[non_exhaustive]
10pub enum Error {
11    /// IO error with context about the file path.
12    #[error("IO error accessing '{path}': {message}")]
13    Io {
14        /// Path where the error occurred
15        path: PathBuf,
16        /// Error message
17        message: String,
18    },
19
20    /// Template rendering error.
21    #[error("Failed to render template '{template}': {message}")]
22    Template {
23        /// Template name
24        template: String,
25        /// Error message
26        message: String,
27    },
28
29    /// Template validation error.
30    #[error("Template validation failed for '{template}': {reason}")]
31    TemplateValidation {
32        /// Template name or path
33        template: String,
34        /// Validation failure reason
35        reason: String,
36    },
37
38    /// Configuration validation error.
39    #[error("Invalid configuration: {message}")]
40    Config {
41        /// Detailed error message
42        message: String,
43    },
44
45    /// File exceeds maximum token limit.
46    #[error("File '{path}' is too large: {size} tokens exceeds limit of {limit} tokens")]
47    FileTooLarge {
48        /// Path to the oversized file
49        path: PathBuf,
50        /// Actual token count
51        size: usize,
52        /// Maximum allowed tokens
53        limit: usize,
54    },
55
56    /// No processable files found in directory.
57    #[error("No processable files found in '{path}'.\n\nPossible causes:\n  • Directory is empty or contains only ignored files\n  • All files are excluded by .gitignore patterns\n  • Wrong directory specified (use --dir to set the correct path)\n\nExample: llm-utl --dir ./my-project --out ./prompts")]
58    NoFiles {
59        /// Directory that was scanned
60        path: PathBuf,
61    },
62
63    /// JSON serialization error.
64    #[error("Serialization error: {message}")]
65    Serialization {
66        /// Error message
67        message: String,
68    },
69
70    /// Invalid UTF-8 encountered in file.
71    #[error("Invalid UTF-8 encoding in file '{path}'. File may be binary or use unsupported encoding.")]
72    InvalidUtf8 {
73        /// Path to file with encoding issues
74        path: PathBuf,
75    },
76
77    /// System time error.
78    #[error("System time error: {message}")]
79    SystemTime {
80        /// Error message
81        message: String,
82    },
83
84    /// Multiple errors occurred during processing.
85    #[error("Multiple errors occurred during processing ({count} errors)")]
86    Multiple {
87        /// Number of errors
88        count: usize,
89        /// Collection of errors
90        errors: Vec<Error>,
91    },
92
93    /// Invalid output pattern.
94    #[error("Invalid output pattern '{pattern}': {reason}")]
95    InvalidPattern {
96        /// The invalid pattern
97        pattern: String,
98        /// Reason why it's invalid
99        reason: String,
100    },
101}
102
103impl Error {
104    /// Creates an IO error with path context.
105    #[must_use]
106    pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
107        Self::Io {
108            path: path.into(),
109            message: source.to_string(),
110        }
111    }
112
113    /// Creates a configuration error.
114    #[must_use]
115    pub fn config(message: impl Into<String>) -> Self {
116        Self::Config {
117            message: message.into(),
118        }
119    }
120
121    /// Creates a template error.
122    #[must_use]
123    pub fn template(template: impl Into<String>, source: tera::Error) -> Self {
124        Self::Template {
125            template: template.into(),
126            message: source.to_string(),
127        }
128    }
129
130    /// Creates a template validation error.
131    #[must_use]
132    pub fn template_validation(template: impl Into<String>, reason: impl Into<String>) -> Self {
133        Self::TemplateValidation {
134            template: template.into(),
135            reason: reason.into(),
136        }
137    }
138
139    /// Creates an invalid UTF-8 error.
140    #[must_use]
141    pub fn invalid_utf8(path: impl Into<PathBuf>) -> Self {
142        Self::InvalidUtf8 { path: path.into() }
143    }
144
145    /// Creates a no files error.
146    #[must_use]
147    pub fn no_files(path: impl Into<PathBuf>) -> Self {
148        Self::NoFiles { path: path.into() }
149    }
150
151    /// Creates an invalid pattern error.
152    #[must_use]
153    pub fn invalid_pattern(pattern: impl Into<String>, reason: impl Into<String>) -> Self {
154        Self::InvalidPattern {
155            pattern: pattern.into(),
156            reason: reason.into(),
157        }
158    }
159
160    /// Combines multiple errors into a single error.
161    #[must_use]
162    pub fn multiple(errors: Vec<Self>) -> Self {
163        let count = errors.len();
164        Self::Multiple { count, errors }
165    }
166
167    /// Returns true if this is an IO error.
168    #[must_use]
169    pub const fn is_io(&self) -> bool {
170        matches!(self, Self::Io { .. })
171    }
172
173    /// Returns true if this is a configuration error.
174    #[must_use]
175    pub const fn is_config(&self) -> bool {
176        matches!(self, Self::Config { .. })
177    }
178}
179
180// Conversion implementations for convenient error handling
181impl From<std::time::SystemTimeError> for Error {
182    fn from(e: std::time::SystemTimeError) -> Self {
183        Self::SystemTime {
184            message: e.to_string(),
185        }
186    }
187}
188
189impl From<tera::Error> for Error {
190    fn from(e: tera::Error) -> Self {
191        Self::Template {
192            template: "unknown".to_string(),
193            message: e.to_string(),
194        }
195    }
196}
197
198impl From<serde_json::Error> for Error {
199    fn from(e: serde_json::Error) -> Self {
200        Self::Serialization {
201            message: e.to_string(),
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_error_creation() {
212        let err = Error::config("test message");
213        assert!(err.is_config());
214        assert!(err.to_string().contains("test message"));
215    }
216
217    #[test]
218    fn test_io_error() {
219        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
220        let err = Error::io("/tmp/test.txt", io_err);
221        assert!(err.is_io());
222        assert!(err.to_string().contains("/tmp/test.txt"));
223    }
224
225    #[test]
226    fn test_multiple_errors() {
227        let errors = vec![
228            Error::config("error 1"),
229            Error::config("error 2"),
230        ];
231        let combined = Error::multiple(errors);
232        assert!(combined.to_string().contains("2 errors"));
233    }
234
235    #[test]
236    fn test_error_clone() {
237        let err = Error::config("test");
238        let cloned = err.clone();
239        assert_eq!(err.to_string(), cloned.to_string());
240    }
241
242    #[test]
243    fn test_serialization_error() {
244        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
245        let err: Error = json_err.into();
246        assert!(err.to_string().contains("Serialization error"));
247    }
248
249    #[test]
250    fn test_system_time_error() {
251        use std::time::{Duration, SystemTime};
252
253        // Create a time error by using invalid arithmetic
254        let past = SystemTime::UNIX_EPOCH;
255        let future = past + Duration::from_secs(1);
256        let result = past.duration_since(future);
257
258        if let Err(e) = result {
259            let err: Error = e.into();
260            assert!(err.to_string().contains("System time error"));
261        }
262    }
263}