mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
Reorganize handling of task data
This abandons field-by-field compatibility with the TaskWarrior TDB2 format, which wasn't a sustainable strategy anyway. Instead, tasks are represented as a TaskMap with custom key formats. In this commit, there are only a few allowed keys, with room to grow. Replica returns convenience wrappers Task (read-only) and TaskMut (read-write) with getters and setters to make modifying tasks easier.
This commit is contained in:
parent
c2c2a00ed5
commit
634aaadb73
10 changed files with 276 additions and 490 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -131,12 +131,6 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "failure"
|
name = "failure"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -182,15 +176,6 @@ dependencies = [
|
||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
|
@ -622,7 +607,6 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"failure",
|
"failure",
|
||||||
"itertools",
|
|
||||||
"kv",
|
"kv",
|
||||||
"lmdb-rkv",
|
"lmdb-rkv",
|
||||||
"proptest",
|
"proptest",
|
||||||
|
|
|
@ -13,7 +13,6 @@ failure = {version = "0.1.5", features = ["derive"] }
|
||||||
clap = "~2.33.0"
|
clap = "~2.33.0"
|
||||||
kv = {version = "0.10.0", features = ["msgpack-value"]}
|
kv = {version = "0.10.0", features = ["msgpack-value"]}
|
||||||
lmdb-rkv = {version = "0.12.3"}
|
lmdb-rkv = {version = "0.12.3"}
|
||||||
itertools = "0.9.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "0.9.4"
|
proptest = "0.9.4"
|
||||||
|
|
|
@ -6,5 +6,6 @@
|
||||||
- [Data Model](./data-model.md)
|
- [Data Model](./data-model.md)
|
||||||
- [Replica Storage](./storage.md)
|
- [Replica Storage](./storage.md)
|
||||||
- [Task Database](./taskdb.md)
|
- [Task Database](./taskdb.md)
|
||||||
|
- [Tasks](./tasks.md)
|
||||||
- [Synchronization](./sync.md)
|
- [Synchronization](./sync.md)
|
||||||
- [Planned Functionality](./plans.md)
|
- [Planned Functionality](./plans.md)
|
||||||
|
|
|
@ -7,47 +7,15 @@ The storage is transaction-protected, with the expectation of a serializable iso
|
||||||
The storage contains the following information:
|
The storage contains the following information:
|
||||||
|
|
||||||
- `tasks`: a set of tasks, indexed by UUID
|
- `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
|
- `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.
|
- `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
|
## Tasks
|
||||||
|
|
||||||
The tasks are stored as an un-ordered collection, keyed by task UUID.
|
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.
|
Each task in the database has represented by a key-value map.
|
||||||
|
See [Tasks](./tasks.md) for details on the content of that map.
|
||||||
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_<timestamp>` - an annotation for this task, with the timestamp as part of the key
|
|
||||||
* `udas` - user-defined attributes
|
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
|
|
38
docs/src/tasks.md
Normal file
38
docs/src/tasks.md
Normal file
|
@ -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.<uuid>` - indicates this task depends on `<uuid>` (value is an empty string)
|
||||||
|
* `tag.<tag>` - indicates this task has tag `<tag>` (value is an empty string)
|
||||||
|
* `annotation.<timestamp>` - value is an annotation created at the given time
|
|
@ -47,8 +47,8 @@ fn main() {
|
||||||
("pending", _) => {
|
("pending", _) => {
|
||||||
let working_set = replica.working_set().unwrap();
|
let working_set = replica.working_set().unwrap();
|
||||||
for i in 1..working_set.len() {
|
for i in 1..working_set.len() {
|
||||||
if let Some((ref uuid, ref task)) = working_set[i] {
|
if let Some(ref task) = working_set[i] {
|
||||||
println!("{}: {} - {:?}", i, uuid, task);
|
println!("{}: {} - {:?}", i, task.get_uuid(), task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
299
src/replica.rs
299
src/replica.rs
|
@ -1,11 +1,10 @@
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::operation::Operation;
|
use crate::operation::Operation;
|
||||||
use crate::task::{Priority, Status, Task, TaskBuilder};
|
use crate::task::{Status, Task};
|
||||||
use crate::taskdb::DB;
|
use crate::taskdb::DB;
|
||||||
use crate::taskstorage::TaskMap;
|
use crate::taskstorage::TaskMap;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::Utc;
|
||||||
use failure::Fallible;
|
use failure::Fallible;
|
||||||
use itertools::join;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
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
|
/// 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
|
/// value is None, the property is deleted. It is not an error to delete a nonexistent
|
||||||
/// property.
|
/// property.
|
||||||
fn update_task<S1, S2>(&mut self, uuid: Uuid, property: S1, value: Option<S2>) -> Fallible<()>
|
pub(crate) fn update_task<S1, S2>(
|
||||||
|
&mut self,
|
||||||
|
uuid: Uuid,
|
||||||
|
property: S1,
|
||||||
|
value: Option<S2>,
|
||||||
|
) -> Fallible<()>
|
||||||
where
|
where
|
||||||
S1: Into<String>,
|
S1: Into<String>,
|
||||||
S2: Into<String>,
|
S2: Into<String>,
|
||||||
|
@ -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.
|
/// Add the given uuid to the working set, returning its index.
|
||||||
fn add_to_working_set(&mut self, uuid: &Uuid) -> Fallible<u64> {
|
pub(crate) fn add_to_working_set(&mut self, uuid: &Uuid) -> Fallible<u64> {
|
||||||
self.taskdb.add_to_working_set(uuid)
|
self.taskdb.add_to_working_set(uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all tasks represented as a map keyed by UUID
|
/// Get all tasks represented as a map keyed by UUID
|
||||||
pub fn all_tasks<'a>(&'a mut self) -> Fallible<HashMap<Uuid, Task>> {
|
pub fn all_tasks<'a>(&'a mut self) -> Fallible<HashMap<Uuid, Task>> {
|
||||||
Ok(self
|
let mut res = HashMap::new();
|
||||||
.taskdb
|
for (uuid, tm) in self.taskdb.all_tasks()?.drain(..) {
|
||||||
.all_tasks()?
|
res.insert(uuid.clone(), Task::new(uuid.clone(), tm));
|
||||||
.iter()
|
}
|
||||||
.map(|(k, v)| (k.clone(), v.into()))
|
Ok(res)
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the UUIDs of all tasks
|
/// 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
|
/// Get the "working set" for this replica -- the set of pending tasks, as indexed by small
|
||||||
/// integers
|
/// integers
|
||||||
pub fn working_set(&mut self) -> Fallible<Vec<Option<(Uuid, Task)>>> {
|
pub fn working_set(&mut self) -> Fallible<Vec<Option<Task>>> {
|
||||||
let working_set = self.taskdb.working_set()?;
|
let working_set = self.taskdb.working_set()?;
|
||||||
let mut res = Vec::with_capacity(working_set.len());
|
let mut res = Vec::with_capacity(working_set.len());
|
||||||
for i in 0..working_set.len() {
|
for i in 0..working_set.len() {
|
||||||
res.push(match working_set[i] {
|
res.push(match working_set[i] {
|
||||||
Some(u) => match self.taskdb.get_task(&u)? {
|
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,
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
|
@ -84,7 +77,10 @@ impl Replica {
|
||||||
|
|
||||||
/// Get an existing task by its UUID
|
/// Get an existing task by its UUID
|
||||||
pub fn get_task(&mut self, uuid: &Uuid) -> Fallible<Option<Task>> {
|
pub fn get_task(&mut self, uuid: &Uuid) -> Fallible<Option<Task>> {
|
||||||
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
|
/// Get an existing task by its working set index
|
||||||
|
@ -92,19 +88,17 @@ impl Replica {
|
||||||
let working_set = self.taskdb.working_set()?;
|
let working_set = self.taskdb.working_set()?;
|
||||||
if (i as usize) < working_set.len() {
|
if (i as usize) < working_set.len() {
|
||||||
if let Some(uuid) = working_set[i as usize] {
|
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);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new task. The task must not already exist.
|
/// Create a new task. The task must not already exist.
|
||||||
pub fn new_task(
|
pub fn new_task(&mut self, uuid: Uuid, status: Status, description: String) -> Fallible<Task> {
|
||||||
&mut self,
|
|
||||||
uuid: Uuid,
|
|
||||||
status: Status,
|
|
||||||
description: String,
|
|
||||||
) -> Fallible<TaskMut> {
|
|
||||||
// check that it doesn't exist; this is a convenience check, as the task
|
// 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
|
// may already exist when this Create operation is finally sync'd with
|
||||||
// operations from other replicas
|
// operations from other replicas
|
||||||
|
@ -113,15 +107,14 @@ impl Replica {
|
||||||
}
|
}
|
||||||
self.taskdb
|
self.taskdb
|
||||||
.apply(Operation::Create { uuid: uuid.clone() })?;
|
.apply(Operation::Create { uuid: uuid.clone() })?;
|
||||||
self.update_task(uuid.clone(), "status", Some(String::from(status.as_ref())))?;
|
let mut task = Task::new(uuid, TaskMap::new()).into_mut(self);
|
||||||
self.update_task(uuid.clone(), "description", Some(description))?;
|
task.set_description(description)?;
|
||||||
let now = format!("{}", Utc::now().timestamp());
|
task.set_status(status)?;
|
||||||
self.update_task(uuid.clone(), "entry", Some(now.clone()))?;
|
Ok(task.into_immut())
|
||||||
self.update_task(uuid.clone(), "modified", Some(now))?;
|
|
||||||
Ok(TaskMut::new(self, uuid))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<()> {
|
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
|
// 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
|
// 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 })
|
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<Option<TaskMut<'a>>> {
|
|
||||||
// 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
|
/// Perform "garbage collection" on this replica. In particular, this renumbers the working
|
||||||
/// set to contain only pending tasks.
|
/// set to contain only pending tasks.
|
||||||
pub fn gc(&mut self) -> Fallible<()> {
|
pub fn gc(&mut self) -> Fallible<()> {
|
||||||
|
let pending = String::from(Status::Pending.to_taskmap());
|
||||||
self.taskdb
|
self.taskdb
|
||||||
.rebuild_working_set(|t| Replica::is_working_set_status(t.get("status")))?;
|
.rebuild_working_set(|t| t.get("status") == Some(&pending))?;
|
||||||
Ok(())
|
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<String>) -> Fallible<()> {
|
|
||||||
self.lastmod()?;
|
|
||||||
self.replica.update_task(self.uuid.clone(), property, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_timestamp(&mut self, property: &str, value: Option<DateTime<Utc>>) -> 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<DateTime<Utc>>) -> Fallible<()> {
|
|
||||||
self.set_timestamp("start", time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's end time
|
|
||||||
pub fn end(&mut self, time: Option<DateTime<Utc>>) -> Fallible<()> {
|
|
||||||
self.set_timestamp("end", time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's due time
|
|
||||||
pub fn due(&mut self, time: Option<DateTime<Utc>>) -> Fallible<()> {
|
|
||||||
self.set_timestamp("due", time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's until time
|
|
||||||
pub fn until(&mut self, time: Option<DateTime<Utc>>) -> Fallible<()> {
|
|
||||||
self.set_timestamp("until", time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's wait time
|
|
||||||
pub fn wait(&mut self, time: Option<DateTime<Utc>>) -> Fallible<()> {
|
|
||||||
self.set_timestamp("wait", time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's scheduled time
|
|
||||||
pub fn scheduled(&mut self, time: Option<DateTime<Utc>>) -> Fallible<()> {
|
|
||||||
self.set_timestamp("scheduled", time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's recur value
|
|
||||||
pub fn recur(&mut self, recur: Option<String>) -> Fallible<()> {
|
|
||||||
self.set_string("recur", recur)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's mask value
|
|
||||||
pub fn mask(&mut self, mask: Option<String>) -> Fallible<()> {
|
|
||||||
self.set_string("mask", mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's imask value
|
|
||||||
pub fn imask(&mut self, imask: Option<u64>) -> Fallible<()> {
|
|
||||||
self.set_string("imask", imask.map(|v| format!("{}", v)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's parent task
|
|
||||||
pub fn parent(&mut self, parent: Option<Uuid>) -> Fallible<()> {
|
|
||||||
self.set_string("parent", parent.map(|v| format!("{}", v)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's project
|
|
||||||
pub fn project(&mut self, project: Option<String>) -> Fallible<()> {
|
|
||||||
self.set_string("project", project)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the task's priority
|
|
||||||
pub fn priority(&mut self, priority: Option<Priority>) -> 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<Uuid>) -> 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<Uuid>) -> Fallible<()> {
|
|
||||||
self.set_string("tags", Some(join(tags.iter(), ",")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: annotations
|
|
||||||
// TODO: udas
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::task::Status;
|
||||||
use crate::taskdb::DB;
|
use crate::taskdb::DB;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_task_and_modify() {
|
fn new_task() {
|
||||||
let mut rep = Replica::new(DB::new_inmemory().into());
|
let mut rep = Replica::new(DB::new_inmemory().into());
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
|
|
||||||
let mut tm = rep
|
let t = rep
|
||||||
.new_task(uuid.clone(), Status::Pending, "a task".into())
|
.new_task(uuid.clone(), Status::Pending, "a task".into())
|
||||||
.unwrap();
|
.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();
|
let t = rep.get_task(&uuid).unwrap().unwrap();
|
||||||
assert_eq!(t.description, String::from("a task"));
|
assert_eq!(t.get_description(), "past tense");
|
||||||
assert_eq!(t.status, Status::Pending);
|
assert_eq!(t.get_status(), Status::Completed);
|
||||||
assert_eq!(t.priority, Some(Priority::L));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -344,34 +202,33 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let t = rep.get_task(&uuid).unwrap().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();
|
let mut t = t.into_mut(&mut rep);
|
||||||
tm.status(Status::Completed).unwrap();
|
t.set_status(Status::Deleted).unwrap();
|
||||||
tm.description("another task, updated".into()).unwrap();
|
t.set_description("gone".into()).unwrap();
|
||||||
tm.priority(Some(Priority::L)).unwrap();
|
|
||||||
tm.project(Some("work".into())).unwrap();
|
|
||||||
|
|
||||||
let t = rep.get_task(&uuid).unwrap().unwrap();
|
let t = rep.get_task(&uuid).unwrap().unwrap();
|
||||||
assert_eq!(t.status, Status::Completed);
|
assert_eq!(t.get_status(), Status::Deleted);
|
||||||
assert_eq!(t.description, String::from("another task, updated"));
|
assert_eq!(t.get_description(), "gone");
|
||||||
assert_eq!(t.project, Some("work".into()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 mut rep = Replica::new(DB::new_inmemory().into());
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
|
|
||||||
rep.new_task(uuid.clone(), Status::Pending, "to-be-pending".into())
|
rep.new_task(uuid.clone(), Status::Pending, "to-be-pending".into())
|
||||||
.unwrap();
|
.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();
|
let t = rep.get_working_set_task(1).unwrap().unwrap();
|
||||||
assert_eq!(t.status, Status::Pending);
|
assert_eq!(t.get_status(), Status::Pending);
|
||||||
assert_eq!(t.description, String::from("to-be-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]
|
#[test]
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
mod task;
|
mod task;
|
||||||
mod taskbuilder;
|
|
||||||
|
|
||||||
pub use self::task::Priority::*;
|
pub use self::task::Priority::*;
|
||||||
pub use self::task::Status::*;
|
pub use self::task::Status::*;
|
||||||
pub use self::task::{Annotation, Priority, Status, Task, Timestamp};
|
pub use self::task::{Annotation, Priority, Status, Task, Timestamp};
|
||||||
pub use self::taskbuilder::TaskBuilder;
|
|
||||||
|
|
198
src/task/task.rs
198
src/task/task.rs
|
@ -1,5 +1,7 @@
|
||||||
|
use crate::replica::Replica;
|
||||||
|
use crate::taskstorage::TaskMap;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use std::collections::HashMap;
|
use failure::Fallible;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -39,33 +41,26 @@ pub enum Status {
|
||||||
Pending,
|
Pending,
|
||||||
Completed,
|
Completed,
|
||||||
Deleted,
|
Deleted,
|
||||||
Recurring,
|
|
||||||
Waiting,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for Status {
|
impl Status {
|
||||||
type Error = failure::Error;
|
/// Get a Status from the 1-character value in a TaskMap,
|
||||||
|
/// defaulting to Pending
|
||||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
pub(crate) fn from_taskmap(s: &str) -> Status {
|
||||||
match s {
|
match s {
|
||||||
"pending" => Ok(Status::Pending),
|
"P" => Status::Pending,
|
||||||
"completed" => Ok(Status::Completed),
|
"C" => Status::Completed,
|
||||||
"deleted" => Ok(Status::Deleted),
|
"D" => Status::Deleted,
|
||||||
"recurring" => Ok(Status::Recurring),
|
_ => Status::Pending,
|
||||||
"waiting" => Ok(Status::Waiting),
|
|
||||||
_ => Err(format_err!("invalid status {}", s)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<str> for Status {
|
/// Get the 1-character value for this status to use in the TaskMap.
|
||||||
fn as_ref(&self) -> &str {
|
pub(crate) fn to_taskmap(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Status::Pending => "pending",
|
Status::Pending => "P",
|
||||||
Status::Completed => "completed",
|
Status::Completed => "C",
|
||||||
Status::Deleted => "deleted",
|
Status::Deleted => "D",
|
||||||
Status::Recurring => "recurring",
|
|
||||||
Status::Waiting => "waiting",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,30 +71,145 @@ pub struct Annotation {
|
||||||
pub description: String,
|
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
|
/// Note that Task objects represent a snapshot of the task at a moment in time, and are not
|
||||||
/// exception that the uuid property is omitted.
|
/// 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)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct Task {
|
pub struct Task {
|
||||||
pub status: Status,
|
uuid: Uuid,
|
||||||
pub entry: Timestamp,
|
taskmap: TaskMap,
|
||||||
pub description: String,
|
}
|
||||||
pub start: Option<Timestamp>,
|
|
||||||
pub end: Option<Timestamp>,
|
/// A mutable task, with setter methods. Calling a setter will update the Replica, as well as the
|
||||||
pub due: Option<Timestamp>,
|
/// included Task.
|
||||||
pub until: Option<Timestamp>,
|
pub struct TaskMut<'r> {
|
||||||
pub wait: Option<Timestamp>,
|
task: Task,
|
||||||
pub modified: Timestamp,
|
replica: &'r mut Replica,
|
||||||
pub scheduled: Option<Timestamp>,
|
updated_modified: bool,
|
||||||
pub recur: Option<String>,
|
}
|
||||||
pub mask: Option<String>,
|
|
||||||
pub imask: Option<u64>,
|
impl Task {
|
||||||
pub parent: Option<Uuid>,
|
pub(crate) fn new(uuid: Uuid, taskmap: TaskMap) -> Task {
|
||||||
pub project: Option<String>,
|
Task { uuid, taskmap }
|
||||||
pub priority: Option<Priority>,
|
}
|
||||||
pub depends: Vec<Uuid>,
|
|
||||||
pub tags: Vec<String>,
|
pub fn get_uuid(&self) -> &Uuid {
|
||||||
pub annotations: Vec<Annotation>,
|
&self.uuid
|
||||||
pub udas: HashMap<String, String>,
|
}
|
||||||
|
|
||||||
|
/// 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<DateTime<Utc>> {
|
||||||
|
self.get_timestamp("modified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- utility functions
|
||||||
|
|
||||||
|
pub fn get_timestamp(&self, property: &str) -> Option<DateTime<Utc>> {
|
||||||
|
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<Utc>) -> 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<String>) -> 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<DateTime<Utc>>) -> 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Status>,
|
|
||||||
entry: Option<Timestamp>,
|
|
||||||
description: Option<String>,
|
|
||||||
start: Option<Timestamp>,
|
|
||||||
end: Option<Timestamp>,
|
|
||||||
due: Option<Timestamp>,
|
|
||||||
until: Option<Timestamp>,
|
|
||||||
wait: Option<Timestamp>,
|
|
||||||
modified: Option<Timestamp>,
|
|
||||||
scheduled: Option<Timestamp>,
|
|
||||||
recur: Option<String>,
|
|
||||||
mask: Option<String>,
|
|
||||||
imask: Option<u64>,
|
|
||||||
parent: Option<Uuid>,
|
|
||||||
project: Option<String>,
|
|
||||||
priority: Option<Priority>,
|
|
||||||
depends: Vec<Uuid>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
annotations: Vec<Annotation>,
|
|
||||||
udas: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse an "integer", allowing for occasional integers with trailing decimal zeroes
|
|
||||||
fn parse_int<T>(value: &str) -> Result<T, <T as str::FromStr>::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<Timestamp, <i64 as str::FromStr>::Err> {
|
|
||||||
Ok(Utc.timestamp(parse_int::<i64>(value)?, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse depends, as a list of ,-separated UUIDs
|
|
||||||
fn parse_depends(value: &str) -> Result<Vec<Uuid>, 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<String> {
|
|
||||||
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::<u64>(&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::<u8>("123").unwrap(), 123u8);
|
|
||||||
assert_eq!(parse_int::<u32>("123000000").unwrap(), 123000000u32);
|
|
||||||
assert_eq!(parse_int::<i32>("-123000000").unwrap(), -123000000i32);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_int_decimals() {
|
|
||||||
assert_eq!(parse_int::<u8>("123.00").unwrap(), 123u8);
|
|
||||||
assert_eq!(parse_int::<u32>("123.0000").unwrap(), 123u32);
|
|
||||||
assert_eq!(parse_int::<i32>("-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()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue