From 0a458b5f5b5534daac851a1ec3a026c18c5ddcfb Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Tue, 29 Dec 2020 22:54:07 +0000 Subject: [PATCH] Treat any bare word in the command line as a report name --- cli/src/argparse/args.rs | 5 + cli/src/argparse/mod.rs | 2 - cli/src/argparse/subcommand.rs | 172 ++++++++++-------- cli/src/invocation/cmd/mod.rs | 2 +- cli/src/invocation/cmd/{list.rs => report.rs} | 26 ++- cli/src/invocation/mod.rs | 8 +- cli/src/invocation/report.rs | 58 +++++- cli/src/lib.rs | 1 + cli/src/{argparse => }/report.rs | 4 +- 9 files changed, 177 insertions(+), 101 deletions(-) rename cli/src/invocation/cmd/{list.rs => report.rs} (55%) rename cli/src/{argparse => }/report.rs (94%) diff --git a/cli/src/argparse/args.rs b/cli/src/argparse/args.rs index c58b5ac51..dc568e7bf 100644 --- a/cli/src/argparse/args.rs +++ b/cli/src/argparse/args.rs @@ -30,6 +30,11 @@ pub(super) fn any(input: &str) -> IResult<&str, &str> { rest(input) } +/// Recognizes a report name +pub(super) fn report_name(input: &str) -> IResult<&str, &str> { + all_consuming(recognize(pair(alpha1, alphanumeric0)))(input) +} + /// Recognizes a literal string pub(super) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> { move |input: &str| all_consuming(nomtag(literal))(input) diff --git a/cli/src/argparse/mod.rs b/cli/src/argparse/mod.rs index 01df66bed..0bf656a8f 100644 --- a/cli/src/argparse/mod.rs +++ b/cli/src/argparse/mod.rs @@ -15,14 +15,12 @@ mod args; mod command; mod filter; mod modification; -mod report; mod subcommand; pub(crate) use args::TaskId; pub(crate) use command::Command; pub(crate) use filter::{Condition, Filter, Universe}; pub(crate) use modification::{DescriptionMod, Modification}; -pub(crate) use report::{Column, Property, Report, Sort, SortBy}; pub(crate) use subcommand::Subcommand; use crate::usage::Usage; diff --git a/cli/src/argparse/subcommand.rs b/cli/src/argparse/subcommand.rs index 5884cc4a9..0caf17ebd 100644 --- a/cli/src/argparse/subcommand.rs +++ b/cli/src/argparse/subcommand.rs @@ -1,7 +1,5 @@ use super::args::*; -use super::{ - ArgList, Column, DescriptionMod, Filter, Modification, Property, Report, Sort, SortBy, -}; +use super::{ArgList, DescriptionMod, Filter, Modification}; use crate::usage; use nom::{branch::alt, combinator::*, sequence::*, IResult}; use taskchampion::Status; @@ -39,8 +37,12 @@ pub(crate) enum Subcommand { }, /// Lists (reports) - List { - report: Report, + Report { + /// The name of the report to show + report_name: String, + + /// Additional filter terms beyond those in the report + filter: Filter, }, /// Per-task information (typically one task) @@ -56,16 +58,17 @@ pub(crate) enum Subcommand { impl Subcommand { pub(super) fn parse(input: ArgList) -> IResult { - alt(( + all_consuming(alt(( Version::parse, Help::parse, Add::parse, Modify::parse, - List::parse, Info::parse, Gc::parse, Sync::parse, - ))(input) + // This must come last since it accepts arbitrary report names + Report::parse, + )))(input) } pub(super) fn get_usage(u: &mut usage::Usage) { @@ -73,10 +76,10 @@ impl Subcommand { Help::get_usage(u); Add::get_usage(u); Modify::get_usage(u); - List::get_usage(u); Info::get_usage(u); Gc::get_usage(u); Sync::get_usage(u); + Report::get_usage(u); } } @@ -251,59 +254,43 @@ impl Modify { } } -struct List; - -impl List { - // temporary - fn default_report() -> Report { - Report { - columns: vec![ - Column { - label: "Id".to_owned(), - property: Property::Id, - }, - Column { - label: "Description".to_owned(), - property: Property::Description, - }, - Column { - label: "Active".to_owned(), - property: Property::Active, - }, - Column { - label: "Tags".to_owned(), - property: Property::Tags, - }, - ], - sort: vec![Sort { - ascending: false, - sort_by: SortBy::Uuid, - }], - ..Default::default() - } - } +struct Report; +impl Report { fn parse(input: ArgList) -> IResult { - fn to_subcommand(input: (Filter, &str)) -> Result { - let report = Report { - filter: input.0, - ..List::default_report() - }; - Ok(Subcommand::List { report }) + fn to_subcommand(filter: Filter, report_name: &str) -> Result { + Ok(Subcommand::Report { + filter, + report_name: report_name.to_owned(), + }) } - map_res( - pair(Filter::parse, arg_matching(literal("list"))), - to_subcommand, - )(input) + // allow the filter expression before or after the report name + alt(( + map_res(pair(arg_matching(report_name), Filter::parse), |input| { + to_subcommand(input.1, input.0) + }), + map_res(pair(Filter::parse, arg_matching(report_name)), |input| { + to_subcommand(input.0, input.1) + }), + // default to a "next" report + map_res(Filter::parse, |input| to_subcommand(input, "next")), + ))(input) } fn get_usage(u: &mut usage::Usage) { u.subcommands.push(usage::Subcommand { - name: "list", - syntax: "[filter] list", - summary: "List tasks", + name: "report", + syntax: "[filter] [report-name] *or* [report-name] [filter]", + summary: "Show a report", description: " - Show a list of the tasks matching the filter", + Show the named report, including only tasks matching the filter", + }); + u.subcommands.push(usage::Subcommand { + name: "next", + syntax: "[filter]", + summary: "Show the 'next' report", + description: " + Show the report named 'next', including only tasks matching the filter", }); } } @@ -634,31 +621,72 @@ mod test { } #[test] - fn test_list() { - let subcommand = Subcommand::List { - report: Report { - ..List::default_report() - }, + fn test_report() { + let subcommand = Subcommand::Report { + filter: Default::default(), + report_name: "myreport".to_owned(), }; assert_eq!( - Subcommand::parse(argv!["list"]).unwrap(), + Subcommand::parse(argv!["myreport"]).unwrap(), (&EMPTY[..], subcommand) ); } #[test] - fn test_list_filter() { - let subcommand = Subcommand::List { - report: Report { - filter: Filter { - universe: Universe::for_ids(vec![12, 13]), - ..Default::default() - }, - ..List::default_report() + fn test_report_filter_before() { + let subcommand = Subcommand::Report { + filter: Filter { + universe: Universe::for_ids(vec![12, 13]), + ..Default::default() }, + report_name: "foo".to_owned(), }; assert_eq!( - Subcommand::parse(argv!["12,13", "list"]).unwrap(), + Subcommand::parse(argv!["12,13", "foo"]).unwrap(), + (&EMPTY[..], subcommand) + ); + } + + #[test] + fn test_report_filter_after() { + let subcommand = Subcommand::Report { + filter: Filter { + universe: Universe::for_ids(vec![12, 13]), + ..Default::default() + }, + report_name: "foo".to_owned(), + }; + assert_eq!( + Subcommand::parse(argv!["foo", "12,13"]).unwrap(), + (&EMPTY[..], subcommand) + ); + } + + #[test] + fn test_report_filter_next() { + let subcommand = Subcommand::Report { + filter: Filter { + universe: Universe::for_ids(vec![12, 13]), + ..Default::default() + }, + report_name: "next".to_owned(), + }; + assert_eq!( + Subcommand::parse(argv!["12,13"]).unwrap(), + (&EMPTY[..], subcommand) + ); + } + + #[test] + fn test_report_next() { + let subcommand = Subcommand::Report { + filter: Filter { + ..Default::default() + }, + report_name: "next".to_owned(), + }; + assert_eq!( + Subcommand::parse(argv![]).unwrap(), (&EMPTY[..], subcommand) ); } @@ -704,11 +732,7 @@ mod test { #[test] fn test_gc_extra_args() { - let subcommand = Subcommand::Gc; - assert_eq!( - Subcommand::parse(argv!["gc", "foo"]).unwrap(), - (&vec!["foo"][..], subcommand) - ); + assert!(Subcommand::parse(argv!["gc", "foo"]).is_err()); } #[test] diff --git a/cli/src/invocation/cmd/mod.rs b/cli/src/invocation/cmd/mod.rs index 43050215b..18a973ebb 100644 --- a/cli/src/invocation/cmd/mod.rs +++ b/cli/src/invocation/cmd/mod.rs @@ -4,7 +4,7 @@ pub(crate) mod add; pub(crate) mod gc; pub(crate) mod help; pub(crate) mod info; -pub(crate) mod list; pub(crate) mod modify; +pub(crate) mod report; pub(crate) mod sync; pub(crate) mod version; diff --git a/cli/src/invocation/cmd/list.rs b/cli/src/invocation/cmd/report.rs similarity index 55% rename from cli/src/invocation/cmd/list.rs rename to cli/src/invocation/cmd/report.rs index 48f8543b6..38a72b3e7 100644 --- a/cli/src/invocation/cmd/list.rs +++ b/cli/src/invocation/cmd/report.rs @@ -1,4 +1,4 @@ -use crate::argparse::Report; +use crate::argparse::Filter; use crate::invocation::display_report; use failure::Fallible; use taskchampion::Replica; @@ -7,35 +7,33 @@ use termcolor::WriteColor; pub(crate) fn execute( w: &mut W, replica: &mut Replica, - report: Report, + report_name: String, + filter: Filter, ) -> Fallible<()> { - display_report(w, replica, &report) + display_report(w, replica, report_name, filter) } #[cfg(test)] mod test { use super::*; - use crate::argparse::{Column, Filter, Property}; + use crate::argparse::Filter; use crate::invocation::test::*; use taskchampion::Status; #[test] - fn test_list() { + fn test_report() { let mut w = test_writer(); let mut replica = test_replica(); replica.new_task(Status::Pending, s!("my task")).unwrap(); - let report = Report { - filter: Filter { - ..Default::default() - }, - columns: vec![Column { - label: "Description".to_owned(), - property: Property::Description, - }], + // The function being tested is only one line long, so this is sort of an integration test + // for display_report. + + let report_name = "next".to_owned(); + let filter = Filter { ..Default::default() }; - execute(&mut w, &mut replica, report).unwrap(); + execute(&mut w, &mut replica, report_name, filter).unwrap(); assert!(w.into_string().contains("my task")); } } diff --git a/cli/src/invocation/mod.rs b/cli/src/invocation/mod.rs index 685c96cf2..6cbc0df03 100644 --- a/cli/src/invocation/mod.rs +++ b/cli/src/invocation/mod.rs @@ -60,9 +60,13 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> { } => return cmd::modify::execute(&mut w, &mut replica, filter, modification), Command { - subcommand: Subcommand::List { report }, + subcommand: + Subcommand::Report { + report_name, + filter, + }, .. - } => return cmd::list::execute(&mut w, &mut replica, report), + } => return cmd::report::execute(&mut w, &mut replica, report_name, filter), Command { subcommand: Subcommand::Info { filter, debug }, diff --git a/cli/src/invocation/report.rs b/cli/src/invocation/report.rs index db5d577df..0ed793f7d 100644 --- a/cli/src/invocation/report.rs +++ b/cli/src/invocation/report.rs @@ -1,7 +1,8 @@ -use crate::argparse::{Column, Property, Report, SortBy}; +use crate::argparse::Filter; use crate::invocation::filtered_tasks; +use crate::report::{Column, Property, Report, Sort, SortBy}; use crate::table; -use failure::Fallible; +use failure::{bail, Fallible}; use prettytable::{Row, Table}; use std::cmp::Ordering; use taskchampion::{Replica, Task, Uuid}; @@ -101,20 +102,64 @@ fn task_column(task: &Task, column: &Column, working_set: &WorkingSet) -> String } } +fn get_report(report_name: String, filter: Filter) -> Fallible { + let columns = vec![ + Column { + label: "Id".to_owned(), + property: Property::Id, + }, + Column { + label: "Description".to_owned(), + property: Property::Description, + }, + Column { + label: "Active".to_owned(), + property: Property::Active, + }, + Column { + label: "Tags".to_owned(), + property: Property::Tags, + }, + ]; + let sort = vec![Sort { + ascending: false, + sort_by: SortBy::Uuid, + }]; + use crate::argparse::Universe; + Ok(match report_name.as_ref() { + "list" => Report { + columns, + sort, + filter, + }, + "next" => Report { + columns, + sort, + // TODO: merge invocation filter and report filter + filter: Filter { + universe: Universe::PendingTasks, + ..Default::default() + }, + }, + _ => bail!("Unknown report {:?}", report_name), + }) +} + pub(super) fn display_report( w: &mut W, replica: &mut Replica, - report: &Report, + report_name: String, + filter: Filter, ) -> Fallible<()> { let mut t = Table::new(); - + let report = get_report(report_name, filter)?; let working_set = WorkingSet::new(replica)?; // Get the tasks from the filter let mut tasks: Vec<_> = filtered_tasks(replica, &report.filter)?.collect(); // ..sort them as desired - sort_tasks(&mut tasks, report, &working_set); + sort_tasks(&mut tasks, &report, &working_set); // ..set up the column titles t.set_format(table::format()); @@ -138,8 +183,8 @@ pub(super) fn display_report( #[cfg(test)] mod test { use super::*; - use crate::argparse::{Column, Property, Report, Sort, SortBy}; use crate::invocation::test::*; + use crate::report::Sort; use std::convert::TryInto; use taskchampion::Status; @@ -371,4 +416,3 @@ mod test { assert_eq!(task_column(&task, &column, &working_set), s!("")); } } -// TODO: test task_column diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 379b3ea81..38546f6cb 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -38,6 +38,7 @@ mod macros; mod argparse; mod invocation; +mod report; mod settings; mod table; mod usage; diff --git a/cli/src/argparse/report.rs b/cli/src/report.rs similarity index 94% rename from cli/src/argparse/report.rs rename to cli/src/report.rs index 3b569652c..370e48706 100644 --- a/cli/src/argparse/report.rs +++ b/cli/src/report.rs @@ -1,4 +1,6 @@ -use super::Filter; +//! This module contains the data structures used to define reports. + +use crate::argparse::Filter; /// A report specifies a filter as well as a sort order and information about which /// task attributes to display