Define UDAs

This commit is contained in:
Dustin J. Mitchell 2021-12-18 23:39:56 +00:00
parent acd4aefc17
commit ef12e1a2f8
2 changed files with 186 additions and 13 deletions

View file

@ -38,3 +38,14 @@ The following keys, and key formats, are defined:
The following are not yet implemented: The following are not yet implemented:
* `dep_<uuid>` - indicates this task depends on `<uuid>` (value is an empty string) * `dep_<uuid>` - indicates this task depends on `<uuid>` (value is an empty string)
### UDAs
Any unrecognized keys are treated as "user-defined attributes" (UDAs).
These attributes can be used to store additional data associated with a task.
For example, applications that synchronize tasks with other systems such as calendars or team planning services might store unique identifiers for those systems as UDAs.
The application defining a UDA defines the format of the value.
UDAs _should_ have a namespaced structure of the form `<namespace>.<key>`, where `<namespace>` identifies the application defining the UDA.
For example, a service named "DevSync" synchronizing tasks from GitHub might use UDAs like `devsync.github.issue-id`.
Note that many existing UDAs for Taskwarrior integrations do not follow this pattern.

View file

@ -6,6 +6,7 @@ use chrono::prelude::*;
use log::trace; use log::trace;
use std::convert::AsRef; use std::convert::AsRef;
use std::convert::TryInto; use std::convert::TryInto;
use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
/* The Task and TaskMut classes wrap the underlying [`TaskMap`], which is a simple key/value map. /* The Task and TaskMut classes wrap the underlying [`TaskMap`], which is a simple key/value map.
@ -46,6 +47,18 @@ pub struct TaskMut<'r> {
updated_modified: bool, updated_modified: bool,
} }
/// An enum containing all of the key names defined in the data model, with the exception
/// of the properties containing data (`tag_..`, etc.)
#[derive(strum_macros::AsRefStr, strum_macros::EnumString)]
#[strum(serialize_all = "kebab-case")]
enum Prop {
Description,
Modified,
Start,
Status,
Wait,
}
impl Task { impl Task {
pub(crate) fn new(uuid: Uuid, taskmap: TaskMap) -> Task { pub(crate) fn new(uuid: Uuid, taskmap: TaskMap) -> Task {
Task { uuid, taskmap } Task { uuid, taskmap }
@ -71,14 +84,14 @@ impl Task {
pub fn get_status(&self) -> Status { pub fn get_status(&self) -> Status {
self.taskmap self.taskmap
.get("status") .get(Prop::Status.as_ref())
.map(|s| Status::from_taskmap(s)) .map(|s| Status::from_taskmap(s))
.unwrap_or(Status::Pending) .unwrap_or(Status::Pending)
} }
pub fn get_description(&self) -> &str { pub fn get_description(&self) -> &str {
self.taskmap self.taskmap
.get("description") .get(Prop::Description.as_ref())
.map(|s| s.as_ref()) .map(|s| s.as_ref())
.unwrap_or("") .unwrap_or("")
} }
@ -86,7 +99,7 @@ impl Task {
/// Get the wait time. If this value is set, it will be returned, even /// Get the wait time. If this value is set, it will be returned, even
/// if it is in the past. /// if it is in the past.
pub fn get_wait(&self) -> Option<DateTime<Utc>> { pub fn get_wait(&self) -> Option<DateTime<Utc>> {
self.get_timestamp("wait") self.get_timestamp(Prop::Wait.as_ref())
} }
/// Determine whether this task is waiting now. /// Determine whether this task is waiting now.
@ -100,7 +113,7 @@ impl Task {
/// Determine whether this task is active -- that is, that it has been started /// Determine whether this task is active -- that is, that it has been started
/// and not stopped. /// and not stopped.
pub fn is_active(&self) -> bool { pub fn is_active(&self) -> bool {
self.taskmap.contains_key("start") self.taskmap.contains_key(Prop::Start.as_ref())
} }
/// Determine whether a given synthetic tag is present on this task. All other /// Determine whether a given synthetic tag is present on this task. All other
@ -161,12 +174,37 @@ impl Task {
}) })
} }
/// Get the named user defined attributes (UDA). This will return None
/// for any key defined in the Task data model, regardless of whether
/// it is set or not.
pub fn get_uda(&self, key: &str) -> Option<&str> {
if Task::is_known_key(key) {
return None;
}
self.taskmap.get(key).map(|s| s.as_ref())
}
/// Get the user defined attributes (UDAs) of this task, in arbitrary order.
pub fn get_udas(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
self.taskmap
.iter()
.filter(|(p, _)| !Task::is_known_key(p))
.map(|(p, v)| (p.as_ref(), v.as_ref()))
}
pub fn get_modified(&self) -> Option<DateTime<Utc>> { pub fn get_modified(&self) -> Option<DateTime<Utc>> {
self.get_timestamp("modified") self.get_timestamp(Prop::Modified.as_ref())
} }
// -- utility functions // -- utility functions
fn is_known_key(key: &str) -> bool {
Prop::from_str(key).is_ok()
|| key.starts_with("tag_")
|| key.starts_with("annotation_")
|| key.starts_with("dep_")
}
fn get_timestamp(&self, property: &str) -> Option<DateTime<Utc>> { fn get_timestamp(&self, property: &str) -> Option<DateTime<Utc>> {
if let Some(ts) = self.taskmap.get(property) { if let Some(ts) = self.taskmap.get(property) {
if let Ok(ts) = ts.parse() { if let Ok(ts) = ts.parse() {
@ -191,19 +229,22 @@ impl<'r> TaskMut<'r> {
let uuid = self.uuid; let uuid = self.uuid;
self.replica.add_to_working_set(uuid)?; self.replica.add_to_working_set(uuid)?;
} }
self.set_string("status", Some(String::from(status.to_taskmap()))) self.set_string(
Prop::Status.as_ref(),
Some(String::from(status.to_taskmap())),
)
} }
pub fn set_description(&mut self, description: String) -> anyhow::Result<()> { pub fn set_description(&mut self, description: String) -> anyhow::Result<()> {
self.set_string("description", Some(description)) self.set_string(Prop::Description.as_ref(), Some(description))
} }
pub fn set_wait(&mut self, wait: Option<DateTime<Utc>>) -> anyhow::Result<()> { pub fn set_wait(&mut self, wait: Option<DateTime<Utc>>) -> anyhow::Result<()> {
self.set_timestamp("wait", wait) self.set_timestamp(Prop::Wait.as_ref(), wait)
} }
pub fn set_modified(&mut self, modified: DateTime<Utc>) -> anyhow::Result<()> { pub fn set_modified(&mut self, modified: DateTime<Utc>) -> anyhow::Result<()> {
self.set_timestamp("modified", Some(modified)) self.set_timestamp(Prop::Modified.as_ref(), Some(modified))
} }
/// Start the task by creating "start": "<timestamp>", if the task is not already /// Start the task by creating "start": "<timestamp>", if the task is not already
@ -212,12 +253,12 @@ impl<'r> TaskMut<'r> {
if self.is_active() { if self.is_active() {
return Ok(()); return Ok(());
} }
self.set_timestamp("start", Some(Utc::now())) self.set_timestamp(Prop::Start.as_ref(), Some(Utc::now()))
} }
/// Stop the task by removing the `start` key /// Stop the task by removing the `start` key
pub fn stop(&mut self) -> anyhow::Result<()> { pub fn stop(&mut self) -> anyhow::Result<()> {
self.set_timestamp("start", None) self.set_timestamp(Prop::Start.as_ref(), None)
} }
/// Mark this task as complete /// Mark this task as complete
@ -255,15 +296,50 @@ impl<'r> TaskMut<'r> {
self.set_string(format!("annotation_{}", entry.timestamp()), None) self.set_string(format!("annotation_{}", entry.timestamp()), None)
} }
/// Set a user-defined attribute (UDA). This will fail if the key is defined by the data
/// model.
pub fn set_uda<S1, S2>(&mut self, key: S1, value: S2) -> anyhow::Result<()>
where
S1: Into<String>,
S2: Into<String>,
{
let key = key.into();
if Task::is_known_key(&key) {
anyhow::bail!(
"Property name {} as special meaning in a task and cannot be used as a UDA",
key
);
}
self.set_string(key, Some(value.into()))
}
/// Remove a user-defined attribute (UDA). This will fail if the key is defined by the data
/// model.
pub fn remove_uda<S>(&mut self, key: S) -> anyhow::Result<()>
where
S: Into<String>,
{
let key = key.into();
if Task::is_known_key(&key) {
anyhow::bail!(
"Property name {} as special meaning in a task and cannot be used as a UDA",
key
);
}
self.set_string(key, None)
}
// -- utility functions // -- utility functions
fn lastmod(&mut self) -> anyhow::Result<()> { fn lastmod(&mut self) -> anyhow::Result<()> {
if !self.updated_modified { if !self.updated_modified {
let now = format!("{}", Utc::now().timestamp()); let now = format!("{}", Utc::now().timestamp());
self.replica self.replica
.update_task(self.task.uuid, "modified", Some(now.clone()))?; .update_task(self.task.uuid, Prop::Modified.as_ref(), Some(now.clone()))?;
trace!("task {}: set property modified={:?}", self.task.uuid, now); trace!("task {}: set property modified={:?}", self.task.uuid, now);
self.task.taskmap.insert(String::from("modified"), now); self.task
.taskmap
.insert(String::from(Prop::Modified.as_ref()), now);
self.updated_modified = true; self.updated_modified = true;
} }
Ok(()) Ok(())
@ -634,4 +710,90 @@ mod test {
assert!(!task.taskmap.contains_key("tag_abc")); assert!(!task.taskmap.contains_key("tag_abc"));
}); });
} }
#[test]
fn test_get_udas() {
let task = Task::new(
Uuid::new_v4(),
vec![
("description".into(), "not a uda".into()),
("modified".into(), "not a uda".into()),
("start".into(), "not a uda".into()),
("status".into(), "not a uda".into()),
("wait".into(), "not a uda".into()),
("start".into(), "not a uda".into()),
("tag_abc".into(), "not a uda".into()),
("dep_1234".into(), "not a uda".into()),
("annotation_1234".into(), "not a uda".into()),
("githubid".into(), "123".into()),
]
.drain(..)
.collect(),
);
let udas: Vec<_> = task.get_udas().collect();
assert_eq!(udas, vec![("githubid", "123")]);
}
#[test]
fn test_get_uda() {
let task = Task::new(
Uuid::new_v4(),
vec![
("description".into(), "not a uda".into()),
("dep_1234".into(), "not a uda".into()),
("githubid".into(), "123".into()),
]
.drain(..)
.collect(),
);
assert_eq!(task.get_uda("description"), None); // invalid UDA
assert_eq!(task.get_uda("dep_1234"), None); // invalid UDA
assert_eq!(task.get_uda("githubid"), Some("123"));
assert_eq!(task.get_uda("jiraid"), None);
}
#[test]
fn test_set_uda() {
with_mut_task(|mut task| {
task.set_uda("githubid", "123").unwrap();
let udas: Vec<_> = task.get_udas().collect();
assert_eq!(udas, vec![("githubid", "123")]);
task.set_uda("jiraid", "TW-1234").unwrap();
let mut udas: Vec<_> = task.get_udas().collect();
udas.sort_unstable();
assert_eq!(udas, vec![("githubid", "123"), ("jiraid", "TW-1234")]);
})
}
#[test]
fn test_set_uda_invalid() {
with_mut_task(|mut task| {
assert!(task.set_uda("modified", "123").is_err());
assert!(task.set_uda("tag_abc", "123").is_err());
})
}
#[test]
fn test_rmmove_uda() {
with_mut_task(|mut task| {
task.set_string("githubid", Some("123".into())).unwrap();
task.remove_uda("githubid").unwrap();
let udas: Vec<_> = task.get_udas().collect();
assert_eq!(udas, vec![]);
})
}
#[test]
fn test_remove_uda_invalid() {
with_mut_task(|mut task| {
assert!(task.remove_uda("modified").is_err());
assert!(task.remove_uda("tag_abc").is_err());
})
}
} }