use super::tag::{SyntheticTag, TagInner}; use super::{Annotation, Status, Tag, Timestamp}; use crate::replica::Replica; use crate::storage::TaskMap; use chrono::prelude::*; use log::trace; use std::convert::AsRef; use std::convert::TryInto; use std::str::FromStr; use uuid::Uuid; /* The Task and TaskMut classes wrap the underlying [`TaskMap`], which is a simple key/value map. * They provide semantic meaning to that TaskMap according to the TaskChampion data model. For * example, [`get_status`](Task::get_status) and [`set_status`](TaskMut::set_status) translate from * strings in the TaskMap to [`Status`]. * * The same approach applies for more complex data such as dependencies or annotations. Users of * this API should only need the [`get_taskmap`](Task::get_taskmap) method for debugging purposes, * and should never need to make changes to the TaskMap directly. */ /// 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 /// protected by the atomicity of the backend storage. Concurrent modifications are safe, /// but a Task that is cached for more than a few seconds may cause the user to see stale /// data. Fetch, use, and drop Tasks quickly. /// /// This struct contains only getters for various values on the task. The /// [`into_mut`](Task::into_mut) method /// returns a TaskMut which can be used to modify the task. #[derive(Debug, Clone, PartialEq)] pub struct Task { uuid: Uuid, taskmap: TaskMap, } /// A mutable task, with setter methods. /// /// Most methods are simple setters and not further described. Calling a setter will update the /// referenced Replica, as well as the included Task, immediately. /// /// The [`Task`] methods are available on [`TaskMut`] via [`Deref`](std::ops::Deref). pub struct TaskMut<'r> { task: Task, replica: &'r mut Replica, updated_modified: bool, } /// An enum containing all of the key names defined in the data model, with the exception /// of the properties containing data (`tag_..`, etc.) #[derive(strum_macros::AsRefStr, strum_macros::EnumString)] #[strum(serialize_all = "kebab-case")] enum Prop { Description, Modified, Start, Status, Wait, } impl Task { pub(crate) fn new(uuid: Uuid, taskmap: TaskMap) -> Task { Task { uuid, taskmap } } pub fn get_uuid(&self) -> Uuid { self.uuid } pub fn get_taskmap(&self) -> &TaskMap { &self.taskmap } /// Prepare to mutate this task, requiring a mutable Replica /// in order to update the data it contains. pub fn into_mut(self, replica: &mut Replica) -> TaskMut { TaskMut { task: self, replica, updated_modified: false, } } pub fn get_status(&self) -> Status { self.taskmap .get(Prop::Status.as_ref()) .map(|s| Status::from_taskmap(s)) .unwrap_or(Status::Pending) } pub fn get_description(&self) -> &str { self.taskmap .get(Prop::Description.as_ref()) .map(|s| s.as_ref()) .unwrap_or("") } /// Get the wait time. If this value is set, it will be returned, even /// if it is in the past. pub fn get_wait(&self) -> Option> { self.get_timestamp(Prop::Wait.as_ref()) } /// Determine whether this task is waiting now. pub fn is_waiting(&self) -> bool { if let Some(ts) = self.get_wait() { return ts > Utc::now(); } false } /// Determine whether this task is active -- that is, that it has been started /// and not stopped. pub fn is_active(&self) -> bool { self.taskmap.contains_key(Prop::Start.as_ref()) } /// Determine whether a given synthetic tag is present on this task. All other /// synthetic tag calculations are based on this one. fn has_synthetic_tag(&self, synth: &SyntheticTag) -> bool { match synth { SyntheticTag::Waiting => self.is_waiting(), SyntheticTag::Active => self.is_active(), SyntheticTag::Pending => self.get_status() == Status::Pending, SyntheticTag::Completed => self.get_status() == Status::Completed, SyntheticTag::Deleted => self.get_status() == Status::Deleted, } } /// Check if this task has the given tag pub fn has_tag(&self, tag: &Tag) -> bool { match tag.inner() { TagInner::User(s) => self.taskmap.contains_key(&format!("tag_{}", s)), TagInner::Synthetic(st) => self.has_synthetic_tag(st), } } /// Iterate over the task's tags pub fn get_tags(&self) -> impl Iterator + '_ { use strum::IntoEnumIterator; self.taskmap .iter() .filter_map(|(k, _)| { if let Some(tag) = k.strip_prefix("tag_") { if let Ok(tag) = tag.try_into() { return Some(tag); } // note that invalid "tag_*" are ignored } None }) .chain( SyntheticTag::iter() .filter(move |st| self.has_synthetic_tag(st)) .map(|st| Tag::from_inner(TagInner::Synthetic(st))), ) } /// Iterate over the task's annotations, in arbitrary order. pub fn get_annotations(&self) -> impl Iterator + '_ { self.taskmap.iter().filter_map(|(k, v)| { if let Some(ts) = k.strip_prefix("annotation_") { if let Ok(ts) = ts.parse::() { return Some(Annotation { entry: Utc.timestamp(ts, 0), description: v.to_owned(), }); } // note that invalid "annotation_*" are ignored } None }) } /// Get the named user defined attributes (UDA). This will return None /// for any key defined in the Task data model, regardless of whether /// it is set or not. pub fn get_uda(&self, key: &str) -> Option<&str> { if Task::is_known_key(key) { return None; } self.taskmap.get(key).map(|s| s.as_ref()) } /// Get the user defined attributes (UDAs) of this task, in arbitrary order. pub fn get_udas(&self) -> impl Iterator + '_ { self.taskmap .iter() .filter(|(p, _)| !Task::is_known_key(p)) .map(|(p, v)| (p.as_ref(), v.as_ref())) } pub fn get_modified(&self) -> Option> { self.get_timestamp(Prop::Modified.as_ref()) } // -- utility functions fn is_known_key(key: &str) -> bool { Prop::from_str(key).is_ok() || key.starts_with("tag_") || key.starts_with("annotation_") || key.starts_with("dep_") } fn get_timestamp(&self, property: &str) -> Option> { if let Some(ts) = self.taskmap.get(property) { if let Ok(ts) = ts.parse() { return Some(Utc.timestamp(ts, 0)); } // if the value does not parse as an integer, default to None } None } } impl<'r> TaskMut<'r> { /// Get the immutable version of this object, ending the exclusive reference to the Replica. pub fn into_immut(self) -> Task { self.task } /// Set the task's status. This also adds the task to the working set if the /// new status puts it in that set. pub fn set_status(&mut self, status: Status) -> anyhow::Result<()> { if status == Status::Pending { let uuid = self.uuid; self.replica.add_to_working_set(uuid)?; } self.set_string( Prop::Status.as_ref(), Some(String::from(status.to_taskmap())), ) } pub fn set_description(&mut self, description: String) -> anyhow::Result<()> { self.set_string(Prop::Description.as_ref(), Some(description)) } pub fn set_wait(&mut self, wait: Option>) -> anyhow::Result<()> { self.set_timestamp(Prop::Wait.as_ref(), wait) } pub fn set_modified(&mut self, modified: DateTime) -> anyhow::Result<()> { self.set_timestamp(Prop::Modified.as_ref(), Some(modified)) } /// Start the task by creating "start": "", if the task is not already /// active. pub fn start(&mut self) -> anyhow::Result<()> { if self.is_active() { return Ok(()); } self.set_timestamp(Prop::Start.as_ref(), Some(Utc::now())) } /// Stop the task by removing the `start` key pub fn stop(&mut self) -> anyhow::Result<()> { self.set_timestamp(Prop::Start.as_ref(), None) } /// Mark this task as complete pub fn done(&mut self) -> anyhow::Result<()> { self.set_status(Status::Completed) } /// Add a tag to this task. Does nothing if the tag is already present. pub fn add_tag(&mut self, tag: &Tag) -> anyhow::Result<()> { if tag.is_synthetic() { anyhow::bail!("Synthetic tags cannot be modified"); } self.set_string(format!("tag_{}", tag), Some("".to_owned())) } /// Remove a tag from this task. Does nothing if the tag is not present. pub fn remove_tag(&mut self, tag: &Tag) -> anyhow::Result<()> { if tag.is_synthetic() { anyhow::bail!("Synthetic tags cannot be modified"); } self.set_string(format!("tag_{}", tag), None) } /// Add a new annotation. Note that annotations with the same entry time /// will overwrite one another. pub fn add_annotation(&mut self, ann: Annotation) -> anyhow::Result<()> { self.set_string( format!("annotation_{}", ann.entry.timestamp()), Some(ann.description), ) } /// Remove an annotation, based on its entry time. pub fn remove_annotation(&mut self, entry: Timestamp) -> anyhow::Result<()> { self.set_string(format!("annotation_{}", entry.timestamp()), None) } /// Set a user-defined attribute (UDA). This will fail if the key is defined by the data /// model. pub fn set_uda(&mut self, key: S1, value: S2) -> anyhow::Result<()> where S1: Into, S2: Into, { let key = key.into(); if Task::is_known_key(&key) { anyhow::bail!( "Property name {} as special meaning in a task and cannot be used as a UDA", key ); } self.set_string(key, Some(value.into())) } /// Remove a user-defined attribute (UDA). This will fail if the key is defined by the data /// model. pub fn remove_uda(&mut self, key: S) -> anyhow::Result<()> where S: Into, { let key = key.into(); if Task::is_known_key(&key) { anyhow::bail!( "Property name {} as special meaning in a task and cannot be used as a UDA", key ); } self.set_string(key, None) } // -- utility functions fn lastmod(&mut self) -> anyhow::Result<()> { if !self.updated_modified { let now = format!("{}", Utc::now().timestamp()); self.replica .update_task(self.task.uuid, Prop::Modified.as_ref(), Some(now.clone()))?; trace!("task {}: set property modified={:?}", self.task.uuid, now); self.task .taskmap .insert(String::from(Prop::Modified.as_ref()), now); self.updated_modified = true; } Ok(()) } fn set_string>( &mut self, property: S, value: Option, ) -> anyhow::Result<()> { let property = property.into(); self.lastmod()?; self.replica .update_task(self.task.uuid, &property, value.as_ref())?; if let Some(v) = value { trace!("task {}: set property {}={:?}", self.task.uuid, property, v); self.task.taskmap.insert(property, v); } else { trace!("task {}: remove property {}", self.task.uuid, property); self.task.taskmap.remove(&property); } Ok(()) } fn set_timestamp( &mut self, property: &str, value: Option>, ) -> anyhow::Result<()> { self.lastmod()?; if let Some(value) = value { let ts = format!("{}", value.timestamp()); self.replica .update_task(self.task.uuid, property, Some(ts.clone()))?; self.task.taskmap.insert(property.to_string(), ts); } else { self.replica .update_task::<_, &str>(self.task.uuid, property, None)?; self.task.taskmap.remove(property); } Ok(()) } /// Used by tests to ensure that updates are properly written #[cfg(test)] fn reload(&mut self) -> anyhow::Result<()> { let uuid = self.uuid; let task = self.replica.get_task(uuid)?.unwrap(); self.task.taskmap = task.taskmap; Ok(()) } } impl<'r> std::ops::Deref for TaskMut<'r> { type Target = Task; fn deref(&self) -> &Self::Target { &self.task } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; fn with_mut_task(f: F) { let mut replica = Replica::new_inmemory(); let task = replica.new_task(Status::Pending, "test".into()).unwrap(); let task = task.into_mut(&mut replica); f(task) } /// Create a user tag, without checking its validity fn utag(name: &'static str) -> Tag { Tag::from_inner(TagInner::User(name.into())) } /// Create a synthetic tag fn stag(synth: SyntheticTag) -> Tag { Tag::from_inner(TagInner::Synthetic(synth)) } #[test] fn test_is_active_never_started() { let task = Task::new(Uuid::new_v4(), TaskMap::new()); assert!(!task.is_active()); } #[test] fn test_is_active_active() { let task = Task::new( Uuid::new_v4(), vec![(String::from("start"), String::from("1234"))] .drain(..) .collect(), ); assert!(task.is_active()); } #[test] fn test_is_active_inactive() { let task = Task::new(Uuid::new_v4(), Default::default()); assert!(!task.is_active()); } #[test] fn test_wait_not_set() { let task = Task::new(Uuid::new_v4(), TaskMap::new()); assert!(!task.is_waiting()); assert_eq!(task.get_wait(), None); } #[test] fn test_wait_in_past() { let ts = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0); let task = Task::new( Uuid::new_v4(), vec![(String::from("wait"), format!("{}", ts.timestamp()))] .drain(..) .collect(), ); assert!(!task.is_waiting()); assert_eq!(task.get_wait(), Some(ts)); } #[test] fn test_wait_in_future() { let ts = Utc.ymd(3000, 1, 1).and_hms(0, 0, 0); let task = Task::new( Uuid::new_v4(), vec![(String::from("wait"), format!("{}", ts.timestamp()))] .drain(..) .collect(), ); assert!(task.is_waiting()); assert_eq!(task.get_wait(), Some(ts)); } #[test] fn test_has_tag() { let task = Task::new( Uuid::new_v4(), vec![ (String::from("tag_abc"), String::from("")), (String::from("start"), String::from("1234")), ] .drain(..) .collect(), ); assert!(task.has_tag(&utag("abc"))); assert!(!task.has_tag(&utag("def"))); assert!(task.has_tag(&stag(SyntheticTag::Active))); assert!(task.has_tag(&stag(SyntheticTag::Pending))); assert!(!task.has_tag(&stag(SyntheticTag::Waiting))); } #[test] fn test_get_tags() { let task = Task::new( Uuid::new_v4(), vec![ (String::from("tag_abc"), String::from("")), (String::from("tag_def"), String::from("")), // set `wait` so the synthetic tag WAITING is present (String::from("wait"), String::from("33158909732")), ] .drain(..) .collect(), ); let mut tags: Vec<_> = task.get_tags().collect(); tags.sort(); let mut exp = vec![ utag("abc"), utag("def"), stag(SyntheticTag::Pending), stag(SyntheticTag::Waiting), ]; exp.sort(); assert_eq!(tags, exp); } #[test] fn test_get_tags_invalid_tags() { let task = Task::new( Uuid::new_v4(), vec![ (String::from("tag_ok"), String::from("")), (String::from("tag_"), String::from("")), (String::from("tag_123"), String::from("")), (String::from("tag_a!!"), String::from("")), ] .drain(..) .collect(), ); // only "ok" is OK let tags: Vec<_> = task.get_tags().collect(); assert_eq!(tags, vec![utag("ok"), stag(SyntheticTag::Pending)]); } #[test] fn test_get_annotations() { let task = Task::new( Uuid::new_v4(), vec![ ( String::from("annotation_1635301873"), String::from("left message"), ), ( String::from("annotation_1635301883"), String::from("left another message"), ), (String::from("annotation_"), String::from("invalid")), (String::from("annotation_abcde"), String::from("invalid")), ] .drain(..) .collect(), ); let mut anns: Vec<_> = task.get_annotations().collect(); anns.sort(); assert_eq!( anns, vec![ Annotation { entry: Utc.timestamp(1635301873, 0), description: "left message".into() }, Annotation { entry: Utc.timestamp(1635301883, 0), description: "left another message".into() } ] ); } #[test] fn test_add_annotation() { with_mut_task(|mut task| { task.add_annotation(Annotation { entry: Utc.timestamp(1635301900, 0), description: "right message".into(), }) .unwrap(); let k = "annotation_1635301900"; assert_eq!(task.taskmap[k], "right message".to_owned()); task.reload().unwrap(); assert_eq!(task.taskmap[k], "right message".to_owned()); // adding with same time overwrites.. task.add_annotation(Annotation { entry: Utc.timestamp(1635301900, 0), description: "right message 2".into(), }) .unwrap(); assert_eq!(task.taskmap[k], "right message 2".to_owned()); }); } #[test] fn test_remove_annotation() { with_mut_task(|mut task| { task.set_string("annotation_1635301873", Some("left message".into())) .unwrap(); task.set_string("annotation_1635301883", Some("left another message".into())) .unwrap(); task.remove_annotation(Utc.timestamp(1635301873, 0)) .unwrap(); task.reload().unwrap(); let mut anns: Vec<_> = task.get_annotations().collect(); anns.sort(); assert_eq!( anns, vec![Annotation { entry: Utc.timestamp(1635301883, 0), description: "left another message".into() }] ); }); } #[test] fn test_start() { with_mut_task(|mut task| { task.start().unwrap(); assert!(task.taskmap.contains_key("start")); task.reload().unwrap(); assert!(task.taskmap.contains_key("start")); // second start doesn't change anything.. task.start().unwrap(); assert!(task.taskmap.contains_key("start")); task.reload().unwrap(); assert!(task.taskmap.contains_key("start")); }); } #[test] fn test_stop() { with_mut_task(|mut task| { task.start().unwrap(); task.stop().unwrap(); assert!(!task.taskmap.contains_key("start")); task.reload().unwrap(); assert!(!task.taskmap.contains_key("start")); // redundant call does nothing.. task.stop().unwrap(); assert!(!task.taskmap.contains_key("start")); task.reload().unwrap(); assert!(!task.taskmap.contains_key("start")); }); } #[test] fn test_done() { with_mut_task(|mut task| { task.done().unwrap(); assert_eq!(task.get_status(), Status::Completed); assert!(task.has_tag(&stag(SyntheticTag::Completed))); // redundant call does nothing.. task.done().unwrap(); assert_eq!(task.get_status(), Status::Completed); assert!(task.has_tag(&stag(SyntheticTag::Completed))); }); } #[test] fn test_add_tags() { with_mut_task(|mut task| { task.add_tag(&utag("abc")).unwrap(); assert!(task.taskmap.contains_key("tag_abc")); task.reload().unwrap(); assert!(task.taskmap.contains_key("tag_abc")); // redundant add has no effect.. task.add_tag(&utag("abc")).unwrap(); assert!(task.taskmap.contains_key("tag_abc")); }); } #[test] fn test_remove_tags() { with_mut_task(|mut task| { task.add_tag(&utag("abc")).unwrap(); task.reload().unwrap(); assert!(task.taskmap.contains_key("tag_abc")); task.remove_tag(&utag("abc")).unwrap(); assert!(!task.taskmap.contains_key("tag_abc")); // redundant remove has no effect.. task.remove_tag(&utag("abc")).unwrap(); assert!(!task.taskmap.contains_key("tag_abc")); }); } #[test] fn test_get_udas() { let task = Task::new( Uuid::new_v4(), vec![ ("description".into(), "not a uda".into()), ("modified".into(), "not a uda".into()), ("start".into(), "not a uda".into()), ("status".into(), "not a uda".into()), ("wait".into(), "not a uda".into()), ("start".into(), "not a uda".into()), ("tag_abc".into(), "not a uda".into()), ("dep_1234".into(), "not a uda".into()), ("annotation_1234".into(), "not a uda".into()), ("githubid".into(), "123".into()), ] .drain(..) .collect(), ); let udas: Vec<_> = task.get_udas().collect(); assert_eq!(udas, vec![("githubid", "123")]); } #[test] fn test_get_uda() { let task = Task::new( Uuid::new_v4(), vec![ ("description".into(), "not a uda".into()), ("dep_1234".into(), "not a uda".into()), ("githubid".into(), "123".into()), ] .drain(..) .collect(), ); assert_eq!(task.get_uda("description"), None); // invalid UDA assert_eq!(task.get_uda("dep_1234"), None); // invalid UDA assert_eq!(task.get_uda("githubid"), Some("123")); assert_eq!(task.get_uda("jiraid"), None); } #[test] fn test_set_uda() { with_mut_task(|mut task| { task.set_uda("githubid", "123").unwrap(); let udas: Vec<_> = task.get_udas().collect(); assert_eq!(udas, vec![("githubid", "123")]); task.set_uda("jiraid", "TW-1234").unwrap(); let mut udas: Vec<_> = task.get_udas().collect(); udas.sort_unstable(); assert_eq!(udas, vec![("githubid", "123"), ("jiraid", "TW-1234")]); }) } #[test] fn test_set_uda_invalid() { with_mut_task(|mut task| { assert!(task.set_uda("modified", "123").is_err()); assert!(task.set_uda("tag_abc", "123").is_err()); }) } #[test] fn test_rmmove_uda() { with_mut_task(|mut task| { task.set_string("githubid", Some("123".into())).unwrap(); task.remove_uda("githubid").unwrap(); let udas: Vec<_> = task.get_udas().collect(); assert_eq!(udas, vec![]); }) } #[test] fn test_remove_uda_invalid() { with_mut_task(|mut task| { assert!(task.remove_uda("modified").is_err()); assert!(task.remove_uda("tag_abc").is_err()); }) } }