aven-core 0.1.35

Core library for the Aven local-first task manager
Documentation
use std::fmt;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidTaskStatus(String);

impl fmt::Display for InvalidTaskStatus {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "error invalid-status input={} choices={}",
            self.0,
            STATUSES.join(",")
        )
    }
}

impl std::error::Error for InvalidTaskStatus {}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidTaskPriority(String);

impl fmt::Display for InvalidTaskPriority {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "error invalid-priority input={} choices={}",
            self.0,
            PRIORITIES.join(",")
        )
    }
}

impl std::error::Error for InvalidTaskPriority {}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidTaskSource(String);

impl fmt::Display for InvalidTaskSource {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "error invalid-task-source input={} choices={}",
            self.0,
            TASK_SOURCES.join(",")
        )
    }
}

impl std::error::Error for InvalidTaskSource {}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TaskSource {
    Cli,
    Tui,
    Api,
    Ios,
    Android,
    #[default]
    Unknown,
}

impl TaskSource {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Cli => "cli",
            Self::Tui => "tui",
            Self::Api => "api",
            Self::Ios => "ios",
            Self::Android => "android",
            Self::Unknown => "unknown",
        }
    }

    pub fn parse(value: &str) -> Result<Self, InvalidTaskSource> {
        match value {
            "cli" => Ok(Self::Cli),
            "tui" => Ok(Self::Tui),
            "api" => Ok(Self::Api),
            "ios" => Ok(Self::Ios),
            "android" => Ok(Self::Android),
            "unknown" => Ok(Self::Unknown),
            _ => Err(InvalidTaskSource(value.to_string())),
        }
    }
}

impl fmt::Display for TaskSource {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl TryFrom<&str> for TaskSource {
    type Error = InvalidTaskSource;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::parse(value)
    }
}

pub const TASK_SOURCES: &[&str] = &[
    TaskSource::Cli.as_str(),
    TaskSource::Tui.as_str(),
    TaskSource::Api.as_str(),
    TaskSource::Ios.as_str(),
    TaskSource::Android.as_str(),
    TaskSource::Unknown.as_str(),
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TaskStatus {
    Inbox,
    Backlog,
    Todo,
    Active,
    Done,
    Canceled,
}

impl TaskStatus {
    #[allow(dead_code)]
    pub const OPEN: [Self; 4] = [Self::Inbox, Self::Backlog, Self::Todo, Self::Active];
    #[allow(dead_code)]
    pub const TERMINAL: [Self; 2] = [Self::Done, Self::Canceled];

    pub const fn is_open(self) -> bool {
        matches!(
            self,
            Self::Inbox | Self::Backlog | Self::Todo | Self::Active
        )
    }

    #[allow(dead_code)]
    pub const fn is_terminal(self) -> bool {
        matches!(self, Self::Done | Self::Canceled)
    }

    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Inbox => "inbox",
            Self::Backlog => "backlog",
            Self::Todo => "todo",
            Self::Active => "active",
            Self::Done => "done",
            Self::Canceled => "canceled",
        }
    }

    pub fn parse(value: &str) -> Result<Self, InvalidTaskStatus> {
        match value {
            "inbox" => Ok(Self::Inbox),
            "backlog" => Ok(Self::Backlog),
            "todo" => Ok(Self::Todo),
            "active" => Ok(Self::Active),
            "done" => Ok(Self::Done),
            "canceled" => Ok(Self::Canceled),
            _ => Err(InvalidTaskStatus(value.to_string())),
        }
    }
}

impl std::fmt::Display for TaskStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl TryFrom<&str> for TaskStatus {
    type Error = InvalidTaskStatus;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::parse(value)
    }
}

pub const STATUSES: &[&str] = &[
    TaskStatus::Inbox.as_str(),
    TaskStatus::Backlog.as_str(),
    TaskStatus::Todo.as_str(),
    TaskStatus::Active.as_str(),
    TaskStatus::Done.as_str(),
    TaskStatus::Canceled.as_str(),
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TaskPriority {
    None,
    Low,
    Medium,
    High,
    Urgent,
}

impl TaskPriority {
    pub const ALL: [Self; 5] = [
        Self::None,
        Self::Low,
        Self::Medium,
        Self::High,
        Self::Urgent,
    ];

    pub const fn as_str(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
            Self::Urgent => "urgent",
        }
    }

    pub const fn promotes_inbox_to_todo(self) -> bool {
        matches!(self, Self::Medium | Self::High | Self::Urgent)
    }

    pub fn parse(value: &str) -> Result<Self, InvalidTaskPriority> {
        match value {
            "none" => Ok(Self::None),
            "low" => Ok(Self::Low),
            "medium" => Ok(Self::Medium),
            "high" => Ok(Self::High),
            "urgent" => Ok(Self::Urgent),
            _ => Err(InvalidTaskPriority(value.to_string())),
        }
    }
}

impl std::fmt::Display for TaskPriority {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl TryFrom<&str> for TaskPriority {
    type Error = InvalidTaskPriority;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::parse(value)
    }
}

pub const PRIORITIES: &[&str] = &[
    TaskPriority::None.as_str(),
    TaskPriority::Low.as_str(),
    TaskPriority::Medium.as_str(),
    TaskPriority::High.as_str(),
    TaskPriority::Urgent.as_str(),
];

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn choices_round_trip_and_reject_invalid_values() {
        for value in TASK_SOURCES {
            assert_eq!(TaskSource::parse(value).unwrap().to_string(), *value);
        }
        for value in STATUSES {
            assert_eq!(TaskStatus::parse(value).unwrap().to_string(), *value);
        }
        for value in PRIORITIES {
            assert_eq!(TaskPriority::parse(value).unwrap().to_string(), *value);
        }

        assert_eq!(
            TaskSource::parse("agent").unwrap_err().to_string(),
            "error invalid-task-source input=agent choices=cli,tui,api,ios,android,unknown"
        );
        assert_eq!(
            TaskStatus::parse("blocked").unwrap_err().to_string(),
            "error invalid-status input=blocked choices=inbox,backlog,todo,active,done,canceled"
        );
        assert_eq!(
            TaskPriority::parse("soon").unwrap_err().to_string(),
            "error invalid-priority input=soon choices=none,low,medium,high,urgent"
        );
    }
}