Support CLI syntax for changing tags

This commit is contained in:
Dustin J. Mitchell 2020-12-21 19:40:07 +00:00
parent 28c5fb2268
commit 54e8484bc2
4 changed files with 97 additions and 35 deletions

View file

@ -58,7 +58,6 @@ pub(super) fn id_list(input: &str) -> IResult<&str, Vec<&str>> {
} }
/// Recognizes a tag prefixed with `+` and returns the tag value /// Recognizes a tag prefixed with `+` and returns the tag value
#[allow(dead_code)] // tags not implemented yet
pub(super) fn plus_tag(input: &str) -> IResult<&str, &str> { pub(super) fn plus_tag(input: &str) -> IResult<&str, &str> {
fn to_tag(input: (char, &str)) -> Result<&str, ()> { fn to_tag(input: (char, &str)) -> Result<&str, ()> {
Ok(input.1) Ok(input.1)
@ -70,7 +69,6 @@ pub(super) fn plus_tag(input: &str) -> IResult<&str, &str> {
} }
/// Recognizes a tag prefixed with `-` and returns the tag value /// Recognizes a tag prefixed with `-` and returns the tag value
#[allow(dead_code)] // tags not implemented yet
pub(super) fn minus_tag(input: &str) -> IResult<&str, &str> { pub(super) fn minus_tag(input: &str) -> IResult<&str, &str> {
fn to_tag(input: (char, &str)) -> Result<&str, ()> { fn to_tag(input: (char, &str)) -> Result<&str, ()> {
Ok(input.1) Ok(input.1)
@ -81,18 +79,6 @@ pub(super) fn minus_tag(input: &str) -> IResult<&str, &str> {
)(input) )(input)
} }
/// Recognizes a tag prefixed with either `-` or `+`, returning true for + and false for -
#[allow(dead_code)] // tags not implemented yet
pub(super) fn tag(input: &str) -> IResult<&str, (bool, &str)> {
fn to_plus(input: &str) -> Result<(bool, &str), ()> {
Ok((true, input))
}
fn to_minus(input: &str) -> Result<(bool, &str), ()> {
Ok((false, input))
}
alt((map_res(plus_tag, to_plus), map_res(minus_tag, to_minus)))(input)
}
/// Consume a single argument from an argument list that matches the given string parser (one /// Consume a single argument from an argument list that matches the given string parser (one
/// of the other functions in this module). The given parser must consume the entire input. /// of the other functions in this module). The given parser must consume the entire input.
pub(super) fn arg_matching<'a, O, F>(f: F) -> impl Fn(ArgList<'a>) -> IResult<ArgList, O> pub(super) fn arg_matching<'a, O, F>(f: F) -> impl Fn(ArgList<'a>) -> IResult<ArgList, O>
@ -131,14 +117,10 @@ mod test {
#[test] #[test]
fn test_arg_matching() { fn test_arg_matching() {
assert_eq!( assert_eq!(
arg_matching(tag)(argv!["+foo", "bar"]).unwrap(), arg_matching(plus_tag)(argv!["+foo", "bar"]).unwrap(),
(argv!["bar"], (true, "foo")) (argv!["bar"], "foo")
); );
assert_eq!( assert!(arg_matching(plus_tag)(argv!["foo", "bar"]).is_err());
arg_matching(tag)(argv!["-foo", "bar"]).unwrap(),
(argv!["bar"], (false, "foo"))
);
assert!(arg_matching(tag)(argv!["foo", "bar"]).is_err());
} }
#[test] #[test]
@ -161,16 +143,6 @@ mod test {
assert!(minus_tag("-1abc").is_err()); assert!(minus_tag("-1abc").is_err());
} }
#[test]
fn test_tag() {
assert_eq!(tag("-abc").unwrap().1, (false, "abc"));
assert_eq!(tag("+abc123").unwrap().1, (true, "abc123"));
assert!(tag("+abc123 --").is_err());
assert!(tag("-abc123 ").is_err());
assert!(tag(" -abc123").is_err());
assert!(tag("-1abc").is_err());
}
#[test] #[test]
fn test_literal() { fn test_literal() {
assert_eq!(literal("list")("list").unwrap().1, "list"); assert_eq!(literal("list")("list").unwrap().1, "list");

View file

@ -1,6 +1,7 @@
use super::args::{any, arg_matching}; use super::args::{any, arg_matching, minus_tag, plus_tag};
use super::ArgList; use super::ArgList;
use nom::{combinator::*, multi::fold_many0, IResult}; use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
use std::collections::HashSet;
use taskchampion::Status; use taskchampion::Status;
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
@ -34,13 +35,21 @@ pub struct Modification {
/// Set the status /// Set the status
pub status: Option<Status>, pub status: Option<Status>,
/// Set the "active" status, that is, start (true) or stop (false) the task. /// Set the "active" state, that is, start (true) or stop (false) the task.
pub active: Option<bool>, pub active: Option<bool>,
/// Add tags
pub add_tags: HashSet<String>,
/// Remove tags
pub remove_tags: HashSet<String>,
} }
/// A single argument that is part of a modification, used internally to this module /// A single argument that is part of a modification, used internally to this module
enum ModArg<'a> { enum ModArg<'a> {
Description(&'a str), Description(&'a str),
PlusTag(&'a str),
MinusTag(&'a str),
} }
impl Modification { impl Modification {
@ -55,11 +64,22 @@ impl Modification {
acc.description = DescriptionMod::Set(description.to_string()); acc.description = DescriptionMod::Set(description.to_string());
} }
} }
ModArg::PlusTag(tag) => {
acc.add_tags.insert(tag.to_owned());
}
ModArg::MinusTag(tag) => {
acc.remove_tags.insert(tag.to_owned());
}
} }
acc acc
} }
fold_many0( fold_many0(
Self::description, alt((
Self::plus_tag,
Self::minus_tag,
// this must come last
Self::description,
)),
Modification { Modification {
..Default::default() ..Default::default()
}, },
@ -73,6 +93,20 @@ impl Modification {
} }
map_res(arg_matching(any), to_modarg)(input) map_res(arg_matching(any), to_modarg)(input)
} }
fn plus_tag(input: ArgList) -> IResult<ArgList, ModArg> {
fn to_modarg(input: &str) -> Result<ModArg, ()> {
Ok(ModArg::PlusTag(input))
}
map_res(arg_matching(plus_tag), to_modarg)(input)
}
fn minus_tag(input: ArgList) -> IResult<ArgList, ModArg> {
fn to_modarg(input: &str) -> Result<ModArg, ()> {
Ok(ModArg::MinusTag(input))
}
map_res(arg_matching(minus_tag), to_modarg)(input)
}
} }
#[cfg(test)] #[cfg(test)]
@ -104,6 +138,19 @@ mod test {
); );
} }
#[test]
fn test_add_tags() {
let (input, modification) = Modification::parse(argv!["+abc", "+def"]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
modification,
Modification {
add_tags: set!["abc".to_owned(), "def".to_owned()],
..Default::default()
}
);
}
#[test] #[test]
fn test_multi_arg_description() { fn test_multi_arg_description() {
let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap(); let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap();
@ -116,4 +163,20 @@ mod test {
} }
); );
} }
#[test]
fn test_multi_arg_description_and_tags() {
let (input, modification) =
Modification::parse(argv!["new", "+next", "desc", "-daytime", "fun"]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
modification,
Modification {
description: DescriptionMod::Set("new desc fun".to_owned()),
add_tags: set!["next".to_owned()],
remove_tags: set!["daytime".to_owned()],
..Default::default()
}
);
}
} }

