Reimplement libimagtodo

Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
This commit is contained in:
Matthias Beyer 2019-06-29 22:53:06 +02:00
parent 21cc901d06
commit 775d3f0a80
7 changed files with 632 additions and 7 deletions

View file

@ -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"]

View file

@ -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)
}

View file

@ -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))
}
}

View file

@ -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");

View file

@ -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
}
}

View file

@ -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());
}

View file

@ -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(())
}