adrscope/
error.rs

1//! Unified error types for ADRScope operations.
2//!
3//! This module provides a single error enum that covers all failure modes
4//! across the application, with rich context for debugging.
5
6use std::path::PathBuf;
7use thiserror::Error;
8
9/// Error type for all ADRScope operations.
10#[derive(Error, Debug)]
11pub enum Error {
12    /// Failed to read an ADR file from the filesystem.
13    #[error("failed to read ADR file at {path}")]
14    FileRead {
15        /// Path to the file that could not be read.
16        path: PathBuf,
17        /// The underlying I/O error.
18        #[source]
19        source: std::io::Error,
20    },
21
22    /// Failed to write output to the filesystem.
23    #[error("failed to write output to {path}")]
24    FileWrite {
25        /// Path where the write failed.
26        path: PathBuf,
27        /// The underlying I/O error.
28        #[source]
29        source: std::io::Error,
30    },
31
32    /// Invalid YAML frontmatter in an ADR file.
33    #[error("invalid frontmatter in {path}: {message}")]
34    InvalidFrontmatter {
35        /// Path to the file with invalid frontmatter.
36        path: PathBuf,
37        /// Description of what's wrong.
38        message: String,
39    },
40
41    /// YAML parsing failed.
42    #[error("YAML parsing failed in {path}")]
43    YamlParse {
44        /// Path to the file that failed to parse.
45        path: PathBuf,
46        /// The underlying YAML error.
47        #[source]
48        source: serde_yaml::Error,
49    },
50
51    /// Missing required frontmatter field.
52    #[error("missing required field '{field}' in {path}")]
53    MissingField {
54        /// Path to the file missing the field.
55        path: PathBuf,
56        /// Name of the missing field.
57        field: &'static str,
58    },
59
60    /// Template rendering failed.
61    #[error("template rendering failed")]
62    TemplateRender {
63        /// The underlying askama error.
64        #[source]
65        source: askama::Error,
66    },
67
68    /// No ADR files found in the specified directory.
69    #[error("no ADR files found in {path}")]
70    NoAdrsFound {
71        /// Directory that was searched.
72        path: PathBuf,
73    },
74
75    /// Validation failed with one or more errors.
76    #[error("validation failed: {0} error(s) found")]
77    ValidationFailed(usize),
78
79    /// Invalid ADR filename format.
80    #[error("invalid ADR filename: {0}")]
81    InvalidFilename(String),
82
83    /// Glob pattern error.
84    #[error("invalid glob pattern: {0}")]
85    GlobPattern(String),
86
87    /// Date parsing error.
88    #[error("invalid date format in {path}: {message}")]
89    DateParse {
90        /// Path to the file with the invalid date.
91        path: PathBuf,
92        /// Description of the date format issue.
93        message: String,
94    },
95
96    /// JSON serialization error.
97    #[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
107/// Result type alias for ADRScope operations.
108pub 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        // Create an askama error and convert it
212        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}