diff --git a/TODO.txt b/TODO.txt index 045580c49..23ad27b6f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,7 @@ * add HTTP API * add pending-task indexing to Replica * abstract server into trait +* fix TODO items in replica.rs * implement snapshot requests * implement backups * implement client-side encryption diff --git a/src/bin/task.rs b/src/bin/task.rs index d692cf384..13a3a749b 100644 --- a/src/bin/task.rs +++ b/src/bin/task.rs @@ -1,7 +1,7 @@ extern crate clap; use clap::{App, Arg, SubCommand}; use std::path::Path; -use taskwarrior_rust::{taskstorage, Replica, DB}; +use taskwarrior_rust::{taskstorage, Replica, Status, DB}; use uuid::Uuid; fn main() { @@ -10,9 +10,11 @@ fn main() { .author("Dustin J. Mitchell ") .about("Replacement for TaskWarrior") .subcommand( - SubCommand::with_name("add") - .about("adds a task") - .arg(Arg::with_name("title").help("task title").required(true)), + SubCommand::with_name("add").about("adds a task").arg( + Arg::with_name("descrpition") + .help("task descrpition") + .required(true), + ), ) .subcommand(SubCommand::with_name("list").about("lists tasks")) .get_matches(); @@ -27,9 +29,12 @@ fn main() { match matches.subcommand() { ("add", Some(matches)) => { let uuid = Uuid::new_v4(); - replica.create_task(uuid).unwrap(); replica - .update_task(uuid, "title", Some(matches.value_of("title").unwrap())) + .new_task( + uuid, + Status::Pending, + matches.value_of("descrpition").unwrap().into(), + ) .unwrap(); } ("list", _) => { diff --git a/src/lib.rs b/src/lib.rs index 8566989cc..475ea1d6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,8 @@ mod tdb2; pub use operation::Operation; pub use replica::Replica; pub use server::Server; +pub use task::Priority; +pub use task::Status; pub use task::Task; pub use taskdb::DB; diff --git a/src/replica.rs b/src/replica.rs index c2df51fc8..59a9dd89d 100644 --- a/src/replica.rs +++ b/src/replica.rs @@ -1,8 +1,10 @@ use crate::operation::Operation; +use crate::task::{Priority, Status, Task, TaskBuilder}; use crate::taskdb::DB; use crate::taskstorage::TaskMap; use chrono::Utc; use failure::Fallible; +use std::collections::HashMap; use uuid::Uuid; /// A replica represents an instance of a user's task data. @@ -15,25 +17,10 @@ impl Replica { return Replica { taskdb }; } - /// Create a new task. The task must not already exist. - pub fn create_task(&mut self, uuid: Uuid) -> Fallible<()> { - self.taskdb.apply(Operation::Create { uuid }) - } - - /// Delete a task. The task must exist. - pub fn delete_task(&mut self, uuid: Uuid) -> Fallible<()> { - self.taskdb.apply(Operation::Delete { uuid }) - } - /// 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. - pub fn update_task( - &mut self, - uuid: Uuid, - property: S1, - value: Option, - ) -> Fallible<()> + fn update_task(&mut self, uuid: Uuid, property: S1, value: Option) -> Fallible<()> where S1: Into, S2: Into, @@ -46,9 +33,14 @@ impl Replica { }) } - /// Get all tasks as an iterator of (&Uuid, &HashMap) - pub fn all_tasks<'a>(&'a mut self) -> Fallible> { - self.taskdb.all_tasks() + /// 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()) } /// Get the UUIDs of all tasks @@ -57,9 +49,126 @@ impl Replica { } /// Get an existing task by its UUID - pub fn get_task(&mut self, uuid: &Uuid) -> Fallible> { - self.taskdb.get_task(&uuid) + pub fn get_task(&mut self, uuid: &Uuid) -> Fallible> { + Ok(self.taskdb.get_task(&uuid)?.map(|t| (&t).into())) } + + /// Create a new task. The task must not already exist. + pub fn new_task( + &mut self, + uuid: Uuid, + status: Status, + description: String, + ) -> Fallible { + // TODO: check that it doesn't exist + 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)) + } + + /// Delete a task. The task must exist. + pub fn delete_task(&mut self, uuid: Uuid) -> Fallible<()> { + // TODO: must verify task does exist + 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()))) + } +} + +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() + } +} + +/// 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(()) + } + + /// Set the task's status + pub fn status(&mut self, status: Status) -> Fallible<()> { + self.lastmod()?; + self.replica.update_task( + self.uuid.clone(), + "status", + Some(String::from(status.as_ref())), + ) + } + + // TODO: description + // TODO: start + // TODO: end + // TODO: due + // TODO: until + // TODO: wait + // TODO: scheduled + // TODO: recur + // TODO: mask + // TODO: imask + // TODO: parent + + /// Set the task's project + pub fn project(&mut self, project: String) -> Fallible<()> { + self.lastmod()?; + self.replica + .update_task(self.uuid.clone(), "project", Some(project)) + } + + /// Set the task's priority + pub fn priority(&mut self, priority: Priority) -> Fallible<()> { + self.lastmod()?; + self.replica.update_task( + self.uuid.clone(), + "priority", + Some(String::from(priority.as_ref())), + ) + } + + // TODO: depends + // TODO: tags + // TODO: annotations + // TODO: udas } #[cfg(test)] @@ -69,35 +178,48 @@ mod tests { use uuid::Uuid; #[test] - fn create() { + fn new_task_and_modify() { let mut rep = Replica::new(DB::new_inmemory().into()); let uuid = Uuid::new_v4(); - rep.create_task(uuid.clone()).unwrap(); - assert_eq!(rep.get_task(&uuid).unwrap(), Some(TaskMap::new())); + let mut tm = rep + .new_task(uuid.clone(), Status::Pending, "a task".into()) + .unwrap(); + tm.priority(Priority::L).unwrap(); + + 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)); } #[test] - fn delete() { + fn delete_task() { let mut rep = Replica::new(DB::new_inmemory().into()); let uuid = Uuid::new_v4(); - rep.create_task(uuid.clone()).unwrap(); + rep.new_task(uuid.clone(), Status::Pending, "a task".into()) + .unwrap(); + rep.delete_task(uuid.clone()).unwrap(); assert_eq!(rep.get_task(&uuid).unwrap(), None); } #[test] - fn update() { + fn get_and_modify() { let mut rep = Replica::new(DB::new_inmemory().into()); let uuid = Uuid::new_v4(); - rep.create_task(uuid.clone()).unwrap(); - rep.update_task(uuid.clone(), "title", Some("snarsblat")) + rep.new_task(uuid.clone(), Status::Pending, "another task".into()) .unwrap(); - let mut task = TaskMap::new(); - task.insert("title".into(), "snarsblat".into()); - assert_eq!(rep.get_task(&uuid).unwrap(), Some(task)); + + let mut tm = rep.get_task_mut(&uuid).unwrap().unwrap(); + tm.priority(Priority::L).unwrap(); + tm.project("work".into()).unwrap(); + + let t = rep.get_task(&uuid).unwrap().unwrap(); + assert_eq!(t.description, String::from("another task")); + assert_eq!(t.project, Some("work".into())); } #[test] diff --git a/src/task/task.rs b/src/task/task.rs index 29e8f4925..e6945f65a 100644 --- a/src/task/task.rs +++ b/src/task/task.rs @@ -1,6 +1,7 @@ -use std::collections::HashMap; -use uuid::Uuid; use chrono::prelude::*; +use std::collections::HashMap; +use std::convert::TryFrom; +use uuid::Uuid; pub type Timestamp = DateTime; @@ -11,6 +12,28 @@ pub enum Priority { H, } +impl TryFrom<&str> for Priority { + type Error = failure::Error; + + fn try_from(s: &str) -> Result { + match s { + "L" => Ok(Priority::L), + "M" => Ok(Priority::M), + "H" => Ok(Priority::H), + _ => Err(format_err!("invalid status {}", s)), + } + } +} + +impl AsRef for Priority { + fn as_ref(&self) -> &str { + match self { + Priority::L => "L", + Priority::M => "M", + Priority::H => "H", + } + } +} #[derive(Debug, PartialEq)] pub enum Status { Pending, @@ -20,6 +43,33 @@ pub enum Status { Waiting, } +impl TryFrom<&str> for Status { + type Error = failure::Error; + + fn try_from(s: &str) -> Result { + 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)), + } + } +} + +impl AsRef for Status { + fn as_ref(&self) -> &str { + match self { + Status::Pending => "pending", + Status::Completed => "completed", + Status::Deleted => "deleted", + Status::Recurring => "recurring", + Status::Waiting => "waiting", + } + } +} + #[derive(Debug, PartialEq)] pub struct Annotation { pub entry: Timestamp, @@ -28,11 +78,11 @@ pub struct Annotation { /// A task, the fundamental business object of this tool. /// -/// This structure is based on https://taskwarrior.org/docs/design/task.html -#[derive(Debug)] +/// This structure is based on https://taskwarrior.org/docs/design/task.html with the +/// exception that the uuid property is omitted. +#[derive(Debug, PartialEq)] pub struct Task { pub status: Status, - pub uuid: Uuid, pub entry: Timestamp, pub description: String, pub start: Option, diff --git a/src/task/taskbuilder.rs b/src/task/taskbuilder.rs index f1d03fc06..53d805cf0 100644 --- a/src/task/taskbuilder.rs +++ b/src/task/taskbuilder.rs @@ -1,14 +1,13 @@ use crate::task::{Annotation, Priority, Status, Task, Timestamp}; use chrono::prelude::*; -use failure::Fallible; use std::collections::HashMap; +use std::convert::TryFrom; use std::str; use uuid::Uuid; #[derive(Default)] pub struct TaskBuilder { status: Option, - uuid: Option, entry: Option, description: Option, start: Option, @@ -51,29 +50,6 @@ where value.parse() } -/// Parse a status into a Status enum value -fn parse_status(value: &str) -> Fallible { - match value { - "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 {}", value)), - } -} - -/// Parse "L", "M", "H" into the Priority enum - -fn parse_priority(value: &str) -> Fallible { - match value { - "L" => Ok(Priority::L), - "M" => Ok(Priority::M), - "H" => Ok(Priority::H), - _ => Err(format_err!("invalid priority {}", value)), - } -} - /// Parse a UNIX timestamp into a UTC DateTime fn parse_timestamp(value: &str) -> Result::Err> { Ok(Utc.timestamp(parse_int::(value)?, 0)) @@ -94,6 +70,7 @@ impl TaskBuilder { 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) { @@ -106,8 +83,7 @@ impl TaskBuilder { return self; } match name { - "status" => self.status = Some(parse_status(&value).unwrap()), - "uuid" => self.uuid = Some(Uuid::parse_str(&value).unwrap()), + "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()), @@ -120,9 +96,9 @@ impl TaskBuilder { "recur" => self.recur = Some(value), "mask" => self.mask = Some(value), "imask" => self.imask = Some(parse_int::(&value).unwrap()), - "parent" => self.uuid = Some(Uuid::parse_str(&value).unwrap()), + "parent" => self.parent = Some(Uuid::parse_str(&value).unwrap()), "project" => self.project = Some(value), - "priority" => self.priority = Some(parse_priority(&value).unwrap()), + "priority" => self.priority = Some(Priority::try_from(value.as_ref()).unwrap()), "depends" => self.depends = parse_depends(&value).unwrap(), "tags" => self.tags = parse_tags(&value), _ => { @@ -135,7 +111,6 @@ impl TaskBuilder { pub fn finish(self) -> Task { Task { status: self.status.unwrap(), - uuid: self.uuid.unwrap(), description: self.description.unwrap(), entry: self.entry.unwrap(), start: self.start,