Skip to main content

org_core/
error.rs

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}