1use std::path::PathBuf;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
8pub enum Error {
9 #[error("failed to read file: {path}")]
11 FileRead {
12 path: PathBuf,
13 #[source]
14 source: std::io::Error,
15 },
16
17 #[error("failed to parse config file: {path}")]
19 ConfigParse {
20 path: PathBuf,
21 #[source]
22 source: toml::de::Error,
23 },
24
25 #[error("invalid language definition '{name}': {reason}")]
27 InvalidLanguage { name: String, reason: String },
28
29 #[error("invalid regex pattern: {pattern}")]
31 InvalidRegex {
32 pattern: String,
33 #[source]
34 source: regex::Error,
35 },
36
37 #[error("invalid glob pattern: {pattern}")]
39 InvalidGlob {
40 pattern: String,
41 #[source]
42 source: globset::Error,
43 },
44
45 #[error("directory not found: {path}")]
47 DirectoryNotFound { path: PathBuf },
48
49 #[error("failed to write output")]
51 OutputWrite(#[from] std::io::Error),
52
53 #[error("failed to render template")]
55 TemplateRender(#[from] askama::Error),
56
57 #[error("failed to serialize JSON")]
59 JsonSerialize(#[from] serde_json::Error),
60
61 #[error("directory traversal error: {0}")]
63 Walk(#[from] ignore::Error),
64
65 #[error("not a git repository: {path}")]
67 NotGitRepo { path: PathBuf },
68
69 #[error("git command failed: {message}")]
71 GitError { message: String },
72
73 #[error("no snapshots found in {path}")]
75 NoSnapshots { path: PathBuf },
76
77 #[error("snapshot not found: {id}")]
79 SnapshotNotFound { id: String },
80
81 #[error("failed to parse language definitions")]
83 LanguageParse(#[from] toml::de::Error),
84}
85
86pub type Result<T> = std::result::Result<T, Error>;
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::io;
93
94 #[test]
95 fn test_error_display_directory_not_found() {
96 let err = Error::DirectoryNotFound {
97 path: PathBuf::from("/nonexistent"),
98 };
99 assert_eq!(err.to_string(), "directory not found: /nonexistent");
100 }
101
102 #[test]
103 fn test_error_display_invalid_language() {
104 let err = Error::InvalidLanguage {
105 name: "Test".to_string(),
106 reason: "missing extensions".to_string(),
107 };
108 assert_eq!(
109 err.to_string(),
110 "invalid language definition 'Test': missing extensions"
111 );
112 }
113
114 #[test]
115 #[allow(clippy::invalid_regex)]
116 fn test_error_display_invalid_regex() {
117 let regex_err = regex::Regex::new("[invalid").unwrap_err();
118 let err = Error::InvalidRegex {
119 pattern: "[invalid".to_string(),
120 source: regex_err,
121 };
122 assert!(err.to_string().contains("invalid regex pattern"));
123 }
124
125 #[test]
126 fn test_error_from_io_error() {
127 let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
128 let err: Error = Error::OutputWrite(io_err);
129 assert!(err.to_string().contains("failed to write output"));
130 }
131
132 #[test]
133 fn test_error_display_not_git_repo() {
134 let err = Error::NotGitRepo {
135 path: PathBuf::from("/some/path"),
136 };
137 assert_eq!(err.to_string(), "not a git repository: /some/path");
138 }
139
140 #[test]
141 fn test_error_display_git_error() {
142 let err = Error::GitError {
143 message: "command not found".to_string(),
144 };
145 assert_eq!(err.to_string(), "git command failed: command not found");
146 }
147
148 #[test]
149 fn test_error_display_no_snapshots() {
150 let err = Error::NoSnapshots {
151 path: PathBuf::from(".codelens/snapshots"),
152 };
153 assert_eq!(err.to_string(), "no snapshots found in .codelens/snapshots");
154 }
155
156 #[test]
157 fn test_error_display_snapshot_not_found() {
158 let err = Error::SnapshotNotFound {
159 id: "2026-04-01".to_string(),
160 };
161 assert_eq!(err.to_string(), "snapshot not found: 2026-04-01");
162 }
163
164 #[test]
165 fn test_result_type() {
166 fn returns_ok() -> Result<i32> {
167 Ok(42)
168 }
169 assert_eq!(returns_ok().unwrap(), 42);
170
171 let err: Result<i32> = Err(Error::DirectoryNotFound {
172 path: PathBuf::from("/test"),
173 });
174 assert!(err.is_err());
175 }
176}