diff options
-rw-r--r-- | lib/domain/libimagtodo/Cargo.toml | 32 | ||||
-rw-r--r-- | lib/domain/libimagtodo/src/entry.rs | 138 | ||||
-rw-r--r-- | lib/domain/libimagtodo/src/iter.rs | 133 | ||||
-rw-r--r-- | lib/domain/libimagtodo/src/lib.rs | 44 | ||||
-rw-r--r-- | lib/domain/libimagtodo/src/priority.rs | 68 | ||||
-rw-r--r-- | lib/domain/libimagtodo/src/status.rs | 72 | ||||
-rw-r--r-- | lib/domain/libimagtodo/src/store.rs | 152 |
7 files changed, 632 insertions, 7 deletions
diff --git a/lib/domain/libimagtodo/Cargo.toml b/lib/domain/libimagtodo/Cargo.toml index 8b2b9f39..ace432dc 100644 --- a/lib/domain/libimagtodo/Cargo.toml +++ b/lib/domain/libimagtodo/Cargo.toml @@ -20,13 +20,31 @@ is-it-maintained-open-issues = { repository = "matthiasbeyer/imag" } maintenance = { status = "actively-developed" } [dependencies] -task-hookrs = "0.6.0" -uuid = "0.7.4" -toml = "0.5.1" -toml-query = "0.9.2" -log = "0.4.6" -serde_json = "1.0.39" -failure = "0.1.5" +failure = "0.1" +filters = "0.3" +log = "0.4" +serde = "1" +serde_derive = "1" +serde_json = "1" +toml = "0.5" libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" } libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" } +libimagentryutil = { version = "0.10.0", path = "../../../lib/entry/libimagentryutil" } +libimagutil = { version = "0.10.0", path = "../../../lib/etc/libimagutil" } + +[dependencies.toml-query] +version = "0.9" +default-features = false +features = ["typed"] + +[dependencies.chrono] +version = "0.4" +default-features = false +features = ["serde"] + +[dependencies.uuid] +version = "0.7" +default-features = false +features = ["serde", "v4"] + diff --git a/lib/domain/libimagtodo/src/entry.rs b/lib/domain/libimagtodo/src/entry.rs new file mode 100644 index 00000000..35fd9b3a --- /dev/null +++ b/lib/domain/libimagtodo/src/entry.rs @@ -0,0 +1,138 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use libimagentryutil::isa::Is; +use libimagentryutil::isa::IsKindHeaderPathProvider; +use libimagstore::store::Entry; +use libimagutil::date::datetime_from_string; + +use failure::Fallible as Result; +use failure::Error; +use failure::ResultExt; +use chrono::NaiveDateTime; +use toml_query::read::Partial; +use toml_query::read::TomlValueReadExt; +use toml_query::insert::TomlValueInsertExt; +use uuid::Uuid; + +use crate::status::Status; +use crate::priority::Priority; + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct TodoHeader { + pub(crate) uuid: Uuid, + pub(crate) status: Status, + pub(crate) scheduled: Option<String>, + pub(crate) hidden: Option<String>, + pub(crate) due: Option<String>, + pub(crate) priority: Option<Priority>, +} + +impl<'a> Partial<'a> for TodoHeader { + const LOCATION: &'static str = "todo"; + type Output = Self; +} + +pub trait Todo { + fn is_todo(&self) -> Result<bool>; + fn get_uuid(&self) -> Result<Uuid>; + fn get_status(&self) -> Result<Status>; + fn set_status(&mut self, status: Status) -> Result<()>; + fn get_scheduled(&self) -> Result<Option<NaiveDateTime>>; + fn set_scheduled(&mut self, scheduled: NaiveDateTime) -> Result<()>; + fn get_hidden(&self) -> Result<Option<NaiveDateTime>>; + fn set_hidden(&mut self, hidden: NaiveDateTime) -> Result<()>; + fn get_due(&self) -> Result<Option<NaiveDateTime>>; + fn set_due(&mut self, due: NaiveDateTime) -> Result<()>; + fn get_priority(&self) -> Result<Option<Priority>>; + fn set_priority(&mut self, prio: Priority) -> Result<()>; +} + +provide_kindflag_path!(pub IsTodo, "todo.is_todo"); + +impl Todo for Entry { + fn is_todo(&self) -> Result<bool> { + self.is::<IsTodo>().context("Cannot check whether Entry is a todo").map_err(From::from) + } + + fn get_uuid(&self) -> Result<Uuid> { + get_header(self).map(|hdr| hdr.uuid) + } + + fn get_status(&self) -> Result<Status> { + get_header(self).map(|hdr| hdr.status) + } + + fn set_status(&mut self, status: Status) -> Result<()> { + self.get_header_mut().insert_serialized("todo.status", status)?; + Ok(()) + } + + fn get_scheduled(&self) -> Result<Option<NaiveDateTime>> { + get_optional_ndt(self, |hdr| hdr.scheduled) + } + + fn set_scheduled(&mut self, scheduled: NaiveDateTime) -> Result<()> { + self.get_header_mut().insert_serialized("todo.scheduled", scheduled)?; + Ok(()) + } + + fn get_hidden(&self) -> Result<Option<NaiveDateTime>> { + get_optional_ndt(self, |hdr| hdr.hidden) + } + + fn set_hidden(&mut self, hidden: NaiveDateTime) -> Result<()> { + self.get_header_mut().insert_serialized("todo.hidden", hidden)?; + Ok(()) + } + + fn get_due(&self) -> Result<Option<NaiveDateTime>> { + get_optional_ndt(self, |hdr| hdr.due) + } + + fn set_due(&mut self, due: NaiveDateTime) -> Result<()> { + self.get_header_mut().insert_serialized("todo.due", due)?; + Ok(()) + } + + fn get_priority(&self) -> Result<Option<Priority>> { + get_header(self).map(|hdr| hdr.priority) + } + + fn set_priority(&mut self, priority: Priority) -> Result<()> { + self.get_header_mut().insert_serialized("todo.priority", priority)?; + Ok(()) + } + +} + +fn get_header(entry: &Entry) -> Result<TodoHeader> { + entry.get_header() + .read_partial::<TodoHeader>()? + .ok_or_else(|| { + format_err!("{} does not contain a TODO header", entry.get_location()) + }) +} + +fn get_optional_ndt<F>(entry: &Entry, extractor: F) + -> Result<Option<NaiveDateTime>> + where F: FnOnce(TodoHeader) -> Option<String> +{ + get_header(entry).map(extractor)?.map(datetime_from_string).transpose().map_err(Error::from) +} diff --git a/lib/domain/libimagtodo/src/iter.rs b/lib/domain/libimagtodo/src/iter.rs new file mode 100644 index 00000000..61379964 --- /dev/null +++ b/lib/domain/libimagtodo/src/iter.rs @@ -0,0 +1,133 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::result::Result as RResult; + +use failure::Fallible as Result; +use failure::Error; +use filters::failable::filter::FailableFilter; + +use libimagstore::store::FileLockEntry; +use libimagstore::store::Entry; + +use crate::entry::Todo; +use crate::status::Status; +use crate::priority::Priority; + +/// Iterator adaptor which filters an Iterator<Item = FileLockEntry> so that only todos are left +pub struct OnlyTodos<'a>(Box<dyn Iterator<Item = FileLockEntry<'a>>>); + +impl<'a> OnlyTodos<'a> { + pub fn new(it: Box<dyn Iterator<Item = FileLockEntry<'a>>>) -> Self { + OnlyTodos(it) + } +} + +impl<'a> Iterator for OnlyTodos<'a> { + type Item = Result<FileLockEntry<'a>>; + + fn next(&mut self) -> Option<Self::Item> { + while let Some(next) = self.0.next() { + match next.is_todo() { + Ok(true) => return Some(Ok(next)), + Ok(false) => continue, + Err(e) => return Some(Err(e)), + } + } + + None + } +} + +/// Helper filter type +/// +/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos by status +/// +pub struct StatusFilter(Status); + +impl FailableFilter<Entry> for StatusFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> { + Ok(entry.get_status()? == self.0) + } +} + +/// Helper filter type +/// +/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos for scheduled todos +/// +pub struct IsScheduledFilter; + +impl FailableFilter<Entry> for IsScheduledFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> { + entry.get_scheduled().map(|s| s.is_some()) + } +} + +/// Helper filter type +/// +/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos for hidden todos +/// +pub struct IsHiddenFilter; + +impl FailableFilter<Entry> for IsHiddenFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> { + entry.get_hidden().map(|s| s.is_some()) + } +} + + +/// Helper filter type +/// +/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos for due todos +/// +pub struct IsDueFilter; + +impl FailableFilter<Entry> for IsDueFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> { + entry.get_due().map(|s| s.is_some()) + } +} + + +/// Helper filter type +/// +/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos for priority +/// +/// # Warning +/// +/// If no priority is set for the entry, this filters out the entry +/// +pub struct PriorityFilter(Priority); + +impl FailableFilter<Entry> for PriorityFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> { + Ok(entry.get_priority()?.map(|p| p == self.0).unwrap_or(false)) + } +} + diff --git a/lib/domain/libimagtodo/src/lib.rs b/lib/domain/libimagtodo/src/lib.rs new file mode 100644 index 00000000..8901077d --- /dev/null +++ b/lib/domain/libimagtodo/src/lib.rs @@ -0,0 +1,44 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + + +extern crate chrono; +extern crate serde; +extern crate toml; +extern crate toml_query; +extern crate uuid; +extern crate filters; + +#[macro_use] extern crate serde_derive; +#[macro_use] extern crate failure; +#[macro_use] extern crate log; + +extern crate libimagerror; +extern crate libimagutil; +#[macro_use] extern crate libimagstore; +#[macro_use] extern crate libimagentryutil; + +pub mod entry; +pub mod iter; +pub mod priority; +pub mod status; +pub mod store; + +module_entry_path_mod!("todo"); + diff --git a/lib/domain/libimagtodo/src/priority.rs b/lib/domain/libimagtodo/src/priority.rs new file mode 100644 index 00000000..e612093d --- /dev/null +++ b/lib/domain/libimagtodo/src/priority.rs @@ -0,0 +1,68 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::cmp::PartialOrd; +use std::cmp::Ordering; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum Priority { + #[serde(rename = "h")] + High, + + #[serde(rename = "m")] + Medium, + + #[serde(rename = "l")] + Low, +} + +impl Priority { + pub fn as_str(&self) -> &str { + match self { + Priority::High => "h", + Priority::Medium => "m", + Priority::Low => "l", + } + } +} + +impl PartialOrd for Priority { + fn partial_cmp(&self, other: &Priority) -> Option<Ordering> { + Some(match (self, other) { + (Priority::Low, Priority::Low) => Ordering::Equal, + (Priority::Low, Priority::Medium) => Ordering::Less, + (Priority::Low, Priority::High) => Ordering::Less, + + (Priority::Medium, Priority::Low) => Ordering::Greater, + (Priority::Medium, Priority::Medium) => Ordering::Equal, + (Priority::Medium, Priority::High) => Ordering::Less, + + (Priority::High, Priority::Low) => Ordering::Greater, + (Priority::High, Priority::Medium) => Ordering::Greater, + (Priority::High, Priority::High) => Ordering::Equal, + }) + } +} + +impl Ord for Priority { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(&other).unwrap() // save by impl above + } +} + diff --git a/lib/domain/libimagtodo/src/status.rs b/lib/domain/libimagtodo/src/status.rs new file mode 100644 index 00000000..63d8fa03 --- /dev/null +++ b/lib/domain/libimagtodo/src/status.rs @@ -0,0 +1,72 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use failure::Fallible as Result; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum Status { + #[serde(rename = "pending")] + Pending, + + #[serde(rename = "done")] + Done, + + #[serde(rename = "deleted")] + Deleted, +} + +impl Status { + pub fn as_str(&self) -> &str { + match self { + Status::Pending => "pending", + Status::Done => "done", + Status::Deleted => "deleted", + } + } + + pub fn from_str(s: &str) -> Result<Self> { + match s { + "pending" => Ok(Status::Pending), + "done" => Ok(Status::Done), + "deleted" => Ok(Status::Deleted), + other => Err(format_err!("{} is not a valid status", other)), + } + } +} + +#[test] +fn test_serializing() { + assert_eq!(Status::Pending.as_str(), "pending"); + assert_eq!(Status::Done.as_str(), "done"); + assert_eq!(Status::Deleted.as_str(), "deleted"); +} + +#[test] +fn test_deserializing() { + assert_eq!(Status::from_str("pending").unwrap(), Status::Pending); + assert_eq!(Status::from_str("done").unwrap(), Status::Done); + assert_eq!(Status::from_str("deleted").unwrap(), Status::Deleted); +} + +#[test] +fn test_serializing_deserializing() { + assert_eq!(Status::Pending.as_str(), Status::from_str("pending").unwrap().as_str()); + assert_eq!(Status::Done.as_str(), Status::from_str("done").unwrap().as_str()); + assert_eq!(Status::Deleted.as_str(), Status::from_str("deleted").unwrap().as_str()); +} diff --git a/lib/domain/libimagtodo/src/store.rs b/lib/domain/libimagtodo/src/store.rs new file mode 100644 index 00000000..cc1f2aa0 --- /dev/null +++ b/lib/domain/libimagtodo/src/store.rs @@ -0,0 +1,152 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::result::Result as RResult; + +use failure::Fallible as Result; +use chrono::NaiveDateTime; +use uuid::Uuid; +use toml_query::insert::TomlValueInsertExt; + +use libimagstore::store::FileLockEntry; +use libimagstore::store::Store; +use libimagstore::iter::Entries; +use libimagutil::date::datetime_to_string; +use libimagentryutil::isa::Is; + +use crate::status::Status; +use crate::priority::Priority; +use crate::entry::TodoHeader; +use crate::entry::IsTodo; + +pub trait TodoStore<'a> { + fn create_todo(&'a self, + status: Status, + scheduled: Option<NaiveDateTime>, + hidden: Option<NaiveDateTime>, + due: Option<NaiveDateTime>, + prio: Option<Priority>, + check_sanity: bool) -> Result<FileLockEntry<'a>>; + + fn get_todo_by_uuid(&'a self, uuid: &Uuid) -> Result<Option<FileLockEntry<'a>>>; + + fn get_todos(&self) -> Result<Entries>; +} + +impl<'a> TodoStore<'a> for Store { + + /// Create a new todo entry + /// + /// # Warning + /// + /// If check_sanity is set to false, this does not sanity-check the scheduled/hidden/due dates. + /// This might result in unintended behaviour (hidden after due date, scheduled before hidden + /// date... etc) + /// + /// An user of this function might want to use `date_sanity_check()` to perform sanity checks + /// before calling TodoStore::create_todo() and show the Err(String) as a warning to user in an + /// interactive way. + fn create_todo(&'a self, + status: Status, + scheduled: Option<NaiveDateTime>, + hidden: Option<NaiveDateTime>, + due: Option<NaiveDateTime>, + prio: Option<Priority>, + check_sanity: bool) -> Result<FileLockEntry<'a>> + { + if check_sanity { + trace!("Checking sanity before creating todo"); + if let Err(s) = date_sanity_check(scheduled.as_ref(), hidden.as_ref(), due.as_ref()) { + trace!("Not sane."); + return Err(format_err!("{}", s)) + } + } + + let uuid = Uuid::new_v4(); + let uuid_s = format!("{}", uuid.to_hyphenated_ref()); // TODO: not how it is supposed to be + debug!("Created new UUID for todo = {}", uuid_s); + + let mut entry = crate::module_path::new_id(uuid_s).and_then(|id| self.create(id))?; + + let header = TodoHeader { + uuid, + status, + scheduled: scheduled.as_ref().map(datetime_to_string), + hidden: hidden.as_ref().map(datetime_to_string), + due: due.as_ref().map(datetime_to_string), + priority: prio + }; + + debug!("Created header for todo: {:?}", header); + + let _ = entry.get_header_mut().insert_serialized("todo", header)?; + let _ = entry.set_isflag::<IsTodo>()?; + + Ok(entry) + } + + fn get_todo_by_uuid(&'a self, uuid: &Uuid) -> Result<Option<FileLockEntry<'a>>> { + let uuid_s = format!("{}", uuid.to_hyphenated_ref()); // TODO: not how it is supposed to be + debug!("Created new UUID for todo = {}", uuid_s); + let id = crate::module_path::new_id(uuid_s)?; + self.get(id) + } + + /// Get all todos using Store::entries() + fn get_todos(&self) -> Result<Entries> { + self.entries().and_then(|es| es.in_collection("todo")) + } +} + +/// Perform a sanity check on the scheduled/hidden/due dates +/// +/// This function returns a String as error, which can be shown as a warning to the user or as an +/// error. +pub fn date_sanity_check(scheduled: Option<&NaiveDateTime>, + hidden: Option<&NaiveDateTime>, + due: Option<&NaiveDateTime>) + -> RResult<(), String> +{ + if let (Some(sched), Some(hid)) = (scheduled.as_ref(), hidden.as_ref()) { + if sched > hid { + return Err(format!("Scheduled date after hidden date: {s}, {h}", + s = sched, + h = hid)) + } + } + + if let (Some(hid), Some(due)) = (hidden.as_ref(), due.as_ref()) { + if hid > due { + return Err(format!("Hidden date after due date: {h}, {d}", + h = hid, + d = due)) + } + } + + if let (Some(sched), Some(due)) = (scheduled.as_ref(), due.as_ref()) { + if sched > due { + return Err(format!("Scheduled date after due date: {s}, {d}", + s = sched, + d = due)) + } + } + + Ok(()) +} + |