diff --git a/Cargo.lock b/Cargo.lock index 8f78fcdfb..61a97dabe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,12 +131,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - [[package]] name = "failure" version = "0.1.6" @@ -182,15 +176,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "0.4.4" @@ -622,7 +607,6 @@ dependencies = [ "chrono", "clap", "failure", - "itertools", "kv", "lmdb-rkv", "proptest", diff --git a/Cargo.toml b/Cargo.toml index e1f5ba545..8e5c07492 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ failure = {version = "0.1.5", features = ["derive"] } clap = "~2.33.0" kv = {version = "0.10.0", features = ["msgpack-value"]} lmdb-rkv = {version = "0.12.3"} -itertools = "0.9.0" [dev-dependencies] proptest = "0.9.4" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 57c42fe10..4555c20c6 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -6,5 +6,6 @@ - [Data Model](./data-model.md) - [Replica Storage](./storage.md) - [Task Database](./taskdb.md) + - [Tasks](./tasks.md) - [Synchronization](./sync.md) - [Planned Functionality](./plans.md) diff --git a/docs/src/storage.md b/docs/src/storage.md index a4f967fc4..977aace74 100644 --- a/docs/src/storage.md +++ b/docs/src/storage.md @@ -7,47 +7,15 @@ The storage is transaction-protected, with the expectation of a serializable iso The storage contains the following information: - `tasks`: a set of tasks, indexed by UUID -- `base_version`: the number of the last version sync'd from the server +- `base_version`: the number of the last version sync'd from the server (a single integer) - `operations`: all operations performed since base_version - `working_set`: a mapping from integer -> UUID, used to keep stable small-integer indexes into the tasks for users' convenience. This data is not synchronized with the server and does not affect any consistency guarantees. ## Tasks The tasks are stored as an un-ordered collection, keyed by task UUID. -Each task in the database has an arbitrary-sized set of key/value properties, with string values. - -Tasks are only created and modified; "deleted" tasks continue to stick around and can be modified and even un-deleted. -Tasks have an expiration time, after which they may be purged from the database. - -### Task Fields - -Each task can have any of the following fields. -Timestamps are stored as UNIX epoch timestamps, in the form of an integer expressed in decimal notation. -Note that it is possible, in task storage, for any field to be omitted. - -NOTE: This structure is based on https://taskwarrior.org/docs/design/task.html, but will diverge from that -model over time. - -* `status` - one of `Pending`, `Completed`, `Deleted`, `Recurring`, or `Waiting` -* `entry` (timestamp) - time that the task was created -* `description` - the one-line summary of the task -* `start` (timestamp) - if set, the task is active and this field gives the time the task was started -* `end` (timestamp) - the time at which the task was deleted or completed -* `due` (timestamp) - the time at which the task is due -* `until` (timestamp) - the time after which recurrent child tasks should not be created -* `wait` (timestamp) - the time before which this task is considered waiting and should not be shown -* `modified` (timestamp) - time that the task was last modified -* `scheduled` (timestamp) - time that the task is available to start -* `recur` - recurrence frequency -* `mask` - recurrence history -* `imask` - for children of recurring tasks, the index into the `mask` property on the parent -* `parent` - for children of recurring tasks, the uuid of the parent task -* `project` - the task's project (usually a short identifier) -* `priority` - the task's priority, one of `L`, `M`, or `H`. -* `depends` - a comma (`,`) separated list of uuids of tasks on which this task depends -* `tags` - a comma (`,`) separated list of tags for this task -* `annotation_` - an annotation for this task, with the timestamp as part of the key -* `udas` - user-defined attributes +Each task in the database has represented by a key-value map. +See [Tasks](./tasks.md) for details on the content of that map. ## Operations diff --git a/docs/src/tasks.md b/docs/src/tasks.md new file mode 100644 index 000000000..5727237dc --- /dev/null +++ b/docs/src/tasks.md @@ -0,0 +1,38 @@ +# Tasks + +Tasks are stored internally as a key/value map with string keys and values. +All fields are optional: the `Create` operation creates an empty task. +Display layers should apply appropriate defaults where necessary. + +## Atomicity + +The synchronization process does not support read-modify-write operations. +For example, suppose tags are updated by reading a list of tags, adding a tag, and writing the result back. +This would be captured as an `Update` operation containing the amended list of tags. +Suppose two such `Update` operations are made in different replicas and must be reconciled: + * `Update("d394be59-60e6-499e-b7e7-ca0142648409", "tags", "oldtag,newtag1", "2020-11-23T14:21:22Z")` + * `Update("d394be59-60e6-499e-b7e7-ca0142648409", "tags", "oldtag,newtag2", "2020-11-23T15:08:57Z")` + +The result of this reconciliation will be `oldtag,newtag2`, while the user almost certainly intended `oldtag,newtag1,newtag2`. + +The key names given below avoid this issue, allowing user updates such as adding a tag or deleting a dependency to be represented in a single `Update` operation. + +## Representations + +Integers are stored in decimal notation. + +Timestamps are stored as UNIX epoch timestamps, in the form of an integer. + +## Keys + +The following keys, and key formats, are defined: + +* `status` - one of `P` for a pending task (the default), `C` for completed or `D` for deleted +* `description` - the one-line summary of the task +* `modified` - the time of the last modification of this task + +The following are not yet implemented: + +* `dep.` - indicates this task depends on `` (value is an empty string) +* `tag.` - indicates this task has tag `` (value is an empty string) +* `annotation.` - value is an annotation created at the given time diff --git a/src/bin/task.rs b/src/bin/task.rs index b816bcc3c..0362eb83f 100644 --- a/src/bin/task.rs +++ b/src/bin/task.rs @@ -47,8 +47,8 @@ fn main() { ("pending", _) => { let working_set = replica.working_set().unwrap(); for i in 1..working_set.len() { - if let Some((ref uuid, ref task)) = working_set[i] { - println!("{}: {} - {:?}", i, uuid, task); + if let Some(ref task) = working_set[i] { + println!("{}: {} - {:?}", i, task.get_uuid(), task); } } } diff --git a/src/replica.rs b/src/replica.rs index de23f89b0..507ea9291 100644 --- a/src/replica.rs +++ b/src/replica.rs @@ -1,11 +1,10 @@ use crate::errors::Error; use crate::operation::Operation; -use crate::task::{Priority, Status, Task, TaskBuilder}; +use crate::task::{Status, Task}; use crate::taskdb::DB; use crate::taskstorage::TaskMap; -use chrono::{DateTime, Utc}; +use chrono::Utc; use failure::Fallible; -use itertools::join; use std::collections::HashMap; use uuid::Uuid; @@ -22,7 +21,12 @@ impl Replica { /// Update an existing task. If the value is Some, the property is added or updated. If the /// value is None, the property is deleted. It is not an error to delete a nonexistent /// property. - fn update_task(&mut self, uuid: Uuid, property: S1, value: Option) -> Fallible<()> + pub(crate) fn update_task( + &mut self, + uuid: Uuid, + property: S1, + value: Option, + ) -> Fallible<()> where S1: Into, S2: Into, @@ -35,29 +39,18 @@ impl Replica { }) } - /// Return true if this status string is such that the task should be included in - /// the working set. - fn is_working_set_status(status: Option<&String>) -> bool { - if let Some(status) = status { - status == "pending" - } else { - false - } - } - /// Add the given uuid to the working set, returning its index. - fn add_to_working_set(&mut self, uuid: &Uuid) -> Fallible { + pub(crate) fn add_to_working_set(&mut self, uuid: &Uuid) -> Fallible { self.taskdb.add_to_working_set(uuid) } /// Get all tasks represented as a map keyed by UUID pub fn all_tasks<'a>(&'a mut self) -> Fallible> { - Ok(self - .taskdb - .all_tasks()? - .iter() - .map(|(k, v)| (k.clone(), v.into())) - .collect()) + let mut res = HashMap::new(); + for (uuid, tm) in self.taskdb.all_tasks()?.drain(..) { + res.insert(uuid.clone(), Task::new(uuid.clone(), tm)); + } + Ok(res) } /// Get the UUIDs of all tasks @@ -67,13 +60,13 @@ impl Replica { /// Get the "working set" for this replica -- the set of pending tasks, as indexed by small /// integers - pub fn working_set(&mut self) -> Fallible>> { + pub fn working_set(&mut self) -> Fallible>> { let working_set = self.taskdb.working_set()?; let mut res = Vec::with_capacity(working_set.len()); for i in 0..working_set.len() { res.push(match working_set[i] { Some(u) => match self.taskdb.get_task(&u)? { - Some(task) => Some((u, (&task).into())), + Some(tm) => Some(Task::new(u, tm)), None => None, }, None => None, @@ -84,7 +77,10 @@ impl Replica { /// Get an existing task by its UUID pub fn get_task(&mut self, uuid: &Uuid) -> Fallible> { - Ok(self.taskdb.get_task(&uuid)?.map(|t| (&t).into())) + Ok(self + .taskdb + .get_task(uuid)? + .map(move |tm| Task::new(uuid.clone(), tm))) } /// Get an existing task by its working set index @@ -92,19 +88,17 @@ impl Replica { let working_set = self.taskdb.working_set()?; if (i as usize) < working_set.len() { if let Some(uuid) = working_set[i as usize] { - return Ok(self.taskdb.get_task(&uuid)?.map(|t| (&t).into())); + return Ok(self + .taskdb + .get_task(&uuid)? + .map(move |tm| Task::new(uuid, tm))); } } return Ok(None); } /// Create a new task. The task must not already exist. - pub fn new_task( - &mut self, - uuid: Uuid, - status: Status, - description: String, - ) -> Fallible { + pub fn new_task(&mut self, uuid: Uuid, status: Status, description: String) -> Fallible { // check that it doesn't exist; this is a convenience check, as the task // may already exist when this Create operation is finally sync'd with // operations from other replicas @@ -113,15 +107,14 @@ impl Replica { } self.taskdb .apply(Operation::Create { uuid: uuid.clone() })?; - self.update_task(uuid.clone(), "status", Some(String::from(status.as_ref())))?; - self.update_task(uuid.clone(), "description", Some(description))?; - let now = format!("{}", Utc::now().timestamp()); - self.update_task(uuid.clone(), "entry", Some(now.clone()))?; - self.update_task(uuid.clone(), "modified", Some(now))?; - Ok(TaskMut::new(self, uuid)) + let mut task = Task::new(uuid, TaskMap::new()).into_mut(self); + task.set_description(description)?; + task.set_status(status)?; + Ok(task.into_immut()) } - /// Delete a task. The task must exist. + /// Delete a task. The task must exist. Note that this is different from setting status to + /// Deleted; this is the final purge of the task. pub fn delete_task(&mut self, uuid: Uuid) -> Fallible<()> { // check that it already exists; this is a convenience check, as the task may already exist // when this Create operation is finally sync'd with operations from other replicas @@ -131,196 +124,61 @@ impl Replica { self.taskdb.apply(Operation::Delete { uuid }) } - /// Get an existing task by its UUID, suitable for modification - pub fn get_task_mut<'a>(&'a mut self, uuid: &Uuid) -> Fallible>> { - // the call to get_task is to ensure the task exists locally - Ok(self - .taskdb - .get_task(&uuid)? - .map(move |_| TaskMut::new(self, uuid.clone()))) - } - /// Perform "garbage collection" on this replica. In particular, this renumbers the working /// set to contain only pending tasks. pub fn gc(&mut self) -> Fallible<()> { + let pending = String::from(Status::Pending.to_taskmap()); self.taskdb - .rebuild_working_set(|t| Replica::is_working_set_status(t.get("status")))?; + .rebuild_working_set(|t| t.get("status") == Some(&pending))?; Ok(()) } } -impl From<&TaskMap> for Task { - fn from(taskmap: &TaskMap) -> Task { - let mut bldr = TaskBuilder::new(); - for (k, v) in taskmap.iter() { - bldr = bldr.set(k, v.into()); - } - bldr.finish() - } -} - -// TODO: move this struct to crate::task, with a trait for update_task, since it is the reverse -// of TaskBuilder::set -/// TaskMut allows changes to a task. It is intended for short-term use, such as changing a few -/// properties, and should not be held for long periods of wall-clock time. -pub struct TaskMut<'a> { - replica: &'a mut Replica, - uuid: Uuid, - // if true, then this TaskMut has already updated the `modified` property and need not do so - // again. - updated_modified: bool, -} - -impl<'a> TaskMut<'a> { - fn new(replica: &'a mut Replica, uuid: Uuid) -> TaskMut { - TaskMut { - replica, - uuid, - updated_modified: false, - } - } - - fn lastmod(&mut self) -> Fallible<()> { - if !self.updated_modified { - let now = format!("{}", Utc::now().timestamp()); - self.replica - .update_task(self.uuid.clone(), "modified", Some(now))?; - self.updated_modified = true; - } - Ok(()) - } - - fn set_string(&mut self, property: &str, value: Option) -> Fallible<()> { - self.lastmod()?; - self.replica.update_task(self.uuid.clone(), property, value) - } - - fn set_timestamp(&mut self, property: &str, value: Option>) -> Fallible<()> { - self.lastmod()?; - self.replica.update_task( - self.uuid.clone(), - property, - value.map(|v| format!("{}", v.timestamp())), - ) - } - - /// 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 status(&mut self, status: Status) -> Fallible<()> { - let status = String::from(status.as_ref()); - if Replica::is_working_set_status(Some(&status)) { - self.replica.add_to_working_set(&self.uuid)?; - } - self.set_string("status", Some(status)) - } - - /// Set the task's description - pub fn description(&mut self, description: String) -> Fallible<()> { - self.set_string("description", Some(description)) - } - - /// Set the task's start time - pub fn start(&mut self, time: Option>) -> Fallible<()> { - self.set_timestamp("start", time) - } - - /// Set the task's end time - pub fn end(&mut self, time: Option>) -> Fallible<()> { - self.set_timestamp("end", time) - } - - /// Set the task's due time - pub fn due(&mut self, time: Option>) -> Fallible<()> { - self.set_timestamp("due", time) - } - - /// Set the task's until time - pub fn until(&mut self, time: Option>) -> Fallible<()> { - self.set_timestamp("until", time) - } - - /// Set the task's wait time - pub fn wait(&mut self, time: Option>) -> Fallible<()> { - self.set_timestamp("wait", time) - } - - /// Set the task's scheduled time - pub fn scheduled(&mut self, time: Option>) -> Fallible<()> { - self.set_timestamp("scheduled", time) - } - - /// Set the task's recur value - pub fn recur(&mut self, recur: Option) -> Fallible<()> { - self.set_string("recur", recur) - } - - /// Set the task's mask value - pub fn mask(&mut self, mask: Option) -> Fallible<()> { - self.set_string("mask", mask) - } - - /// Set the task's imask value - pub fn imask(&mut self, imask: Option) -> Fallible<()> { - self.set_string("imask", imask.map(|v| format!("{}", v))) - } - - /// Set the task's parent task - pub fn parent(&mut self, parent: Option) -> Fallible<()> { - self.set_string("parent", parent.map(|v| format!("{}", v))) - } - - /// Set the task's project - pub fn project(&mut self, project: Option) -> Fallible<()> { - self.set_string("project", project) - } - - /// Set the task's priority - pub fn priority(&mut self, priority: Option) -> Fallible<()> { - self.set_string("priority", priority.map(|v| String::from(v.as_ref()))) - } - - /// Set the task's depends; note that this completely replaces the list of tasks on which this - /// one depends. - pub fn depends(&mut self, depends: Vec) -> Fallible<()> { - self.set_string( - "depends", - if depends.len() > 0 { - Some(join(depends.iter(), ",")) - } else { - None - }, - ) - } - - /// Set the task's tags; note that this completely replaces the list of tags - pub fn tags(&mut self, tags: Vec) -> Fallible<()> { - self.set_string("tags", Some(join(tags.iter(), ","))) - } - - // TODO: annotations - // TODO: udas -} - #[cfg(test)] mod tests { use super::*; + use crate::task::Status; use crate::taskdb::DB; use uuid::Uuid; #[test] - fn new_task_and_modify() { + fn new_task() { let mut rep = Replica::new(DB::new_inmemory().into()); let uuid = Uuid::new_v4(); - let mut tm = rep + let t = rep .new_task(uuid.clone(), Status::Pending, "a task".into()) .unwrap(); - tm.priority(Some(Priority::L)).unwrap(); + assert_eq!(t.get_description(), String::from("a task")); + assert_eq!(t.get_status(), Status::Pending); + assert!(t.get_modified().is_some()); + } + #[test] + fn modify_task() { + let mut rep = Replica::new(DB::new_inmemory().into()); + let uuid = Uuid::new_v4(); + + let t = rep + .new_task(uuid.clone(), Status::Pending, "a task".into()) + .unwrap(); + + let mut t = t.into_mut(&mut rep); + t.set_description(String::from("past tense")).unwrap(); + t.set_status(Status::Completed).unwrap(); + // check that values have changed on the TaskMut + assert_eq!(t.get_description(), "past tense"); + assert_eq!(t.get_status(), Status::Completed); + + // check that values have changed after into_immut + let t = t.into_immut(); + assert_eq!(t.get_description(), "past tense"); + assert_eq!(t.get_status(), Status::Completed); + + // check tha values have changed in storage, too let t = rep.get_task(&uuid).unwrap().unwrap(); - assert_eq!(t.description, String::from("a task")); - assert_eq!(t.status, Status::Pending); - assert_eq!(t.priority, Some(Priority::L)); + assert_eq!(t.get_description(), "past tense"); + assert_eq!(t.get_status(), Status::Completed); } #[test] @@ -344,34 +202,33 @@ mod tests { .unwrap(); let t = rep.get_task(&uuid).unwrap().unwrap(); - assert_eq!(t.description, String::from("another task")); + assert_eq!(t.get_description(), String::from("another task")); - let mut tm = rep.get_task_mut(&uuid).unwrap().unwrap(); - tm.status(Status::Completed).unwrap(); - tm.description("another task, updated".into()).unwrap(); - tm.priority(Some(Priority::L)).unwrap(); - tm.project(Some("work".into())).unwrap(); + let mut t = t.into_mut(&mut rep); + t.set_status(Status::Deleted).unwrap(); + t.set_description("gone".into()).unwrap(); let t = rep.get_task(&uuid).unwrap().unwrap(); - assert_eq!(t.status, Status::Completed); - assert_eq!(t.description, String::from("another task, updated")); - assert_eq!(t.project, Some("work".into())); + assert_eq!(t.get_status(), Status::Deleted); + assert_eq!(t.get_description(), "gone"); } #[test] - fn set_pending_adds_to_working_set() { + fn new_pending_adds_to_working_set() { let mut rep = Replica::new(DB::new_inmemory().into()); let uuid = Uuid::new_v4(); rep.new_task(uuid.clone(), Status::Pending, "to-be-pending".into()) .unwrap(); - let mut tm = rep.get_task_mut(&uuid).unwrap().unwrap(); - tm.status(Status::Pending).unwrap(); - let t = rep.get_working_set_task(1).unwrap().unwrap(); - assert_eq!(t.status, Status::Pending); - assert_eq!(t.description, String::from("to-be-pending")); + assert_eq!(t.get_status(), Status::Pending); + assert_eq!(t.get_description(), "to-be-pending"); + + let ws = rep.working_set().unwrap(); + assert_eq!(ws.len(), 2); + assert!(ws[0].is_none()); + assert_eq!(ws[1].as_ref().unwrap().get_uuid(), &uuid); } #[test] diff --git a/src/task/mod.rs b/src/task/mod.rs index f22b30c70..5e6b67770 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -1,7 +1,5 @@ mod task; -mod taskbuilder; pub use self::task::Priority::*; pub use self::task::Status::*; pub use self::task::{Annotation, Priority, Status, Task, Timestamp}; -pub use self::taskbuilder::TaskBuilder; diff --git a/src/task/task.rs b/src/task/task.rs index e6945f65a..a8b95989b 100644 --- a/src/task/task.rs +++ b/src/task/task.rs @@ -1,5 +1,7 @@ +use crate::replica::Replica; +use crate::taskstorage::TaskMap; use chrono::prelude::*; -use std::collections::HashMap; +use failure::Fallible; use std::convert::TryFrom; use uuid::Uuid; @@ -39,33 +41,26 @@ pub enum Status { Pending, Completed, Deleted, - Recurring, - Waiting, } -impl TryFrom<&str> for Status { - type Error = failure::Error; - - fn try_from(s: &str) -> Result { +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 { - "pending" => Ok(Status::Pending), - "completed" => Ok(Status::Completed), - "deleted" => Ok(Status::Deleted), - "recurring" => Ok(Status::Recurring), - "waiting" => Ok(Status::Waiting), - _ => Err(format_err!("invalid status {}", s)), + "P" => Status::Pending, + "C" => Status::Completed, + "D" => Status::Deleted, + _ => Status::Pending, } } -} -impl AsRef for Status { - fn as_ref(&self) -> &str { + /// Get the 1-character value for this status to use in the TaskMap. + pub(crate) fn to_taskmap(&self) -> &str { match self { - Status::Pending => "pending", - Status::Completed => "completed", - Status::Deleted => "deleted", - Status::Recurring => "recurring", - Status::Waiting => "waiting", + Status::Pending => "P", + Status::Completed => "C", + Status::Deleted => "D", } } } @@ -76,30 +71,145 @@ pub struct Annotation { pub description: String, } -/// A task, the fundamental business object of this tool. +/// A task, as publicly exposed by this crate. /// -/// This structure is based on https://taskwarrior.org/docs/design/task.html with the -/// exception that the uuid property is omitted. +/// 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` method returns +/// a TaskMut which can be used to modify the task. #[derive(Debug, PartialEq)] pub struct Task { - pub status: Status, - pub entry: Timestamp, - pub description: String, - pub start: Option, - pub end: Option, - pub due: Option, - pub until: Option, - pub wait: Option, - pub modified: Timestamp, - pub scheduled: Option, - pub recur: Option, - pub mask: Option, - pub imask: Option, - pub parent: Option, - pub project: Option, - pub priority: Option, - pub depends: Vec, - pub tags: Vec, - pub annotations: Vec, - pub udas: HashMap, + uuid: Uuid, + taskmap: TaskMap, +} + +/// A mutable task, with setter methods. Calling a setter will update the Replica, as well as the +/// included Task. +pub struct TaskMut<'r> { + task: Task, + replica: &'r mut Replica, + updated_modified: bool, +} + +impl Task { + pub(crate) fn new(uuid: Uuid, taskmap: TaskMap) -> Task { + Task { uuid, taskmap } + } + + pub fn get_uuid(&self) -> &Uuid { + &self.uuid + } + + /// 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: replica, + updated_modified: false, + } + } + + pub fn get_status(&self) -> Status { + self.taskmap + .get("status") + .map(|s| Status::from_taskmap(s)) + .unwrap_or(Status::Pending) + } + + pub fn get_description(&self) -> &str { + self.taskmap + .get("description") + .map(|s| s.as_ref()) + .unwrap_or("") + } + + pub fn get_modified(&self) -> Option> { + self.get_timestamp("modified") + } + + // -- utility functions + + pub 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 task + 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) -> Fallible<()> { + if status == Status::Pending { + let uuid = self.uuid.clone(); + self.replica.add_to_working_set(&uuid)?; + } + self.set_string("status", Some(String::from(status.to_taskmap()))) + } + + /// Set the task's description + pub fn set_description(&mut self, description: String) -> Fallible<()> { + self.set_string("description", Some(description)) + } + + /// Set the task's description + pub fn set_modified(&mut self, modified: DateTime) -> Fallible<()> { + self.set_timestamp("modified", Some(modified)) + } + + // -- utility functions + + fn lastmod(&mut self) -> Fallible<()> { + if !self.updated_modified { + let now = format!("{}", Utc::now().timestamp()); + self.replica + .update_task(self.task.uuid.clone(), "modified", Some(now.clone()))?; + self.task.taskmap.insert(String::from("modified"), now); + self.updated_modified = true; + } + Ok(()) + } + + fn set_string(&mut self, property: &str, value: Option) -> Fallible<()> { + self.lastmod()?; + self.replica + .update_task(self.task.uuid.clone(), property, value.as_ref())?; + + if let Some(v) = value { + self.task.taskmap.insert(property.to_string(), v); + } else { + self.task.taskmap.remove(property); + } + Ok(()) + } + + fn set_timestamp(&mut self, property: &str, value: Option>) -> Fallible<()> { + self.lastmod()?; + self.replica.update_task( + self.task.uuid.clone(), + property, + value.map(|v| format!("{}", v.timestamp())), + ) + } +} + +impl<'r> std::ops::Deref for TaskMut<'r> { + type Target = Task; + + fn deref(&self) -> &Self::Target { + &self.task + } } diff --git a/src/task/taskbuilder.rs b/src/task/taskbuilder.rs deleted file mode 100644 index 6d5bc0d92..000000000 --- a/src/task/taskbuilder.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::task::{Annotation, Priority, Status, Task, Timestamp}; -use chrono::prelude::*; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::str; -use uuid::Uuid; - -#[derive(Default)] -pub struct TaskBuilder { - status: Option, - entry: Option, - description: Option, - start: Option, - end: Option, - due: Option, - until: Option, - wait: Option, - modified: Option, - scheduled: Option, - recur: Option, - mask: Option, - imask: Option, - parent: Option, - project: Option, - priority: Option, - depends: Vec, - tags: Vec, - annotations: Vec, - udas: HashMap, -} - -/// Parse an "integer", allowing for occasional integers with trailing decimal zeroes -fn parse_int(value: &str) -> Result::Err> -where - T: str::FromStr, -{ - // some integers are rendered with following decimal zeroes - if let Some(i) = value.find('.') { - let mut nonzero = false; - for c in value[i + 1..].chars() { - if c != '0' { - nonzero = true; - break; - } - } - if !nonzero { - return value[..i].parse(); - } - } - value.parse() -} - -/// Parse a UNIX timestamp into a UTC DateTime -fn parse_timestamp(value: &str) -> Result::Err> { - Ok(Utc.timestamp(parse_int::(value)?, 0)) -} - -/// Parse depends, as a list of ,-separated UUIDs -fn parse_depends(value: &str) -> Result, uuid::Error> { - value.split(',').map(|s| Uuid::parse_str(s)).collect() -} - -/// Parse tags, as a list of ,-separated strings -fn parse_tags(value: &str) -> Vec { - value.split(',').map(|s| s.to_string()).collect() -} - -impl TaskBuilder { - pub fn new() -> Self { - Default::default() - } - - // TODO: fallible - pub fn set(mut self, name: &str, value: String) -> Self { - const ANNOTATION_PREFIX: &str = "annotation_"; - if name.starts_with(ANNOTATION_PREFIX) { - let entry = parse_timestamp(&name[ANNOTATION_PREFIX.len()..]).unwrap(); - // TODO: sort by entry time - self.annotations.push(Annotation { - entry, - description: value.to_string(), - }); - return self; - } - match name { - "status" => self.status = Some(Status::try_from(value.as_ref()).unwrap()), - "entry" => self.entry = Some(parse_timestamp(&value).unwrap()), - "description" => self.description = Some(value), - "start" => self.start = Some(parse_timestamp(&value).unwrap()), - "end" => self.end = Some(parse_timestamp(&value).unwrap()), - "due" => self.due = Some(parse_timestamp(&value).unwrap()), - "until" => self.until = Some(parse_timestamp(&value).unwrap()), - "wait" => self.wait = Some(parse_timestamp(&value).unwrap()), - "modified" => self.modified = Some(parse_timestamp(&value).unwrap()), - "scheduled" => self.scheduled = Some(parse_timestamp(&value).unwrap()), - "recur" => self.recur = Some(value), - "mask" => self.mask = Some(value), - "imask" => self.imask = Some(parse_int::(&value).unwrap()), - "parent" => self.parent = Some(Uuid::parse_str(&value).unwrap()), - "project" => self.project = Some(value), - "priority" => self.priority = Some(Priority::try_from(value.as_ref()).unwrap()), - "depends" => self.depends = parse_depends(&value).unwrap(), - "tags" => self.tags = parse_tags(&value), - _ => { - self.udas.insert(name.to_string(), value); - } - } - self - } - - pub fn finish(self) -> Task { - Task { - status: self.status.unwrap(), - description: self.description.unwrap(), - entry: self.entry.unwrap(), - start: self.start, - end: self.end, - due: self.due, - until: self.until, - wait: self.wait, - modified: self.modified.unwrap(), - scheduled: self.scheduled, - recur: self.recur, - mask: self.mask, - imask: self.imask, - parent: self.parent, - project: self.project, - priority: self.priority, - depends: self.depends, - tags: self.tags, - annotations: self.annotations, - udas: self.udas, - } - } -} - -#[cfg(test)] -mod test { - use super::{parse_depends, parse_int}; - use uuid::Uuid; - - #[test] - fn test_parse_int() { - assert_eq!(parse_int::("123").unwrap(), 123u8); - assert_eq!(parse_int::("123000000").unwrap(), 123000000u32); - assert_eq!(parse_int::("-123000000").unwrap(), -123000000i32); - } - - #[test] - fn test_parse_int_decimals() { - assert_eq!(parse_int::("123.00").unwrap(), 123u8); - assert_eq!(parse_int::("123.0000").unwrap(), 123u32); - assert_eq!(parse_int::("-123.").unwrap(), -123i32); - } - - #[test] - fn test_parse_depends() { - let u1 = "123e4567-e89b-12d3-a456-426655440000"; - let u2 = "123e4567-e89b-12d3-a456-999999990000"; - assert_eq!( - parse_depends(u1).unwrap(), - vec![Uuid::parse_str(u1).unwrap()] - ); - assert_eq!( - parse_depends(&format!("{},{}", u1, u2)).unwrap(), - vec![Uuid::parse_str(u1).unwrap(), Uuid::parse_str(u2).unwrap()] - ); - } -}