From ff23c9148b434619ffd5b0a88e0a3b9544d9aa47 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sat, 5 Jun 2021 20:42:39 -0400 Subject: [PATCH] refactor taskchampion::task into submodules --- taskchampion/src/task/annotation.rs | 10 ++ taskchampion/src/task/mod.rs | 18 +++ taskchampion/src/task/priority.rs | 48 ++++++ taskchampion/src/task/status.rs | 54 +++++++ taskchampion/src/task/tag.rs | 136 ++++++++++++++++ taskchampion/src/{ => task}/task.rs | 236 +--------------------------- 6 files changed, 268 insertions(+), 234 deletions(-) create mode 100644 taskchampion/src/task/annotation.rs create mode 100644 taskchampion/src/task/mod.rs create mode 100644 taskchampion/src/task/priority.rs create mode 100644 taskchampion/src/task/status.rs create mode 100644 taskchampion/src/task/tag.rs rename taskchampion/src/{ => task}/task.rs (72%) diff --git a/taskchampion/src/task/annotation.rs b/taskchampion/src/task/annotation.rs new file mode 100644 index 000000000..dadb72b0f --- /dev/null +++ b/taskchampion/src/task/annotation.rs @@ -0,0 +1,10 @@ +use super::Timestamp; + +/// An annotation for a task +#[derive(Debug, PartialEq)] +pub struct Annotation { + /// Time the annotation was made + pub entry: Timestamp, + /// Content of the annotation + pub description: String, +} diff --git a/taskchampion/src/task/mod.rs b/taskchampion/src/task/mod.rs new file mode 100644 index 000000000..6548bf404 --- /dev/null +++ b/taskchampion/src/task/mod.rs @@ -0,0 +1,18 @@ +#![allow(clippy::module_inception)] +use chrono::prelude::*; + +mod annotation; +mod priority; +mod status; +mod tag; +mod task; + +pub use annotation::Annotation; +pub use priority::Priority; +pub use status::Status; +pub use tag::{Tag, INVALID_TAG_CHARACTERS}; +pub use task::{Task, TaskMut}; + +use tag::SyntheticTag; + +pub type Timestamp = DateTime; diff --git a/taskchampion/src/task/priority.rs b/taskchampion/src/task/priority.rs new file mode 100644 index 000000000..5e3f29d0d --- /dev/null +++ b/taskchampion/src/task/priority.rs @@ -0,0 +1,48 @@ +/// The priority of a task +#[derive(Debug, PartialEq)] +pub enum Priority { + /// Low + L, + /// Medium + M, + /// High + H, +} + +#[allow(dead_code)] +impl Priority { + /// Get a Priority from the 1-character value in a TaskMap, + /// defaulting to M + pub(crate) fn from_taskmap(s: &str) -> Priority { + match s { + "L" => Priority::L, + "M" => Priority::M, + "H" => Priority::H, + _ => Priority::M, + } + } + + /// Get the 1-character value for this priority to use in the TaskMap. + pub(crate) fn to_taskmap(&self) -> &str { + match self { + Priority::L => "L", + Priority::M => "M", + Priority::H => "H", + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_priority() { + assert_eq!(Priority::L.to_taskmap(), "L"); + assert_eq!(Priority::M.to_taskmap(), "M"); + assert_eq!(Priority::H.to_taskmap(), "H"); + assert_eq!(Priority::from_taskmap("L"), Priority::L); + assert_eq!(Priority::from_taskmap("M"), Priority::M); + assert_eq!(Priority::from_taskmap("H"), Priority::H); + } +} diff --git a/taskchampion/src/task/status.rs b/taskchampion/src/task/status.rs new file mode 100644 index 000000000..f9f9fe773 --- /dev/null +++ b/taskchampion/src/task/status.rs @@ -0,0 +1,54 @@ +/// The status of a task. The default status in "Pending". +#[derive(Debug, PartialEq, Clone)] +pub enum Status { + Pending, + Completed, + Deleted, +} + +impl Status { + /// Get a Status from the 1-character value in a TaskMap, + /// defaulting to Pending + pub(crate) fn from_taskmap(s: &str) -> Status { + match s { + "P" => Status::Pending, + "C" => Status::Completed, + "D" => Status::Deleted, + _ => Status::Pending, + } + } + + /// Get the 1-character value for this status to use in the TaskMap. + pub(crate) fn to_taskmap(&self) -> &str { + match self { + Status::Pending => "P", + Status::Completed => "C", + Status::Deleted => "D", + } + } + + /// Get the full-name value for this status to use in the TaskMap. + pub fn to_string(&self) -> &str { + // TODO: should be impl Display + match self { + Status::Pending => "Pending", + Status::Completed => "Completed", + Status::Deleted => "Deleted", + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_status() { + assert_eq!(Status::Pending.to_taskmap(), "P"); + assert_eq!(Status::Completed.to_taskmap(), "C"); + assert_eq!(Status::Deleted.to_taskmap(), "D"); + assert_eq!(Status::from_taskmap("P"), Status::Pending); + assert_eq!(Status::from_taskmap("C"), Status::Completed); + assert_eq!(Status::from_taskmap("D"), Status::Deleted); + } +} diff --git a/taskchampion/src/task/tag.rs b/taskchampion/src/task/tag.rs new file mode 100644 index 000000000..2a60a66c9 --- /dev/null +++ b/taskchampion/src/task/tag.rs @@ -0,0 +1,136 @@ +use std::convert::TryFrom; +use std::fmt; +use std::str::FromStr; + +/// A Tag is a descriptor for a task, that is either present or absent, and can be used for +/// filtering. Tags composed of all uppercase letters are reserved for synthetic tags. +/// +/// Valid tags must not contain whitespace or any of the characters in [`INVALID_TAG_CHARACTERS`]. +/// The first characters additionally cannot be a digit, and subsequent characters cannot be `:`. +/// This definition is based on [that of +/// TaskWarrior](https://github.com/GothenburgBitFactory/taskwarrior/blob/663c6575ceca5bd0135ae884879339dac89d3142/src/Lexer.cpp#L146-L164). +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum Tag { + User(String), + Synthetic(SyntheticTag), +} + +pub const INVALID_TAG_CHARACTERS: &str = "+-*/(<>^! %=~"; + +impl Tag { + fn from_str(value: &str) -> Result { + fn err(value: &str) -> Result { + anyhow::bail!("invalid tag {:?}", value) + } + + // first, look for synthetic tags + if value.chars().all(|c| c.is_ascii_uppercase()) { + if let Ok(st) = SyntheticTag::from_str(value) { + return Ok(Self::Synthetic(st)); + } + // all uppercase, but not a valid synthetic tag + return err(value); + } + + if let Some(c) = value.chars().next() { + if c.is_whitespace() || c.is_ascii_digit() || INVALID_TAG_CHARACTERS.contains(c) { + return err(value); + } + } else { + return err(value); + } + if !value + .chars() + .skip(1) + .all(|c| !(c.is_whitespace() || c == ':' || INVALID_TAG_CHARACTERS.contains(c))) + { + return err(value); + } + Ok(Self::User(String::from(value))) + } +} + +impl TryFrom<&str> for Tag { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + Self::from_str(value) + } +} + +impl TryFrom<&String> for Tag { + type Error = anyhow::Error; + + fn try_from(value: &String) -> Result { + Self::from_str(&value[..]) + } +} + +impl fmt::Display for Tag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::User(s) => s.fmt(f), + Self::Synthetic(st) => st.as_ref().fmt(f), + } + } +} + +impl AsRef for Tag { + fn as_ref(&self) -> &str { + match self { + Self::User(s) => s.as_ref(), + Self::Synthetic(st) => st.as_ref(), + } + } +} + +#[derive( + Debug, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + strum_macros::EnumString, + strum_macros::AsRefStr, + strum_macros::EnumIter, +)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum SyntheticTag { + Waiting, + Active, +} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + use std::convert::TryInto; + + #[rstest] + #[case::simple("abc")] + #[case::colon_prefix(":abc")] + #[case::letters_and_numbers("a123_456")] + #[case::synthetic("WAITING")] + fn test_tag_try_into_success(#[case] s: &'static str) { + let tag: Tag = s.try_into().unwrap(); + // check Display (via to_string) and AsRef while we're here + assert_eq!(tag.to_string(), s.to_owned()); + assert_eq!(tag.as_ref(), s); + } + + #[rstest] + #[case::empty("")] + #[case::colon_infix("a:b")] + #[case::digits("999")] + #[case::bangs("abc!!!")] + #[case::no_such_synthetic("NOSUCH")] + fn test_tag_try_into_err(#[case] s: &'static str) { + let tag: Result = s.try_into(); + assert_eq!( + tag.unwrap_err().to_string(), + format!("invalid tag \"{}\"", s) + ); + } +} diff --git a/taskchampion/src/task.rs b/taskchampion/src/task/task.rs similarity index 72% rename from taskchampion/src/task.rs rename to taskchampion/src/task/task.rs index 8c7167cf7..c3678f730 100644 --- a/taskchampion/src/task.rs +++ b/taskchampion/src/task/task.rs @@ -1,197 +1,12 @@ +use super::{Status, SyntheticTag, Tag}; use crate::replica::Replica; use crate::storage::TaskMap; use chrono::prelude::*; use log::trace; use std::convert::AsRef; -use std::convert::{TryFrom, TryInto}; -use std::fmt; -use std::str::FromStr; +use std::convert::TryInto; use uuid::Uuid; -pub type Timestamp = DateTime; - -/// The priority of a task -#[derive(Debug, PartialEq)] -pub enum Priority { - /// Low - L, - /// Medium - M, - /// High - H, -} - -#[allow(dead_code)] -impl Priority { - /// Get a Priority from the 1-character value in a TaskMap, - /// defaulting to M - pub(crate) fn from_taskmap(s: &str) -> Priority { - match s { - "L" => Priority::L, - "M" => Priority::M, - "H" => Priority::H, - _ => Priority::M, - } - } - - /// Get the 1-character value for this priority to use in the TaskMap. - pub(crate) fn to_taskmap(&self) -> &str { - match self { - Priority::L => "L", - Priority::M => "M", - Priority::H => "H", - } - } -} - -/// The status of a task. The default status in "Pending". -#[derive(Debug, PartialEq, Clone)] -pub enum Status { - Pending, - Completed, - Deleted, -} - -impl Status { - /// Get a Status from the 1-character value in a TaskMap, - /// defaulting to Pending - pub(crate) fn from_taskmap(s: &str) -> Status { - match s { - "P" => Status::Pending, - "C" => Status::Completed, - "D" => Status::Deleted, - _ => Status::Pending, - } - } - - /// Get the 1-character value for this status to use in the TaskMap. - pub(crate) fn to_taskmap(&self) -> &str { - match self { - Status::Pending => "P", - Status::Completed => "C", - Status::Deleted => "D", - } - } - - /// Get the full-name value for this status to use in the TaskMap. - pub fn to_string(&self) -> &str { - // TODO: should be impl Display - match self { - Status::Pending => "Pending", - Status::Completed => "Completed", - Status::Deleted => "Deleted", - } - } -} - -// TODO: separate module, wrap in newtype to avoid pub details -/// A Tag is a descriptor for a task, that is either present or absent, and can be used for -/// filtering. Tags composed of all uppercase letters are reserved for synthetic tags. -/// -/// Valid tags must not contain whitespace or any of the characters in [`INVALID_TAG_CHARACTERS`]. -/// The first characters additionally cannot be a digit, and subsequent characters cannot be `:`. -/// This definition is based on [that of -/// TaskWarrior](https://github.com/GothenburgBitFactory/taskwarrior/blob/663c6575ceca5bd0135ae884879339dac89d3142/src/Lexer.cpp#L146-L164). -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -// TODO: impl Default -pub enum Tag { - User(String), - Synthetic(SyntheticTag), -} - -pub const INVALID_TAG_CHARACTERS: &str = "+-*/(<>^! %=~"; - -impl Tag { - fn from_str(value: &str) -> Result { - fn err(value: &str) -> Result { - anyhow::bail!("invalid tag {:?}", value) - } - - // first, look for synthetic tags - if value.chars().all(|c| c.is_ascii_uppercase()) { - if let Ok(st) = SyntheticTag::from_str(value) { - return Ok(Self::Synthetic(st)); - } - // all uppercase, but not a valid synthetic tag - return err(value); - } - - if let Some(c) = value.chars().next() { - if c.is_whitespace() || c.is_ascii_digit() || INVALID_TAG_CHARACTERS.contains(c) { - return err(value); - } - } else { - return err(value); - } - if !value - .chars() - .skip(1) - .all(|c| !(c.is_whitespace() || c == ':' || INVALID_TAG_CHARACTERS.contains(c))) - { - return err(value); - } - Ok(Self::User(String::from(value))) - } -} - -impl TryFrom<&str> for Tag { - type Error = anyhow::Error; - - fn try_from(value: &str) -> Result { - Self::from_str(value) - } -} - -impl TryFrom<&String> for Tag { - type Error = anyhow::Error; - - fn try_from(value: &String) -> Result { - Self::from_str(&value[..]) - } -} - -impl fmt::Display for Tag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::User(s) => s.fmt(f), - Self::Synthetic(st) => st.as_ref().fmt(f), - } - } -} - -impl AsRef for Tag { - fn as_ref(&self) -> &str { - match self { - Self::User(s) => s.as_ref(), - Self::Synthetic(st) => st.as_ref(), - } - } -} - -#[derive( - Debug, - Clone, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, - strum_macros::EnumString, - strum_macros::AsRefStr, - strum_macros::EnumIter, -)] -#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] -pub enum SyntheticTag { - Waiting, - Active, -} - -#[derive(Debug, PartialEq)] -pub struct Annotation { - pub entry: Timestamp, - pub description: String, -} - /// A task, as publicly exposed by this crate. /// /// Note that Task objects represent a snapshot of the task at a moment in time, and are not @@ -476,7 +291,6 @@ impl<'r> std::ops::Deref for TaskMut<'r> { #[cfg(test)] mod test { use super::*; - use rstest::rstest; fn with_mut_task(f: F) { let mut replica = Replica::new_inmemory(); @@ -495,32 +309,6 @@ mod test { Tag::Synthetic(synth) } - #[rstest] - #[case::simple("abc")] - #[case::colon_prefix(":abc")] - #[case::letters_and_numbers("a123_456")] - #[case::synthetic("WAITING")] - fn test_tag_try_into_success(#[case] s: &'static str) { - let tag: Tag = s.try_into().unwrap(); - // check Display (via to_string) and AsRef while we're here - assert_eq!(tag.to_string(), s.to_owned()); - assert_eq!(tag.as_ref(), s); - } - - #[rstest] - #[case::empty("")] - #[case::colon_infix("a:b")] - #[case::digits("999")] - #[case::bangs("abc!!!")] - #[case::no_such_synthetic("NOSUCH")] - fn test_tag_try_into_err(#[case] s: &'static str) { - let tag: Result = s.try_into(); - assert_eq!( - tag.unwrap_err().to_string(), - format!("invalid tag \"{}\"", s) - ); - } - #[test] fn test_is_active_never_started() { let task = Task::new(Uuid::new_v4(), TaskMap::new()); @@ -763,24 +551,4 @@ mod test { assert!(!task.taskmap.contains_key("tag.abc")); }); } - - #[test] - fn test_priority() { - assert_eq!(Priority::L.to_taskmap(), "L"); - assert_eq!(Priority::M.to_taskmap(), "M"); - assert_eq!(Priority::H.to_taskmap(), "H"); - assert_eq!(Priority::from_taskmap("L"), Priority::L); - assert_eq!(Priority::from_taskmap("M"), Priority::M); - assert_eq!(Priority::from_taskmap("H"), Priority::H); - } - - #[test] - fn test_status() { - assert_eq!(Status::Pending.to_taskmap(), "P"); - assert_eq!(Status::Completed.to_taskmap(), "C"); - assert_eq!(Status::Deleted.to_taskmap(), "D"); - assert_eq!(Status::from_taskmap("P"), Status::Pending); - assert_eq!(Status::from_taskmap("C"), Status::Completed); - assert_eq!(Status::from_taskmap("D"), Status::Deleted); - } }