mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Merge pull request #266 from taskchampion/issue80
Add support for synthetic tags
This commit is contained in:
commit
ebdae1f44a
10 changed files with 438 additions and 213 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
@ -851,8 +851,8 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"strum 0.18.0",
|
||||
"strum_macros 0.18.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2894,6 +2894,12 @@ version = "0.18.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.18.0"
|
||||
|
@ -2906,6 +2912,18 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.72"
|
||||
|
@ -2933,8 +2951,11 @@ dependencies = [
|
|||
"lmdb-rkv 0.14.0",
|
||||
"log",
|
||||
"proptest",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.21.0",
|
||||
"strum_macros 0.21.1",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tindercrypt",
|
||||
|
|
|
@ -403,8 +403,11 @@ mod test {
|
|||
};
|
||||
|
||||
let task = replica.get_task(uuids[0]).unwrap().unwrap();
|
||||
assert_eq!(task_column(&task, &column, &working_set), s!("+bar +foo"));
|
||||
assert_eq!(
|
||||
task_column(&task, &column, &working_set),
|
||||
s!("+PENDING +bar +foo")
|
||||
);
|
||||
let task = replica.get_task(uuids[2]).unwrap().unwrap();
|
||||
assert_eq!(task_column(&task, &column, &working_set), s!(""));
|
||||
assert_eq!(task_column(&task, &column, &working_set), s!("+PENDING"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,3 +10,17 @@ For example, when it's time to continue the job search, `ta +jobsearch` will sho
|
|||
|
||||
Specifically, tags must be at least one character long and cannot contain whitespace or any of the characters `+-*/(<>^! %=~`.
|
||||
The first character cannot be a digit, and `:` is not allowed after the first character.
|
||||
All-capital tags are reserved for synthetic tags (below) and cannot be added or removed from tasks.
|
||||
|
||||
## Synthetic Tags
|
||||
|
||||
Synthetic tags are present on tasks that meet specific criteria, that are commonly used for filtering.
|
||||
For example, `WAITING` is set for tasks that are currently waiting.
|
||||
These tags cannot be added or removed from a task, but appear and disappear as the task changes.
|
||||
The following synthetic tags are defined:
|
||||
|
||||
* `WAITING` - set if the task is waiting (has a `wait` property with a date in the future)
|
||||
* `ACTIVE` - set if the task is active (has been started and not stopped)
|
||||
* `PENDING` - set if the task is pending (not completed or deleted)
|
||||
* `COMPLETED` - set if the task has been completed
|
||||
* `DELETED` - set if the task has been deleted (but not yet flushed from the task list)
|
||||
|
|
|
@ -22,7 +22,10 @@ lmdb-rkv = {version = "^0.14.0"}
|
|||
ureq = "^2.1.0"
|
||||
log = "^0.4.14"
|
||||
tindercrypt = { version = "^0.2.2", default-features = false }
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "^1.0.0"
|
||||
tempfile = "3"
|
||||
rstest = "0.10"
|
||||
|
|
10
taskchampion/src/task/annotation.rs
Normal file
10
taskchampion/src/task/annotation.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use super::Timestamp;
|
||||
|
||||
/// An annotation for a task
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Annotation {
|
||||
/// Time the annotation was made
|
||||
pub entry: Timestamp,
|
||||
/// Content of the annotation
|
||||
pub description: String,
|
||||
}
|
16
taskchampion/src/task/mod.rs
Normal file
16
taskchampion/src/task/mod.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
#![allow(clippy::module_inception)]
|
||||
use chrono::prelude::*;
|
||||
|
||||
mod annotation;
|
||||
mod priority;
|
||||
mod status;
|
||||
mod tag;
|
||||
mod task;
|
||||
|
||||
pub use annotation::Annotation;
|
||||
pub use priority::Priority;
|
||||
pub use status::Status;
|
||||
pub use tag::{Tag, INVALID_TAG_CHARACTERS};
|
||||
pub use task::{Task, TaskMut};
|
||||
|
||||
pub type Timestamp = DateTime<Utc>;
|
48
taskchampion/src/task/priority.rs
Normal file
48
taskchampion/src/task/priority.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
/// The priority of a task
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Priority {
|
||||
/// Low
|
||||
L,
|
||||
/// Medium
|
||||
M,
|
||||
/// High
|
||||
H,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Priority {
|
||||
/// Get a Priority from the 1-character value in a TaskMap,
|
||||
/// defaulting to M
|
||||
pub(crate) fn from_taskmap(s: &str) -> Priority {
|
||||
match s {
|
||||
"L" => Priority::L,
|
||||
"M" => Priority::M,
|
||||
"H" => Priority::H,
|
||||
_ => Priority::M,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the 1-character value for this priority to use in the TaskMap.
|
||||
pub(crate) fn to_taskmap(&self) -> &str {
|
||||
match self {
|
||||
Priority::L => "L",
|
||||
Priority::M => "M",
|
||||
Priority::H => "H",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_priority() {
|
||||
assert_eq!(Priority::L.to_taskmap(), "L");
|
||||
assert_eq!(Priority::M.to_taskmap(), "M");
|
||||
assert_eq!(Priority::H.to_taskmap(), "H");
|
||||
assert_eq!(Priority::from_taskmap("L"), Priority::L);
|
||||
assert_eq!(Priority::from_taskmap("M"), Priority::M);
|
||||
assert_eq!(Priority::from_taskmap("H"), Priority::H);
|
||||
}
|
||||
}
|
54
taskchampion/src/task/status.rs
Normal file
54
taskchampion/src/task/status.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
/// The status of a task. The default status in "Pending".
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Status {
|
||||
Pending,
|
||||
Completed,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
/// Get a Status from the 1-character value in a TaskMap,
|
||||
/// defaulting to Pending
|
||||
pub(crate) fn from_taskmap(s: &str) -> Status {
|
||||
match s {
|
||||
"P" => Status::Pending,
|
||||
"C" => Status::Completed,
|
||||
"D" => Status::Deleted,
|
||||
_ => Status::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the 1-character value for this status to use in the TaskMap.
|
||||
pub(crate) fn to_taskmap(&self) -> &str {
|
||||
match self {
|
||||
Status::Pending => "P",
|
||||
Status::Completed => "C",
|
||||
Status::Deleted => "D",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full-name value for this status to use in the TaskMap.
|
||||
pub fn to_string(&self) -> &str {
|
||||
// TODO: should be impl Display
|
||||
match self {
|
||||
Status::Pending => "Pending",
|
||||
Status::Completed => "Completed",
|
||||
Status::Deleted => "Deleted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_status() {
|
||||
assert_eq!(Status::Pending.to_taskmap(), "P");
|
||||
assert_eq!(Status::Completed.to_taskmap(), "C");
|
||||
assert_eq!(Status::Deleted.to_taskmap(), "D");
|
||||
assert_eq!(Status::from_taskmap("P"), Status::Pending);
|
||||
assert_eq!(Status::from_taskmap("C"), Status::Completed);
|
||||
assert_eq!(Status::from_taskmap("D"), Status::Deleted);
|
||||
}
|
||||
}
|
169
taskchampion/src/task/tag.rs
Normal file
169
taskchampion/src/task/tag.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A Tag is a descriptor for a task, that is either present or absent, and can be used for
|
||||
/// filtering. Tags composed of all uppercase letters are reserved for synthetic tags.
|
||||
///
|
||||
/// Valid tags must not contain whitespace or any of the characters in [`INVALID_TAG_CHARACTERS`].
|
||||
/// The first characters additionally cannot be a digit, and subsequent characters cannot be `:`.
|
||||
/// This definition is based on [that of
|
||||
/// TaskWarrior](https://github.com/GothenburgBitFactory/taskwarrior/blob/663c6575ceca5bd0135ae884879339dac89d3142/src/Lexer.cpp#L146-L164).
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct Tag(TagInner);
|
||||
|
||||
/// Inner type to hide the implementation
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub(super) enum TagInner {
|
||||
User(String),
|
||||
Synthetic(SyntheticTag),
|
||||
}
|
||||
|
||||
pub const INVALID_TAG_CHARACTERS: &str = "+-*/(<>^! %=~";
|
||||
|
||||
impl Tag {
|
||||
/// True if this tag is a synthetic tag
|
||||
pub fn is_synthetic(&self) -> bool {
|
||||
matches!(self.0, TagInner::Synthetic(_))
|
||||
}
|
||||
|
||||
/// True if this tag is a user-provided tag (not synthetic)
|
||||
pub fn is_user(&self) -> bool {
|
||||
matches!(self.0, TagInner::User(_))
|
||||
}
|
||||
|
||||
pub(super) fn inner(&self) -> &TagInner {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub(super) fn from_inner(inner: TagInner) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Tag {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Tag, anyhow::Error> {
|
||||
fn err(value: &str) -> Result<Tag, anyhow::Error> {
|
||||
anyhow::bail!("invalid tag {:?}", value)
|
||||
}
|
||||
|
||||
// first, look for synthetic tags
|
||||
if value.chars().all(|c| c.is_ascii_uppercase()) {
|
||||
if let Ok(st) = SyntheticTag::from_str(value) {
|
||||
return Ok(Self(TagInner::Synthetic(st)));
|
||||
}
|
||||
// all uppercase, but not a valid synthetic tag
|
||||
return err(value);
|
||||
}
|
||||
|
||||
if let Some(c) = value.chars().next() {
|
||||
if c.is_whitespace() || c.is_ascii_digit() || INVALID_TAG_CHARACTERS.contains(c) {
|
||||
return err(value);
|
||||
}
|
||||
} else {
|
||||
return err(value);
|
||||
}
|
||||
if !value
|
||||
.chars()
|
||||
.skip(1)
|
||||
.all(|c| !(c.is_whitespace() || c == ':' || INVALID_TAG_CHARACTERS.contains(c)))
|
||||
{
|
||||
return err(value);
|
||||
}
|
||||
Ok(Self(TagInner::User(String::from(value))))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Tag {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Tag, Self::Error> {
|
||||
Self::from_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&String> for Tag {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &String) -> Result<Tag, Self::Error> {
|
||||
Self::from_str(&value[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Tag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.0 {
|
||||
TagInner::User(s) => s.fmt(f),
|
||||
TagInner::Synthetic(st) => st.as_ref().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Tag {
|
||||
fn as_ref(&self) -> &str {
|
||||
match &self.0 {
|
||||
TagInner::User(s) => s.as_ref(),
|
||||
TagInner::Synthetic(st) => st.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A synthetic tag, represented as an `enum`. This type is used directly by
|
||||
/// [`taskchampion::task::task`] for efficiency.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Hash,
|
||||
strum_macros::EnumString,
|
||||
strum_macros::AsRefStr,
|
||||
strum_macros::EnumIter,
|
||||
)]
|
||||
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub(super) enum SyntheticTag {
|
||||
// When adding items here, also implement and test them in `task.rs` and document them in
|
||||
// `docs/src/tags.md`.
|
||||
Waiting,
|
||||
Active,
|
||||
Pending,
|
||||
Completed,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[rstest]
|
||||
#[case::simple("abc")]
|
||||
#[case::colon_prefix(":abc")]
|
||||
#[case::letters_and_numbers("a123_456")]
|
||||
#[case::synthetic("WAITING")]
|
||||
fn test_tag_try_into_success(#[case] s: &'static str) {
|
||||
let tag: Tag = s.try_into().unwrap();
|
||||
// check Display (via to_string) and AsRef while we're here
|
||||
assert_eq!(tag.to_string(), s.to_owned());
|
||||
assert_eq!(tag.as_ref(), s);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty("")]
|
||||
#[case::colon_infix("a:b")]
|
||||
#[case::digits("999")]
|
||||
#[case::bangs("abc!!!")]
|
||||
#[case::no_such_synthetic("NOSUCH")]
|
||||
fn test_tag_try_into_err(#[case] s: &'static str) {
|
||||
let tag: Result<Tag, _> = s.try_into();
|
||||
assert_eq!(
|
||||
tag.unwrap_err().to_string(),
|
||||
format!("invalid tag \"{}\"", s)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,156 +1,13 @@
|
|||
use super::tag::{SyntheticTag, TagInner};
|
||||
use super::{Status, Tag};
|
||||
use crate::replica::Replica;
|
||||
use crate::storage::TaskMap;
|
||||
use chrono::prelude::*;
|
||||
use log::trace;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::convert::AsRef;
|
||||
use std::convert::TryInto;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type Timestamp = DateTime<Utc>;
|
||||
|
||||
/// The priority of a task
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Priority {
|
||||
/// Low
|
||||
L,
|
||||
/// Medium
|
||||
M,
|
||||
/// High
|
||||
H,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Priority {
|
||||
/// Get a Priority from the 1-character value in a TaskMap,
|
||||
/// defaulting to M
|
||||
pub(crate) fn from_taskmap(s: &str) -> Priority {
|
||||
match s {
|
||||
"L" => Priority::L,
|
||||
"M" => Priority::M,
|
||||
"H" => Priority::H,
|
||||
_ => Priority::M,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the 1-character value for this priority to use in the TaskMap.
|
||||
pub(crate) fn to_taskmap(&self) -> &str {
|
||||
match self {
|
||||
Priority::L => "L",
|
||||
Priority::M => "M",
|
||||
Priority::H => "H",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The status of a task. The default status in "Pending".
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Status {
|
||||
Pending,
|
||||
Completed,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
/// Get a Status from the 1-character value in a TaskMap,
|
||||
/// defaulting to Pending
|
||||
pub(crate) fn from_taskmap(s: &str) -> Status {
|
||||
match s {
|
||||
"P" => Status::Pending,
|
||||
"C" => Status::Completed,
|
||||
"D" => Status::Deleted,
|
||||
_ => Status::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the 1-character value for this status to use in the TaskMap.
|
||||
pub(crate) fn to_taskmap(&self) -> &str {
|
||||
match self {
|
||||
Status::Pending => "P",
|
||||
Status::Completed => "C",
|
||||
Status::Deleted => "D",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full-name value for this status to use in the TaskMap.
|
||||
pub fn to_string(&self) -> &str {
|
||||
// TODO: should be impl Display
|
||||
match self {
|
||||
Status::Pending => "Pending",
|
||||
Status::Completed => "Completed",
|
||||
Status::Deleted => "Deleted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Tag is a newtype around a String that limits its values to valid tags.
|
||||
///
|
||||
/// Valid tags must not contain whitespace or any of the characters in [`INVALID_TAG_CHARACTERS`].
|
||||
/// The first characters additionally cannot be a digit, and subsequent characters cannot be `:`.
|
||||
/// This definition is based on [that of
|
||||
/// TaskWarrior](https://github.com/GothenburgBitFactory/taskwarrior/blob/663c6575ceca5bd0135ae884879339dac89d3142/src/Lexer.cpp#L146-L164).
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
pub struct Tag(String);
|
||||
|
||||
pub const INVALID_TAG_CHARACTERS: &str = "+-*/(<>^! %=~";
|
||||
|
||||
impl Tag {
|
||||
fn from_str(value: &str) -> Result<Tag, anyhow::Error> {
|
||||
fn err(value: &str) -> Result<Tag, anyhow::Error> {
|
||||
anyhow::bail!("invalid tag {:?}", value)
|
||||
}
|
||||
|
||||
if let Some(c) = value.chars().next() {
|
||||
if c.is_whitespace() || c.is_ascii_digit() || INVALID_TAG_CHARACTERS.contains(c) {
|
||||
return err(value);
|
||||
}
|
||||
} else {
|
||||
return err(value);
|
||||
}
|
||||
if !value
|
||||
.chars()
|
||||
.skip(1)
|
||||
.all(|c| !(c.is_whitespace() || c == ':' || INVALID_TAG_CHARACTERS.contains(c)))
|
||||
{
|
||||
return err(value);
|
||||
}
|
||||
Ok(Self(String::from(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Tag {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Tag, Self::Error> {
|
||||
Self::from_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&String> for Tag {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &String) -> Result<Tag, Self::Error> {
|
||||
Self::from_str(&value[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Tag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Tag {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Annotation {
|
||||
pub entry: Timestamp,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// A task, as publicly exposed by this crate.
|
||||
///
|
||||
/// Note that Task objects represent a snapshot of the task at a moment in time, and are not
|
||||
|
@ -233,22 +90,46 @@ impl Task {
|
|||
.any(|(k, v)| k.starts_with("start.") && v.is_empty())
|
||||
}
|
||||
|
||||
/// Determine whether a given synthetic tag is present on this task. All other
|
||||
/// synthetic tag calculations are based on this one.
|
||||
fn has_synthetic_tag(&self, synth: &SyntheticTag) -> bool {
|
||||
match synth {
|
||||
SyntheticTag::Waiting => self.is_waiting(),
|
||||
SyntheticTag::Active => self.is_active(),
|
||||
SyntheticTag::Pending => self.get_status() == Status::Pending,
|
||||
SyntheticTag::Completed => self.get_status() == Status::Completed,
|
||||
SyntheticTag::Deleted => self.get_status() == Status::Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this task has the given tag
|
||||
pub fn has_tag(&self, tag: &Tag) -> bool {
|
||||
self.taskmap.contains_key(&format!("tag.{}", tag))
|
||||
match tag.inner() {
|
||||
TagInner::User(s) => self.taskmap.contains_key(&format!("tag.{}", s)),
|
||||
TagInner::Synthetic(st) => self.has_synthetic_tag(st),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the task's tags
|
||||
pub fn get_tags(&self) -> impl Iterator<Item = Tag> + '_ {
|
||||
self.taskmap.iter().filter_map(|(k, _)| {
|
||||
if let Some(tag) = k.strip_prefix("tag.") {
|
||||
if let Ok(tag) = tag.try_into() {
|
||||
return Some(tag);
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
self.taskmap
|
||||
.iter()
|
||||
.filter_map(|(k, _)| {
|
||||
if let Some(tag) = k.strip_prefix("tag.") {
|
||||
if let Ok(tag) = tag.try_into() {
|
||||
return Some(tag);
|
||||
}
|
||||
// note that invalid "tag.*" are ignored
|
||||
}
|
||||
// note that invalid "tag.*" are ignored
|
||||
}
|
||||
None
|
||||
})
|
||||
None
|
||||
})
|
||||
.chain(
|
||||
SyntheticTag::iter()
|
||||
.filter(move |st| self.has_synthetic_tag(st))
|
||||
.map(|st| Tag::from_inner(TagInner::Synthetic(st))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_modified(&self) -> Option<DateTime<Utc>> {
|
||||
|
@ -324,13 +205,24 @@ impl<'r> TaskMut<'r> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark this task as complete
|
||||
pub fn done(&mut self) -> anyhow::Result<()> {
|
||||
self.set_status(Status::Completed)
|
||||
}
|
||||
|
||||
/// Add a tag to this task. Does nothing if the tag is already present.
|
||||
pub fn add_tag(&mut self, tag: &Tag) -> anyhow::Result<()> {
|
||||
if tag.is_synthetic() {
|
||||
anyhow::bail!("Synthetic tags cannot be modified");
|
||||
}
|
||||
self.set_string(format!("tag.{}", tag), Some("".to_owned()))
|
||||
}
|
||||
|
||||
/// Remove a tag from this task. Does nothing if the tag is not present.
|
||||
pub fn remove_tag(&mut self, tag: &Tag) -> anyhow::Result<()> {
|
||||
if tag.is_synthetic() {
|
||||
anyhow::bail!("Synthetic tags cannot be modified");
|
||||
}
|
||||
self.set_string(format!("tag.{}", tag), None)
|
||||
}
|
||||
|
||||
|
@ -416,28 +308,14 @@ mod test {
|
|||
f(task)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag_from_str() {
|
||||
let tag: Tag = "abc".try_into().unwrap();
|
||||
assert_eq!(tag, Tag("abc".to_owned()));
|
||||
/// Create a user tag, without checking its validity
|
||||
fn utag(name: &'static str) -> Tag {
|
||||
Tag::from_inner(TagInner::User(name.into()))
|
||||
}
|
||||
|
||||
let tag: Tag = ":abc".try_into().unwrap();
|
||||
assert_eq!(tag, Tag(":abc".to_owned()));
|
||||
|
||||
let tag: Tag = "a123_456".try_into().unwrap();
|
||||
assert_eq!(tag, Tag("a123_456".to_owned()));
|
||||
|
||||
let tag: Result<Tag, _> = "".try_into();
|
||||
assert_eq!(tag.unwrap_err().to_string(), "invalid tag \"\"");
|
||||
|
||||
let tag: Result<Tag, _> = "a:b".try_into();
|
||||
assert_eq!(tag.unwrap_err().to_string(), "invalid tag \"a:b\"");
|
||||
|
||||
let tag: Result<Tag, _> = "999".try_into();
|
||||
assert_eq!(tag.unwrap_err().to_string(), "invalid tag \"999\"");
|
||||
|
||||
let tag: Result<Tag, _> = "abc!!".try_into();
|
||||
assert_eq!(tag.unwrap_err().to_string(), "invalid tag \"abc!!\"");
|
||||
/// Create a synthetic tag
|
||||
fn stag(synth: SyntheticTag) -> Tag {
|
||||
Tag::from_inner(TagInner::Synthetic(synth))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -511,13 +389,19 @@ mod test {
|
|||
fn test_has_tag() {
|
||||
let task = Task::new(
|
||||
Uuid::new_v4(),
|
||||
vec![(String::from("tag.abc"), String::from(""))]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
vec![
|
||||
(String::from("tag.abc"), String::from("")),
|
||||
(String::from("start.1234"), String::from("")),
|
||||
]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
assert!(task.has_tag(&"abc".try_into().unwrap()));
|
||||
assert!(!task.has_tag(&"def".try_into().unwrap()));
|
||||
assert!(task.has_tag(&utag("abc")));
|
||||
assert!(!task.has_tag(&utag("def")));
|
||||
assert!(task.has_tag(&stag(SyntheticTag::Active)));
|
||||
assert!(task.has_tag(&stag(SyntheticTag::Pending)));
|
||||
assert!(!task.has_tag(&stag(SyntheticTag::Waiting)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -527,6 +411,8 @@ mod test {
|
|||
vec![
|
||||
(String::from("tag.abc"), String::from("")),
|
||||
(String::from("tag.def"), String::from("")),
|
||||
// set `wait` so the synthetic tag WAITING is present
|
||||
(String::from("wait"), String::from("33158909732")),
|
||||
]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
|
@ -534,7 +420,14 @@ mod test {
|
|||
|
||||
let mut tags: Vec<_> = task.get_tags().collect();
|
||||
tags.sort();
|
||||
assert_eq!(tags, vec![Tag("abc".to_owned()), Tag("def".to_owned())]);
|
||||
let mut exp = vec![
|
||||
utag("abc"),
|
||||
utag("def"),
|
||||
stag(SyntheticTag::Pending),
|
||||
stag(SyntheticTag::Waiting),
|
||||
];
|
||||
exp.sort();
|
||||
assert_eq!(tags, exp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -553,7 +446,7 @@ mod test {
|
|||
|
||||
// only "ok" is OK
|
||||
let tags: Vec<_> = task.get_tags().collect();
|
||||
assert_eq!(tags, vec![Tag("ok".to_owned())]);
|
||||
assert_eq!(tags, vec![utag("ok"), stag(SyntheticTag::Pending)]);
|
||||
}
|
||||
|
||||
fn count_taskmap(task: &TaskMut, f: fn(&(&String, &String)) -> bool) -> usize {
|
||||
|
@ -613,6 +506,20 @@ mod test {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_done() {
|
||||
with_mut_task(|mut task| {
|
||||
task.done().unwrap();
|
||||
assert_eq!(task.get_status(), Status::Completed);
|
||||
assert!(task.has_tag(&stag(SyntheticTag::Completed)));
|
||||
|
||||
// redundant call does nothing..
|
||||
task.done().unwrap();
|
||||
assert_eq!(task.get_status(), Status::Completed);
|
||||
assert!(task.has_tag(&stag(SyntheticTag::Completed)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop_multiple() {
|
||||
with_mut_task(|mut task| {
|
||||
|
@ -648,12 +555,12 @@ mod test {
|
|||
#[test]
|
||||
fn test_add_tags() {
|
||||
with_mut_task(|mut task| {
|
||||
task.add_tag(&Tag("abc".to_owned())).unwrap();
|
||||
task.add_tag(&utag("abc")).unwrap();
|
||||
assert!(task.taskmap.contains_key("tag.abc"));
|
||||
task.reload().unwrap();
|
||||
assert!(task.taskmap.contains_key("tag.abc"));
|
||||
// redundant add has no effect..
|
||||
task.add_tag(&Tag("abc".to_owned())).unwrap();
|
||||
task.add_tag(&utag("abc")).unwrap();
|
||||
assert!(task.taskmap.contains_key("tag.abc"));
|
||||
});
|
||||
}
|
||||
|
@ -661,35 +568,15 @@ mod test {
|
|||
#[test]
|
||||
fn test_remove_tags() {
|
||||
with_mut_task(|mut task| {
|
||||
task.add_tag(&Tag("abc".to_owned())).unwrap();
|
||||
task.add_tag(&utag("abc")).unwrap();
|
||||
task.reload().unwrap();
|
||||
assert!(task.taskmap.contains_key("tag.abc"));
|
||||
|
||||
task.remove_tag(&Tag("abc".to_owned())).unwrap();
|
||||
task.remove_tag(&utag("abc")).unwrap();
|
||||
assert!(!task.taskmap.contains_key("tag.abc"));
|
||||
// redundant remove has no effect..
|
||||
task.remove_tag(&Tag("abc".to_owned())).unwrap();
|
||||
task.remove_tag(&utag("abc")).unwrap();
|
||||
assert!(!task.taskmap.contains_key("tag.abc"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority() {
|
||||
assert_eq!(Priority::L.to_taskmap(), "L");
|
||||
assert_eq!(Priority::M.to_taskmap(), "M");
|
||||
assert_eq!(Priority::H.to_taskmap(), "H");
|
||||
assert_eq!(Priority::from_taskmap("L"), Priority::L);
|
||||
assert_eq!(Priority::from_taskmap("M"), Priority::M);
|
||||
assert_eq!(Priority::from_taskmap("H"), Priority::H);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status() {
|
||||
assert_eq!(Status::Pending.to_taskmap(), "P");
|
||||
assert_eq!(Status::Completed.to_taskmap(), "C");
|
||||
assert_eq!(Status::Deleted.to_taskmap(), "D");
|
||||
assert_eq!(Status::from_taskmap("P"), Status::Pending);
|
||||
assert_eq!(Status::from_taskmap("C"), Status::Completed);
|
||||
assert_eq!(Status::from_taskmap("D"), Status::Deleted);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue