use std::fmt;
#[derive(Debug)]
pub enum OrgModeError {
InvalidDirectory(String),
InvalidHeadingPath(String),
InvalidElementId(String),
InvalidAgendaViewType(String),
WalkError(ignore::Error),
GlobError(globset::Error),
IoError(std::io::Error),
ShellExpansionError(String),
ConfigError(String),
InvalidTodoKeyword(String),
InvalidPriority(String),
InvalidTitle(String),
InvalidLevel(usize),
InvalidTag(String),
InvalidTimestamp { field: &'static str, value: String },
InvalidPropertyKey(String),
InvalidPropertyValue { key: String, reason: String },
DuplicatePropertyKey(String),
InvalidDatetreeDate(String),
DatetreeDateWithoutFlag,
HeadingNotFound(String),
AmbiguousTarget(String),
InvalidUpdate(String),
}
impl fmt::Display for OrgModeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OrgModeError::InvalidDirectory(path) => {
write!(f, "Invalid or inaccessible directory: {path}")
}
OrgModeError::InvalidHeadingPath(path) => {
write!(f, "Invalid heading path: {path}")
}
OrgModeError::InvalidElementId(id) => {
write!(f, "Invalid element id: {id}")
}
OrgModeError::InvalidAgendaViewType(input) => {
write!(f, "Invalid agenda view type: {input}")
}
OrgModeError::WalkError(err) => write!(f, "Error walking directory: {err}"),
OrgModeError::GlobError(err) => write!(f, "Error with glob pattern: {err}"),
OrgModeError::IoError(err) => write!(f, "IO error: {err}"),
OrgModeError::ShellExpansionError(path) => write!(f, "Failed to expand path: {path}"),
OrgModeError::ConfigError(msg) => write!(f, "Configuration error: {msg}"),
OrgModeError::InvalidTodoKeyword(kw) => {
write!(f, "Invalid TODO keyword: {kw}")
}
OrgModeError::InvalidPriority(p) => write!(f, "Invalid priority: {p}"),
OrgModeError::InvalidTitle(reason) => write!(f, "Invalid heading title: {reason}"),
OrgModeError::InvalidLevel(level) => {
write!(f, "Invalid heading level: {level} (must be 1..=19)")
}
OrgModeError::InvalidTag(tag) => {
write!(f, "Invalid tag '{tag}': tags must match [A-Za-z0-9_@]+")
}
OrgModeError::InvalidTimestamp { field, value } => write!(
f,
"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}}]"
),
OrgModeError::InvalidPropertyKey(key) => write!(
f,
"Invalid property key '{key}': must be non-empty and contain only [A-Za-z0-9_-]"
),
OrgModeError::InvalidPropertyValue { key, reason } => {
write!(f, "Invalid property value for key '{key}': {reason}")
}
OrgModeError::DuplicatePropertyKey(key) => {
write!(f, "Duplicate property key: '{key}'")
}
OrgModeError::InvalidDatetreeDate(value) => {
write!(f, "Invalid datetree date '{value}': expected YYYY-MM-DD")
}
OrgModeError::DatetreeDateWithoutFlag => {
write!(f, "Datetree date specified without enabling datetree")
}
OrgModeError::HeadingNotFound(target) => {
write!(f, "Heading not found: {target}")
}
OrgModeError::AmbiguousTarget(target) => {
write!(
f,
"Ambiguous target (matches more than one heading): {target}"
)
}
OrgModeError::InvalidUpdate(reason) => {
write!(f, "Invalid update: {reason}")
}
}
}
}
impl std::error::Error for OrgModeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
OrgModeError::WalkError(err) => Some(err),
OrgModeError::IoError(err) => Some(err),
_ => None,
}
}
}
impl From<ignore::Error> for OrgModeError {
fn from(err: ignore::Error) -> Self {
OrgModeError::WalkError(err)
}
}
impl From<std::io::Error> for OrgModeError {
fn from(err: std::io::Error) -> Self {
OrgModeError::IoError(err)
}
}
impl From<config::ConfigError> for OrgModeError {
fn from(err: config::ConfigError) -> Self {
OrgModeError::ConfigError(err.to_string())
}
}
impl From<globset::Error> for OrgModeError {
fn from(err: globset::Error) -> Self {
OrgModeError::GlobError(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_invalid_priority() {
let s = format!("{}", OrgModeError::InvalidPriority("X".to_string()));
assert_eq!(s, "Invalid priority: X");
}
#[test]
fn test_display_invalid_title() {
let s = format!(
"{}",
OrgModeError::InvalidTitle("must not be empty".to_string())
);
assert_eq!(s, "Invalid heading title: must not be empty");
}
#[test]
fn test_display_invalid_level() {
let s = format!("{}", OrgModeError::InvalidLevel(42));
assert_eq!(s, "Invalid heading level: 42 (must be 1..=19)");
}
#[test]
fn test_display_invalid_tag() {
let s = format!("{}", OrgModeError::InvalidTag("bad tag".to_string()));
assert_eq!(s, "Invalid tag 'bad tag': tags must match [A-Za-z0-9_@]+");
}
#[test]
fn test_display_invalid_timestamp() {
let s = format!(
"{}",
OrgModeError::InvalidTimestamp {
field: "scheduled",
value: "tomorrow".to_string(),
}
);
assert_eq!(
s,
"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}]"
);
}
#[test]
fn test_display_invalid_property_key() {
let s = format!(
"{}",
OrgModeError::InvalidPropertyKey("BAD KEY".to_string())
);
assert_eq!(
s,
"Invalid property key 'BAD KEY': must be non-empty and contain only [A-Za-z0-9_-]"
);
}
#[test]
fn test_display_invalid_property_value() {
let s = format!(
"{}",
OrgModeError::InvalidPropertyValue {
key: "CATEGORY".to_string(),
reason: "contains newline".to_string(),
}
);
assert_eq!(
s,
"Invalid property value for key 'CATEGORY': contains newline"
);
}
#[test]
fn test_display_duplicate_property_key() {
let s = format!(
"{}",
OrgModeError::DuplicatePropertyKey("CATEGORY".to_string())
);
assert_eq!(s, "Duplicate property key: 'CATEGORY'");
}
#[test]
fn test_display_invalid_datetree_date() {
let s = format!(
"{}",
OrgModeError::InvalidDatetreeDate("tomorrow".to_string())
);
assert_eq!(s, "Invalid datetree date 'tomorrow': expected YYYY-MM-DD");
}
#[test]
fn test_display_datetree_date_without_flag() {
let s = format!("{}", OrgModeError::DatetreeDateWithoutFlag);
assert_eq!(s, "Datetree date specified without enabling datetree");
}
#[test]
fn test_display_heading_not_found() {
let s = format!("{}", OrgModeError::HeadingNotFound("task-123".to_string()));
assert_eq!(s, "Heading not found: task-123");
}
#[test]
fn test_display_ambiguous_target() {
let s = format!(
"{}",
OrgModeError::AmbiguousTarget("Projects/Work".to_string())
);
assert_eq!(
s,
"Ambiguous target (matches more than one heading): Projects/Work"
);
}
#[test]
fn test_display_invalid_update() {
let s = format!(
"{}",
OrgModeError::InvalidUpdate("nothing to update".to_string())
);
assert_eq!(s, "Invalid update: nothing to update");
}
}