From b7c12eec1e731a769862152ae909c47a9e8d872b Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 30 Dec 2020 00:51:29 +0000 Subject: [PATCH] Allow filtering by status --- cli/src/argparse/args.rs | 48 +++++++++++++++++++++++++++++++- cli/src/argparse/filter.rs | 56 ++++++++++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/cli/src/argparse/args.rs b/cli/src/argparse/args.rs index dc568e7bf..337bca763 100644 --- a/cli/src/argparse/args.rs +++ b/cli/src/argparse/args.rs @@ -10,7 +10,7 @@ use nom::{ sequence::*, Err, IResult, }; -use taskchampion::Uuid; +use taskchampion::{Status, Uuid}; /// A task identifier, as given in a filter command-line expression #[derive(Debug, PartialEq, Clone)] @@ -40,6 +40,32 @@ pub(super) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, & move |input: &str| all_consuming(nomtag(literal))(input) } +/// Recognizes a colon-prefixed pair +pub(super) fn colon_prefixed(prefix: &'static str) -> impl Fn(&str) -> IResult<&str, &str> { + fn to_suffix<'a>(input: (&'a str, char, &'a str)) -> Result<&'a str, ()> { + Ok(input.2) + } + move |input: &str| { + map_res( + all_consuming(tuple((nomtag(prefix), char(':'), any))), + to_suffix, + )(input) + } +} + +/// Recognizes `status:{pending,completed,deleted}` +pub(super) fn status_colon(input: &str) -> IResult<&str, Status> { + fn to_status(input: &str) -> Result { + match input { + "pending" => Ok(Status::Pending), + "completed" => Ok(Status::Completed), + "deleted" => Ok(Status::Deleted), + _ => Err(()), + } + } + map_res(colon_prefixed("status"), to_status)(input) +} + /// Recognizes a comma-separated list of TaskIds pub(super) fn id_list(input: &str) -> IResult<&str, Vec> { fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> { @@ -164,6 +190,26 @@ mod test { assert!(arg_matching(plus_tag)(argv!["foo", "bar"]).is_err()); } + #[test] + fn test_colon_prefixed() { + assert_eq!(colon_prefixed("foo")("foo:abc").unwrap().1, "abc"); + assert_eq!(colon_prefixed("foo")("foo:").unwrap().1, ""); + assert!(colon_prefixed("foo")("foo").is_err()); + } + + #[test] + fn test_status_colon() { + assert_eq!(status_colon("status:pending").unwrap().1, Status::Pending); + assert_eq!( + status_colon("status:completed").unwrap().1, + Status::Completed + ); + assert_eq!(status_colon("status:deleted").unwrap().1, Status::Deleted); + assert!(status_colon("status:foo").is_err()); + assert!(status_colon("status:complete").is_err()); + assert!(status_colon("status").is_err()); + } + #[test] fn test_plus_tag() { assert_eq!(plus_tag("+abc").unwrap().1, "abc"); diff --git a/cli/src/argparse/filter.rs b/cli/src/argparse/filter.rs index ba01a3fd0..6a8033eb3 100644 --- a/cli/src/argparse/filter.rs +++ b/cli/src/argparse/filter.rs @@ -1,4 +1,4 @@ -use super::args::{arg_matching, id_list, minus_tag, plus_tag, TaskId}; +use super::args::{arg_matching, id_list, minus_tag, plus_tag, status_colon, TaskId}; use super::ArgList; use crate::usage; use nom::{branch::alt, combinator::*, multi::fold_many0, IResult}; @@ -25,7 +25,6 @@ pub(crate) enum Condition { /// Task does not have the given tag NoTag(String), - // TODO: add command-line syntax for this /// Task has the given status Status(Status), @@ -36,7 +35,12 @@ pub(crate) enum Condition { impl Filter { pub(super) fn parse(input: ArgList) -> IResult { fold_many0( - alt((Self::id_list, Self::plus_tag, Self::minus_tag)), + alt(( + Self::parse_id_list, + Self::parse_plus_tag, + Self::parse_minus_tag, + Self::parse_status, + )), Filter { ..Default::default() }, @@ -78,25 +82,32 @@ impl Filter { // parsers - fn id_list(input: ArgList) -> IResult { - fn to_filterarg(input: Vec) -> Result { + fn parse_id_list(input: ArgList) -> IResult { + fn to_condition(input: Vec) -> Result { Ok(Condition::IdList(input)) } - map_res(arg_matching(id_list), to_filterarg)(input) + map_res(arg_matching(id_list), to_condition)(input) } - fn plus_tag(input: ArgList) -> IResult { - fn to_filterarg(input: &str) -> Result { + fn parse_plus_tag(input: ArgList) -> IResult { + fn to_condition(input: &str) -> Result { Ok(Condition::HasTag(input.to_owned())) } - map_res(arg_matching(plus_tag), to_filterarg)(input) + map_res(arg_matching(plus_tag), to_condition)(input) } - fn minus_tag(input: ArgList) -> IResult { - fn to_filterarg(input: &str) -> Result { + fn parse_minus_tag(input: ArgList) -> IResult { + fn to_condition(input: &str) -> Result { Ok(Condition::NoTag(input.to_owned())) } - map_res(arg_matching(minus_tag), to_filterarg)(input) + map_res(arg_matching(minus_tag), to_condition)(input) + } + + fn parse_status(input: ArgList) -> IResult { + fn to_condition(input: Status) -> Result { + Ok(Condition::Status(input)) + } + map_res(arg_matching(status_colon), to_condition)(input) } // usage @@ -123,6 +134,12 @@ impl Filter { description: " Select tasks that do not have the given tag.", }); + u.filters.push(usage::Filter { + syntax: "status:pending, status:completed, status:deleted", + summary: "Task status", + description: " + Select tasks with the given status.", + }); } } @@ -218,6 +235,21 @@ mod test { ); } + #[test] + fn test_status() { + let (input, filter) = Filter::parse(argv!["status:completed", "status:pending"]).unwrap(); + assert_eq!(input.len(), 0); + assert_eq!( + filter, + Filter { + conditions: vec![ + Condition::Status(Status::Completed), + Condition::Status(Status::Pending), + ], + } + ); + } + #[test] fn intersect_idlist_idlist() { let left = Filter::parse(argv!["1,2", "+yes"]).unwrap().1;