Skip to main content

lechange_core/
error.rs

1//! Error types for lechange-core
2
3use std::fmt;
4
5/// Result type alias for lechange operations
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Main error type for lechange operations
9#[derive(Debug)]
10pub enum Error {
11    /// Git operation error
12    Git(String),
13
14    /// Invalid configuration
15    Config(String),
16
17    /// Invalid path
18    InvalidPath(String),
19
20    /// I/O error
21    Io(std::io::Error),
22
23    /// Runtime error (Tokio, threading, etc.)
24    Runtime(String),
25
26    /// Pattern matching error
27    Pattern(String),
28
29    /// HTTP/API error
30    Http(String),
31
32    /// Workflow API error
33    Workflow(String),
34
35    /// Workflow timeout error
36    WorkflowTimeout(String),
37
38    /// API rate limit exceeded
39    RateLimitExceeded(String),
40
41    /// File recovery error
42    Recovery(String),
43
44    /// YAML parsing error
45    Yaml(String),
46
47    /// GitHub event parsing error
48    EventParse(String),
49
50    /// Shallow clone depth exhausted
51    ShallowExhausted(String),
52
53    /// Other errors
54    Other(String),
55}
56
57impl fmt::Display for Error {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Error::Git(msg) => write!(f, "Git error: {}", msg),
61            Error::Config(msg) => write!(f, "Configuration error: {}", msg),
62            Error::InvalidPath(path) => write!(f, "Invalid path: {}", path),
63            Error::Io(err) => write!(f, "I/O error: {}", err),
64            Error::Runtime(msg) => write!(f, "Runtime error: {}", msg),
65            Error::Pattern(msg) => write!(f, "Pattern error: {}", msg),
66            Error::Http(msg) => write!(f, "HTTP error: {}", msg),
67            Error::Workflow(msg) => write!(f, "Workflow error: {}", msg),
68            Error::WorkflowTimeout(msg) => write!(f, "Workflow timeout: {}", msg),
69            Error::RateLimitExceeded(msg) => write!(f, "Rate limit exceeded: {}", msg),
70            Error::Recovery(msg) => write!(f, "Recovery error: {}", msg),
71            Error::Yaml(msg) => write!(f, "YAML error: {}", msg),
72            Error::EventParse(msg) => write!(f, "Event parse error: {}", msg),
73            Error::ShallowExhausted(msg) => write!(f, "Shallow clone exhausted: {}", msg),
74            Error::Other(msg) => write!(f, "Error: {}", msg),
75        }
76    }
77}
78
79impl std::error::Error for Error {
80    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81        match self {
82            Error::Io(err) => Some(err),
83            _ => None,
84        }
85    }
86}
87
88impl From<std::io::Error> for Error {
89    fn from(err: std::io::Error) -> Self {
90        Error::Io(err)
91    }
92}
93
94impl From<git2::Error> for Error {
95    fn from(err: git2::Error) -> Self {
96        Error::Git(err.to_string())
97    }
98}
99
100impl From<globset::Error> for Error {
101    fn from(err: globset::Error) -> Self {
102        Error::Pattern(err.to_string())
103    }
104}
105
106impl From<reqwest::Error> for Error {
107    fn from(err: reqwest::Error) -> Self {
108        Error::Http(err.to_string())
109    }
110}
111
112impl From<serde_json::Error> for Error {
113    fn from(err: serde_json::Error) -> Self {
114        Error::Other(format!("JSON error: {}", err))
115    }
116}
117
118impl From<serde_norway::Error> for Error {
119    fn from(err: serde_norway::Error) -> Self {
120        Error::Yaml(err.to_string())
121    }
122}
123
124/// Fieldless error category for zero-cost pattern matching.
125///
126/// Single byte representation (`#[repr(u8)]`), `Copy`, no allocations.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128#[repr(u8)]
129pub enum ErrorKind {
130    /// Git operation error
131    Git,
132    /// Invalid file path error
133    InvalidPath,
134    /// Configuration error
135    Config,
136    /// I/O operation error
137    Io,
138    /// Runtime error
139    Runtime,
140    /// Pattern matching error
141    Pattern,
142    /// HTTP/API error
143    Http,
144    /// Workflow API error
145    Workflow,
146    /// Workflow timeout error
147    WorkflowTimeout,
148    /// API rate limit exceeded
149    RateLimitExceeded,
150    /// File recovery error
151    Recovery,
152    /// YAML parsing error
153    Yaml,
154    /// GitHub event parsing error
155    EventParse,
156    /// Shallow clone depth exhausted
157    ShallowExhausted,
158    /// Other errors
159    Other,
160}
161
162impl Error {
163    /// Get the error kind — zero allocation, returns a Copy enum.
164    #[inline]
165    pub const fn kind(&self) -> ErrorKind {
166        match self {
167            Error::Git(_) => ErrorKind::Git,
168            Error::InvalidPath(_) => ErrorKind::InvalidPath,
169            Error::Config(_) => ErrorKind::Config,
170            Error::Io(_) => ErrorKind::Io,
171            Error::Runtime(_) => ErrorKind::Runtime,
172            Error::Pattern(_) => ErrorKind::Pattern,
173            Error::Http(_) => ErrorKind::Http,
174            Error::Workflow(_) => ErrorKind::Workflow,
175            Error::WorkflowTimeout(_) => ErrorKind::WorkflowTimeout,
176            Error::RateLimitExceeded(_) => ErrorKind::RateLimitExceeded,
177            Error::Recovery(_) => ErrorKind::Recovery,
178            Error::Yaml(_) => ErrorKind::Yaml,
179            Error::EventParse(_) => ErrorKind::EventParse,
180            Error::ShallowExhausted(_) => ErrorKind::ShallowExhausted,
181            Error::Other(_) => ErrorKind::Other,
182        }
183    }
184
185    /// Borrow the error message — zero allocation.
186    #[inline]
187    pub fn message(&self) -> &str {
188        match self {
189            Error::Git(msg)
190            | Error::Config(msg)
191            | Error::InvalidPath(msg)
192            | Error::Runtime(msg)
193            | Error::Pattern(msg)
194            | Error::Http(msg)
195            | Error::Workflow(msg)
196            | Error::WorkflowTimeout(msg)
197            | Error::RateLimitExceeded(msg)
198            | Error::Recovery(msg)
199            | Error::Yaml(msg)
200            | Error::EventParse(msg)
201            | Error::ShallowExhausted(msg)
202            | Error::Other(msg) => msg,
203            Error::Io(_) => "I/O error",
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_error_kind_is_copy() {
214        let err = Error::Git("test".to_string());
215        let k = err.kind();
216        let k2 = k; // Copy — no move
217        assert_eq!(k, k2);
218    }
219
220    #[test]
221    fn test_error_kind_zero_alloc() {
222        // ErrorKind is a fieldless enum — no String data
223        assert_eq!(std::mem::size_of::<ErrorKind>(), 1);
224    }
225
226    #[test]
227    fn test_error_message_borrows() {
228        let err = Error::Config("bad config".to_string());
229        let msg: &str = err.message();
230        assert_eq!(msg, "bad config");
231        // msg borrows from err — no allocation
232    }
233
234    #[test]
235    fn test_all_error_variants_have_kind() {
236        let cases: Vec<(Error, ErrorKind)> = vec![
237            (Error::Git("g".into()), ErrorKind::Git),
238            (Error::InvalidPath("p".into()), ErrorKind::InvalidPath),
239            (Error::Config("c".into()), ErrorKind::Config),
240            (Error::Io(std::io::Error::other("io")), ErrorKind::Io),
241            (Error::Runtime("r".into()), ErrorKind::Runtime),
242            (Error::Pattern("pat".into()), ErrorKind::Pattern),
243            (Error::Http("h".into()), ErrorKind::Http),
244            (Error::Workflow("w".into()), ErrorKind::Workflow),
245            (
246                Error::WorkflowTimeout("wt".into()),
247                ErrorKind::WorkflowTimeout,
248            ),
249            (
250                Error::RateLimitExceeded("rl".into()),
251                ErrorKind::RateLimitExceeded,
252            ),
253            (Error::Recovery("rec".into()), ErrorKind::Recovery),
254            (Error::Yaml("y".into()), ErrorKind::Yaml),
255            (Error::EventParse("ep".into()), ErrorKind::EventParse),
256            (
257                Error::ShallowExhausted("se".into()),
258                ErrorKind::ShallowExhausted,
259            ),
260            (Error::Other("o".into()), ErrorKind::Other),
261        ];
262
263        for (err, expected_kind) in cases {
264            assert_eq!(err.kind(), expected_kind, "Mismatch for {:?}", err);
265        }
266    }
267
268    #[test]
269    fn test_error_kind_repr_u8() {
270        assert_eq!(std::mem::size_of::<ErrorKind>(), 1);
271    }
272
273    #[test]
274    fn test_error_messages_never_contain_token_patterns() {
275        // Verify that all error variant messages don't accidentally include
276        // GitHub token patterns (ghp_, gho_, ghs_, github_pat_)
277        let token_patterns = ["ghp_", "gho_", "ghs_", "github_pat_", "Bearer "];
278        let errors: Vec<Error> = vec![
279            Error::Git("git error".into()),
280            Error::Config("config error".into()),
281            Error::Http("http error".into()),
282            Error::Workflow("workflow error".into()),
283            Error::Runtime("runtime error".into()),
284            Error::RateLimitExceeded("rate limit exceeded".into()),
285        ];
286
287        for err in &errors {
288            let msg = err.message();
289            let display = format!("{}", err);
290            let debug = format!("{:?}", err);
291            for pattern in &token_patterns {
292                assert!(
293                    !msg.contains(pattern),
294                    "Error message contains token pattern '{}': {}",
295                    pattern,
296                    msg
297                );
298                assert!(
299                    !display.contains(pattern),
300                    "Error Display contains token pattern '{}': {}",
301                    pattern,
302                    display
303                );
304                assert!(
305                    !debug.contains(pattern),
306                    "Error Debug contains token pattern '{}': {}",
307                    pattern,
308                    debug
309                );
310            }
311        }
312    }
313}