1use std::path::PathBuf;
7use thiserror::Error;
8
9#[derive(Error, Debug)]
11pub enum Error {
12 #[error("failed to read ADR file at {path}")]
14 FileRead {
15 path: PathBuf,
17 #[source]
19 source: std::io::Error,
20 },
21
22 #[error("failed to write output to {path}")]
24 FileWrite {
25 path: PathBuf,
27 #[source]
29 source: std::io::Error,
30 },
31
32 #[error("invalid frontmatter in {path}: {message}")]
34 InvalidFrontmatter {
35 path: PathBuf,
37 message: String,
39 },
40
41 #[error("YAML parsing failed in {path}")]
43 YamlParse {
44 path: PathBuf,
46 #[source]
48 source: serde_yaml::Error,
49 },
50
51 #[error("missing required field '{field}' in {path}")]
53 MissingField {
54 path: PathBuf,
56 field: &'static str,
58 },
59
60 #[error("template rendering failed")]
62 TemplateRender {
63 #[source]
65 source: askama::Error,
66 },
67
68 #[error("no ADR files found in {path}")]
70 NoAdrsFound {
71 path: PathBuf,
73 },
74
75 #[error("validation failed: {0} error(s) found")]
77 ValidationFailed(usize),
78
79 #[error("invalid ADR filename: {0}")]
81 InvalidFilename(String),
82
83 #[error("invalid glob pattern: {0}")]
85 GlobPattern(String),
86
87 #[error("invalid date format in {path}: {message}")]
89 DateParse {
90 path: PathBuf,
92 message: String,
94 },
95
96 #[error("JSON serialization failed: {0}")]
98 JsonSerialize(String),
99}
100
101impl From<askama::Error> for Error {
102 fn from(source: askama::Error) -> Self {
103 Self::TemplateRender { source }
104 }
105}
106
107pub type Result<T> = std::result::Result<T, Error>;
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_error_display_file_read() {
116 let err = Error::FileRead {
117 path: PathBuf::from("/test/path.md"),
118 source: std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
119 };
120 let display = err.to_string();
121 assert!(display.contains("failed to read ADR file"));
122 assert!(display.contains("/test/path.md"));
123 }
124
125 #[test]
126 fn test_error_display_missing_field() {
127 let err = Error::MissingField {
128 path: PathBuf::from("adr_0001.md"),
129 field: "title",
130 };
131 let display = err.to_string();
132 assert!(display.contains("missing required field"));
133 assert!(display.contains("title"));
134 assert!(display.contains("adr_0001.md"));
135 }
136
137 #[test]
138 fn test_error_display_validation_failed() {
139 let err = Error::ValidationFailed(5);
140 assert_eq!(err.to_string(), "validation failed: 5 error(s) found");
141 }
142
143 #[test]
144 fn test_error_display_no_adrs_found() {
145 let err = Error::NoAdrsFound {
146 path: PathBuf::from("docs/decisions"),
147 };
148 let display = err.to_string();
149 assert!(display.contains("no ADR files found"));
150 assert!(display.contains("docs/decisions"));
151 }
152
153 #[test]
154 fn test_error_display_file_write() {
155 let err = Error::FileWrite {
156 path: PathBuf::from("/output/file.html"),
157 source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied"),
158 };
159 let display = err.to_string();
160 assert!(display.contains("failed to write output"));
161 assert!(display.contains("/output/file.html"));
162 }
163
164 #[test]
165 fn test_error_display_invalid_frontmatter() {
166 let err = Error::InvalidFrontmatter {
167 path: PathBuf::from("test.md"),
168 message: "missing closing delimiter".to_string(),
169 };
170 let display = err.to_string();
171 assert!(display.contains("invalid frontmatter"));
172 assert!(display.contains("test.md"));
173 assert!(display.contains("missing closing delimiter"));
174 }
175
176 #[test]
177 fn test_error_display_invalid_filename() {
178 let err = Error::InvalidFilename("bad_name".to_string());
179 let display = err.to_string();
180 assert!(display.contains("invalid ADR filename"));
181 assert!(display.contains("bad_name"));
182 }
183
184 #[test]
185 fn test_error_display_glob_pattern() {
186 let err = Error::GlobPattern("invalid pattern".to_string());
187 let display = err.to_string();
188 assert!(display.contains("invalid glob pattern"));
189 }
190
191 #[test]
192 fn test_error_display_date_parse() {
193 let err = Error::DateParse {
194 path: PathBuf::from("adr.md"),
195 message: "invalid date format".to_string(),
196 };
197 let display = err.to_string();
198 assert!(display.contains("invalid date format"));
199 assert!(display.contains("adr.md"));
200 }
201
202 #[test]
203 fn test_error_display_json_serialize() {
204 let err = Error::JsonSerialize("serialization failed".to_string());
205 let display = err.to_string();
206 assert!(display.contains("JSON serialization failed"));
207 }
208
209 #[test]
210 fn test_error_from_askama() {
211 let askama_err = askama::Error::Custom(Box::new(std::io::Error::other("template error")));
213 let err: Error = askama_err.into();
214 let display = err.to_string();
215 assert!(display.contains("template rendering failed"));
216 }
217}