1use std::path::PathBuf;
2use thiserror::Error;
3
4pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Error, Debug, Clone)]
9#[non_exhaustive]
10pub enum Error {
11 #[error("IO error accessing '{path}': {message}")]
13 Io {
14 path: PathBuf,
16 message: String,
18 },
19
20 #[error("Failed to render template '{template}': {message}")]
22 Template {
23 template: String,
25 message: String,
27 },
28
29 #[error("Template validation failed for '{template}': {reason}")]
31 TemplateValidation {
32 template: String,
34 reason: String,
36 },
37
38 #[error("Invalid configuration: {message}")]
40 Config {
41 message: String,
43 },
44
45 #[error("File '{path}' is too large: {size} tokens exceeds limit of {limit} tokens")]
47 FileTooLarge {
48 path: PathBuf,
50 size: usize,
52 limit: usize,
54 },
55
56 #[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 path: PathBuf,
61 },
62
63 #[error("Serialization error: {message}")]
65 Serialization {
66 message: String,
68 },
69
70 #[error("Invalid UTF-8 encoding in file '{path}'. File may be binary or use unsupported encoding.")]
72 InvalidUtf8 {
73 path: PathBuf,
75 },
76
77 #[error("System time error: {message}")]
79 SystemTime {
80 message: String,
82 },
83
84 #[error("Multiple errors occurred during processing ({count} errors)")]
86 Multiple {
87 count: usize,
89 errors: Vec<Error>,
91 },
92
93 #[error("Invalid output pattern '{pattern}': {reason}")]
95 InvalidPattern {
96 pattern: String,
98 reason: String,
100 },
101}
102
103impl Error {
104 #[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 #[must_use]
115 pub fn config(message: impl Into<String>) -> Self {
116 Self::Config {
117 message: message.into(),
118 }
119 }
120
121 #[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 #[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 #[must_use]
141 pub fn invalid_utf8(path: impl Into<PathBuf>) -> Self {
142 Self::InvalidUtf8 { path: path.into() }
143 }
144
145 #[must_use]
147 pub fn no_files(path: impl Into<PathBuf>) -> Self {
148 Self::NoFiles { path: path.into() }
149 }
150
151 #[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 #[must_use]
162 pub fn multiple(errors: Vec<Self>) -> Self {
163 let count = errors.len();
164 Self::Multiple { count, errors }
165 }
166
167 #[must_use]
169 pub const fn is_io(&self) -> bool {
170 matches!(self, Self::Io { .. })
171 }
172
173 #[must_use]
175 pub const fn is_config(&self) -> bool {
176 matches!(self, Self::Config { .. })
177 }
178}
179
180impl 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 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}