markdown_org_extract/
error.rs1use std::fmt;
8use std::io;
9
10#[derive(Debug)]
12pub enum AppError {
13 Io {
19 context: String,
24 source: io::Error,
26 },
27 InvalidDirectory(String),
29 InvalidGlob(String),
31 InvalidDate(String),
33 InvalidTimezone(String),
35 InvalidOutput(String),
37 DateRange(String),
39 Serialization(String),
41 Regex(String),
43}
44
45impl fmt::Display for AppError {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
52 AppError::Io { context, source } => write!(f, "io: {context}: {source}"),
53 AppError::InvalidDirectory(msg) => write!(f, "{msg}"),
54 AppError::InvalidGlob(msg) => write!(f, "{msg}"),
55 AppError::InvalidDate(msg) => write!(f, "{msg}"),
56 AppError::InvalidTimezone(tz) => write!(f, "invalid timezone: {tz}"),
57 AppError::InvalidOutput(msg) => write!(f, "{msg}"),
58 AppError::DateRange(msg) => write!(f, "{msg}"),
59 AppError::Serialization(msg) => write!(f, "serialization: {msg}"),
60 AppError::Regex(msg) => write!(f, "regex: {msg}"),
61 }
62 }
63}
64
65impl AppError {
66 pub fn exit_code(&self) -> i32 {
75 match self {
76 AppError::InvalidDirectory(_)
77 | AppError::InvalidGlob(_)
78 | AppError::InvalidDate(_)
79 | AppError::InvalidTimezone(_)
80 | AppError::InvalidOutput(_)
81 | AppError::DateRange(_) => 2,
82 AppError::Io { .. } => 74,
83 AppError::Regex(_) | AppError::Serialization(_) => 70,
84 }
85 }
86
87 pub fn io(context: impl Into<String>, source: io::Error) -> Self {
96 AppError::Io {
97 context: context.into(),
98 source,
99 }
100 }
101}
102
103impl std::error::Error for AppError {
104 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
105 match self {
111 AppError::Io { source, .. } => Some(source),
112 _ => None,
113 }
114 }
115}
116
117impl From<serde_json::Error> for AppError {
118 fn from(err: serde_json::Error) -> Self {
119 AppError::Serialization(err.to_string())
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use std::io::{self, ErrorKind};
127
128 #[test]
129 fn display_invalid_directory() {
130 let e = AppError::InvalidDirectory("directory does not exist: /no/such".into());
131 assert_eq!(e.to_string(), "directory does not exist: /no/such");
132 }
133
134 #[test]
135 fn display_invalid_glob() {
136 let e = AppError::InvalidGlob("invalid pattern '[': ...".into());
137 assert_eq!(e.to_string(), "invalid pattern '[': ...");
138 }
139
140 #[test]
141 fn display_invalid_timezone() {
142 assert_eq!(
143 AppError::InvalidTimezone("X".into()).to_string(),
144 "invalid timezone: X"
145 );
146 }
147
148 #[test]
149 fn display_invalid_output() {
150 assert_eq!(
151 AppError::InvalidOutput("refusing to overwrite symlink: /tmp/foo".into()).to_string(),
152 "refusing to overwrite symlink: /tmp/foo"
153 );
154 }
155
156 #[test]
157 fn display_date_range() {
158 assert_eq!(
159 AppError::DateRange("from > to".into()).to_string(),
160 "from > to"
161 );
162 }
163
164 #[test]
165 fn io_constructor_preserves_context_and_source() {
166 use std::error::Error as _;
171 let io_err = io::Error::new(ErrorKind::NotFound, "missing");
172 let e = AppError::io("/tmp/out.json", io_err);
173 assert!(matches!(e, AppError::Io { .. }));
174 let msg = e.to_string();
175 assert!(msg.starts_with("io: "), "got: {msg}");
176 assert!(msg.contains("/tmp/out.json"), "got: {msg}");
177 assert!(msg.contains("missing"), "got: {msg}");
178 let src = e.source().expect("source should be set for Io");
179 assert!(src.to_string().contains("missing"));
180 }
181
182 #[test]
183 fn source_returns_none_for_non_io_variants() {
184 use std::error::Error as _;
189 assert!(AppError::InvalidDirectory("x".into()).source().is_none());
190 assert!(AppError::Regex("x".into()).source().is_none());
191 assert!(AppError::Serialization("x".into()).source().is_none());
192 }
193
194 #[test]
195 fn from_serde_json_error_wraps() {
196 let parsed: Result<serde_json::Value, _> = serde_json::from_str("not json");
197 let e: AppError = parsed.unwrap_err().into();
198 assert!(matches!(e, AppError::Serialization(_)));
199 assert!(e.to_string().starts_with("serialization: "));
200 }
201
202 #[test]
203 fn errors_are_send_sync() {
204 fn is_send_sync<T: Send + Sync>() {}
207 is_send_sync::<AppError>();
208 }
209
210 #[test]
211 fn exit_code_usage_errors_return_2() {
212 assert_eq!(
213 AppError::InvalidDirectory("x".into()).exit_code(),
214 2,
215 "InvalidDirectory is a usage error and must map to exit 2"
216 );
217 assert_eq!(AppError::InvalidGlob("x".into()).exit_code(), 2);
218 assert_eq!(AppError::InvalidDate("x".into()).exit_code(), 2);
219 assert_eq!(AppError::InvalidTimezone("x".into()).exit_code(), 2);
220 assert_eq!(AppError::InvalidOutput("x".into()).exit_code(), 2);
221 assert_eq!(AppError::DateRange("x".into()).exit_code(), 2);
222 }
223
224 #[test]
225 fn exit_code_io_returns_74() {
226 let io = io::Error::new(ErrorKind::NotFound, "missing");
227 assert_eq!(
228 AppError::io("/tmp/x", io).exit_code(),
229 74,
230 "Io maps to EX_IOERR (74) from sysexits.h"
231 );
232 }
233
234 #[test]
235 fn exit_code_software_errors_return_70() {
236 assert_eq!(
237 AppError::Regex("x".into()).exit_code(),
238 70,
239 "Regex compile failure is an internal software error (EX_SOFTWARE = 70)"
240 );
241 assert_eq!(AppError::Serialization("x".into()).exit_code(), 70);
242 }
243}