View file

@ -1,5 +1,6 @@
use crate::argparse::{DescriptionMod, Modification}; use crate::argparse::{DescriptionMod, Modification};
use failure::Fallible; use failure::Fallible;
use std::convert::TryInto;
use taskchampion::TaskMut; use taskchampion::TaskMut;
use termcolor::WriteColor; use termcolor::WriteColor;
@ -32,6 +33,18 @@ pub(super) fn apply_modification<W: WriteColor>(
task.stop()?; task.stop()?;
} }
for tag in modification.add_tags.iter() {
// note that the parser should have already ensured that this tag was valid
let tag = tag.try_into()?;
task.add_tag(&tag)?;
}
for tag in modification.remove_tags.iter() {
// note that the parser should have already ensured that this tag was valid
let tag = tag.try_into()?;
task.remove_tag(&tag)?;
}
write!(w, "modified task {}\n", task.get_uuid())?; write!(w, "modified task {}\n", task.get_uuid())?;
Ok(()) Ok(())

View file

@ -10,3 +10,17 @@ macro_rules! argv {
&[$($x),*][..] &[$($x),*][..]
); );
} }
/// Create a hashset, similar to vec!
#[cfg(test)]
macro_rules! set(
{ $($key:expr),+ } => {
{
let mut s = ::std::collections::HashSet::new();
$(
s.insert($key);
)+
s
}
};
);