1use std::fmt;
2
3#[derive(Debug)]
4pub enum OrgModeError {
5 InvalidDirectory(String),
6 InvalidHeadingPath(String),
7 InvalidElementId(String),
8 InvalidAgendaViewType(String),
9 WalkError(ignore::Error),
10 GlobError(globset::Error),
11 IoError(std::io::Error),
12 ShellExpansionError(String),
13 ConfigError(String),
14 InvalidTodoKeyword(String),
15 InvalidPriority(String),
16 InvalidTitle(String),
17 InvalidLevel(usize),
18 InvalidTag(String),
19 InvalidTimestamp { field: &'static str, value: String },
20 InvalidPropertyKey(String),
21 InvalidPropertyValue { key: String, reason: String },
22 DuplicatePropertyKey(String),
23 InvalidDatetreeDate(String),
24 DatetreeDateWithoutFlag,
25 HeadingNotFound(String),
26 AmbiguousTarget(String),
27 InvalidUpdate(String),
28}
29
30impl fmt::Display for OrgModeError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 OrgModeError::InvalidDirectory(path) => {
34 write!(f, "Invalid or inaccessible directory: {path}")
35 }
36 OrgModeError::InvalidHeadingPath(path) => {
37 write!(f, "Invalid heading path: {path}")
38 }
39 OrgModeError::InvalidElementId(id) => {
40 write!(f, "Invalid element id: {id}")
41 }
42 OrgModeError::InvalidAgendaViewType(input) => {
43 write!(f, "Invalid agenda view type: {input}")
44 }
45 OrgModeError::WalkError(err) => write!(f, "Error walking directory: {err}"),
46 OrgModeError::GlobError(err) => write!(f, "Error with glob pattern: {err}"),
47 OrgModeError::IoError(err) => write!(f, "IO error: {err}"),
48 OrgModeError::ShellExpansionError(path) => write!(f, "Failed to expand path: {path}"),
49 OrgModeError::ConfigError(msg) => write!(f, "Configuration error: {msg}"),
50 OrgModeError::InvalidTodoKeyword(kw) => {
51 write!(f, "Invalid TODO keyword: {kw}")
52 }
53 OrgModeError::InvalidPriority(p) => write!(f, "Invalid priority: {p}"),
54 OrgModeError::InvalidTitle(reason) => write!(f, "Invalid heading title: {reason}"),
55 OrgModeError::InvalidLevel(level) => {
56 write!(f, "Invalid heading level: {level} (must be 1..=19)")
57 }
58 OrgModeError::InvalidTag(tag) => {
59 write!(f, "Invalid tag '{tag}': tags must match [A-Za-z0-9_@]+")
60 }
61 OrgModeError::InvalidTimestamp { field, value } => write!(
62 f,
63 "Invalid timestamp for {field}: '{value}', expected YYYY-MM-DD [HH:MM] [repeater +N|++N|.+N{{h|d|w|m|y}}] [warning -N{{h|d|w|m|y}}]"
64 ),
65 OrgModeError::InvalidPropertyKey(key) => write!(
66 f,
67 "Invalid property key '{key}': must be non-empty and contain only [A-Za-z0-9_-]"
68 ),
69 OrgModeError::InvalidPropertyValue { key, reason } => {
70 write!(f, "Invalid property value for key '{key}': {reason}")
71 }
72 OrgModeError::DuplicatePropertyKey(key) => {
73 write!(f, "Duplicate property key: '{key}'")
74 }
75 OrgModeError::InvalidDatetreeDate(value) => {
76 write!(f, "Invalid datetree date '{value}': expected YYYY-MM-DD")
77 }
78 OrgModeError::DatetreeDateWithoutFlag => {
79 write!(f, "Datetree date specified without enabling datetree")
80 }
81 OrgModeError::HeadingNotFound(target) => {
82 write!(f, "Heading not found: {target}")
83 }
84 OrgModeError::AmbiguousTarget(target) => {
85 write!(
86 f,
87 "Ambiguous target (matches more than one heading): {target}"
88 )
89 }
90 OrgModeError::InvalidUpdate(reason) => {
91 write!(f, "Invalid update: {reason}")
92 }
93 }
94 }
95}
96
97impl std::error::Error for OrgModeError {
98 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
99 match self {
100 OrgModeError::WalkError(err) => Some(err),
101 OrgModeError::IoError(err) => Some(err),
102 _ => None,
103 }
104 }
105}
106
107impl From<ignore::Error> for OrgModeError {
108 fn from(err: ignore::Error) -> Self {
109 OrgModeError::WalkError(err)
110 }
111}
112
113impl From<std::io::Error> for OrgModeError {
114 fn from(err: std::io::Error) -> Self {
115 OrgModeError::IoError(err)
116 }
117}
118
119impl From<config::ConfigError> for OrgModeError {
120 fn from(err: config::ConfigError) -> Self {
121 OrgModeError::ConfigError(err.to_string())
122 }
123}
124
125impl From<globset::Error> for OrgModeError {
126 fn from(err: globset::Error) -> Self {
127 OrgModeError::GlobError(err)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_display_invalid_priority() {
137 let s = format!("{}", OrgModeError::InvalidPriority("X".to_string()));
138 assert_eq!(s, "Invalid priority: X");
139 }
140
141 #[test]
142 fn test_display_invalid_title() {
143 let s = format!(
144 "{}",
145 OrgModeError::InvalidTitle("must not be empty".to_string())
146 );
147 assert_eq!(s, "Invalid heading title: must not be empty");
148 }
149
150 #[test]
151 fn test_display_invalid_level() {
152 let s = format!("{}", OrgModeError::InvalidLevel(42));
153 assert_eq!(s, "Invalid heading level: 42 (must be 1..=19)");
154 }
155
156 #[test]
157 fn test_display_invalid_tag() {
158 let s = format!("{}", OrgModeError::InvalidTag("bad tag".to_string()));
159 assert_eq!(s, "Invalid tag 'bad tag': tags must match [A-Za-z0-9_@]+");
160 }
161
162 #[test]
163 fn test_display_invalid_timestamp() {
164 let s = format!(
165 "{}",
166 OrgModeError::InvalidTimestamp {
167 field: "scheduled",
168 value: "tomorrow".to_string(),
169 }
170 );
171 assert_eq!(
172 s,
173 "Invalid timestamp for scheduled: 'tomorrow', expected YYYY-MM-DD [HH:MM] [repeater +N|++N|.+N{h|d|w|m|y}] [warning -N{h|d|w|m|y}]"
174 );
175 }
176
177 #[test]
178 fn test_display_invalid_property_key() {
179 let s = format!(
180 "{}",
181 OrgModeError::InvalidPropertyKey("BAD KEY".to_string())
182 );
183 assert_eq!(
184 s,
185 "Invalid property key 'BAD KEY': must be non-empty and contain only [A-Za-z0-9_-]"
186 );
187 }
188
189 #[test]
190 fn test_display_invalid_property_value() {
191 let s = format!(
192 "{}",
193 OrgModeError::InvalidPropertyValue {
194 key: "CATEGORY".to_string(),
195 reason: "contains newline".to_string(),
196 }
197 );
198 assert_eq!(
199 s,
200 "Invalid property value for key 'CATEGORY': contains newline"
201 );
202 }
203
204 #[test]
205 fn test_display_duplicate_property_key() {
206 let s = format!(
207 "{}",
208 OrgModeError::DuplicatePropertyKey("CATEGORY".to_string())
209 );
210 assert_eq!(s, "Duplicate property key: 'CATEGORY'");
211 }
212
213 #[test]
214 fn test_display_invalid_datetree_date() {
215 let s = format!(
216 "{}",
217 OrgModeError::InvalidDatetreeDate("tomorrow".to_string())
218 );
219 assert_eq!(s, "Invalid datetree date 'tomorrow': expected YYYY-MM-DD");
220 }
221
222 #[test]
223 fn test_display_datetree_date_without_flag() {
224 let s = format!("{}", OrgModeError::DatetreeDateWithoutFlag);
225 assert_eq!(s, "Datetree date specified without enabling datetree");
226 }
227
228 #[test]
229 fn test_display_heading_not_found() {
230 let s = format!("{}", OrgModeError::HeadingNotFound("task-123".to_string()));
231 assert_eq!(s, "Heading not found: task-123");
232 }
233
234 #[test]
235 fn test_display_ambiguous_target() {
236 let s = format!(
237 "{}",
238 OrgModeError::AmbiguousTarget("Projects/Work".to_string())
239 );
240 assert_eq!(
241 s,
242 "Ambiguous target (matches more than one heading): Projects/Work"
243 );
244 }
245
246 #[test]
247 fn test_display_invalid_update() {
248 let s = format!(
249 "{}",
250 OrgModeError::InvalidUpdate("nothing to update".to_string())
251 );
252 assert_eq!(s, "Invalid update: nothing to update");
253 }
254}