Treat any bare word in the command line as a report name

This commit is contained in:
Dustin J. Mitchell 2020-12-29 22:54:07 +00:00
parent 21684666a6
commit 0a458b5f5b
9 changed files with 177 additions and 101 deletions

View file

@ -30,6 +30,11 @@ pub(super) fn any(input: &str) -> IResult<&str, &str> {
rest(input) 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 /// Recognizes a literal string
pub(super) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> { pub(super) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
move |input: &str| all_consuming(nomtag(literal))(input) move |input: &str| all_consuming(nomtag(literal))(input)

View file

@ -15,14 +15,12 @@ mod args;
mod command; mod command;
mod filter; mod filter;
mod modification; mod modification;
mod report;
mod subcommand; mod subcommand;
pub(crate) use args::TaskId; pub(crate) use args::TaskId;
pub(crate) use command::Command; pub(crate) use command::Command;
pub(crate) use filter::{Condition, Filter, Universe}; pub(crate) use filter::{Condition, Filter, Universe};
pub(crate) use modification::{DescriptionMod, Modification}; pub(crate) use modification::{DescriptionMod, Modification};
pub(crate) use report::{Column, Property, Report, Sort, SortBy};
pub(crate) use subcommand::Subcommand; pub(crate) use subcommand::Subcommand;
use crate::usage::Usage; use crate::usage::Usage;

View file

@ -1,7 +1,5 @@
use super::args::*; use super::args::*;
use super::{ use super::{ArgList, DescriptionMod, Filter, Modification};
ArgList, Column, DescriptionMod, Filter, Modification, Property, Report, Sort, SortBy,
};
use crate::usage; use crate::usage;
use nom::{branch::alt, combinator::*, sequence::*, IResult}; use nom::{branch::alt, combinator::*, sequence::*, IResult};
use taskchampion::Status; use taskchampion::Status;
@ -39,8 +37,12 @@ pub(crate) enum Subcommand {
}, },
/// Lists (reports) /// 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) /// Per-task information (typically one task)
@ -56,16 +58,17 @@ pub(crate) enum Subcommand {
impl Subcommand { impl Subcommand {
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Subcommand> { pub(super) fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
alt(( all_consuming(alt((
Version::parse, Version::parse,
Help::parse, Help::parse,
Add::parse, Add::parse,
Modify::parse, Modify::parse,
List::parse,
Info::parse, Info::parse,
Gc::parse, Gc::parse,
Sync::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) { pub(super) fn get_usage(u: &mut usage::Usage) {
@ -73,10 +76,10 @@ impl Subcommand {
Help::get_usage(u); Help::get_usage(u);
Add::get_usage(u); Add::get_usage(u);
Modify::get_usage(u); Modify::get_usage(u);
List::get_usage(u);
Info::get_usage(u); Info::get_usage(u);
Gc::get_usage(u); Gc::get_usage(u);
Sync::get_usage(u); Sync::get_usage(u);
Report::get_usage(u);
} }
} }
@ -251,59 +254,43 @@ impl Modify {
} }
} }
struct List; struct Report;
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()
}
}
impl Report {
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> { fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(input: (Filter, &str)) -> Result<Subcommand, ()> { fn to_subcommand(filter: Filter, report_name: &str) -> Result<Subcommand, ()> {
let report = Report { Ok(Subcommand::Report {
filter: input.0, filter,
..List::default_report() report_name: report_name.to_owned(),
}; })
Ok(Subcommand::List { report })
} }
map_res( // allow the filter expression before or after the report name
pair(Filter::parse, arg_matching(literal("list"))), alt((
to_subcommand, map_res(pair(arg_matching(report_name), Filter::parse), |input| {
)(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) { fn get_usage(u: &mut usage::Usage) {
u.subcommands.push(usage::Subcommand { u.subcommands.push(usage::Subcommand {
name: "list", name: "report",
syntax: "[filter] list", syntax: "[filter] [report-name] *or* [report-name] [filter]",
summary: "List tasks", summary: "Show a report",
description: " 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] #[test]
fn test_list() { fn test_report() {
let subcommand = Subcommand::List { let subcommand = Subcommand::Report {
report: Report { filter: Default::default(),
..List::default_report() report_name: "myreport".to_owned(),
},
}; };
assert_eq!( assert_eq!(
Subcommand::parse(argv!["list"]).unwrap(), Subcommand::parse(argv!["myreport"]).unwrap(),
(&EMPTY[..], subcommand) (&EMPTY[..], subcommand)
); );
} }
#[test] #[test]
fn test_list_filter() { fn test_report_filter_before() {
let subcommand = Subcommand::List { let subcommand = Subcommand::Report {
report: Report { filter: Filter {
filter: Filter { universe: Universe::for_ids(vec![12, 13]),
universe: Universe::for_ids(vec![12, 13]), ..Default::default()
..Default::default()
},
..List::default_report()
}, },
report_name: "foo".to_owned(),
}; };
assert_eq!( 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) (&EMPTY[..], subcommand)
); );
} }
@ -704,11 +732,7 @@ mod test {
#[test] #[test]
fn test_gc_extra_args() { fn test_gc_extra_args() {
let subcommand = Subcommand::Gc; assert!(Subcommand::parse(argv!["gc", "foo"]).is_err());
assert_eq!(
Subcommand::parse(argv!["gc", "foo"]).unwrap(),
(&vec!["foo"][..], subcommand)
);
} }
#[test] #[test]

View file

@ -4,7 +4,7 @@ pub(crate) mod add;
pub(crate) mod gc; pub(crate) mod gc;
pub(crate) mod help; pub(crate) mod help;
pub(crate) mod info; pub(crate) mod info;
pub(crate) mod list;
pub(crate) mod modify; pub(crate) mod modify;
pub(crate) mod report;
pub(crate) mod sync; pub(crate) mod sync;
pub(crate) mod version; pub(crate) mod version;

View file

@ -1,4 +1,4 @@
use crate::argparse::Report; use crate::argparse::Filter;
use crate::invocation::display_report; use crate::invocation::display_report;
use failure::Fallible; use failure::Fallible;
use taskchampion::Replica; use taskchampion::Replica;
@ -7,35 +7,33 @@ use termcolor::WriteColor;
pub(crate) fn execute<W: WriteColor>( pub(crate) fn execute<W: WriteColor>(
w: &mut W, w: &mut W,
replica: &mut Replica, replica: &mut Replica,
report: Report, report_name: String,
filter: Filter,
) -> Fallible<()> { ) -> Fallible<()> {
display_report(w, replica, &report) display_report(w, replica, report_name, filter)
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::argparse::{Column, Filter, Property}; use crate::argparse::Filter;
use crate::invocation::test::*; use crate::invocation::test::*;
use taskchampion::Status; use taskchampion::Status;
#[test] #[test]
fn test_list() { fn test_report() {
let mut w = test_writer(); let mut w = test_writer();
let mut replica = test_replica(); let mut replica = test_replica();
replica.new_task(Status::Pending, s!("my task")).unwrap(); replica.new_task(Status::Pending, s!("my task")).unwrap();
let report = Report { // The function being tested is only one line long, so this is sort of an integration test
filter: Filter { // for display_report.
..Default::default()
}, let report_name = "next".to_owned();
columns: vec![Column { let filter = Filter {
label: "Description".to_owned(),
property: Property::Description,
}],
..Default::default() ..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")); assert!(w.into_string().contains("my task"));
} }
} }

View file

@ -60,9 +60,13 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> {
} => return cmd::modify::execute(&mut w, &mut replica, filter, modification), } => return cmd::modify::execute(&mut w, &mut replica, filter, modification),
Command { 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 { Command {
subcommand: Subcommand::Info { filter, debug }, subcommand: Subcommand::Info { filter, debug },

View file

@ -1,7 +1,8 @@
use crate::argparse::{Column, Property, Report, SortBy}; use crate::argparse::Filter;
use crate::invocation::filtered_tasks; use crate::invocation::filtered_tasks;
use crate::report::{Column, Property, Report, Sort, SortBy};
use crate::table; use crate::table;
use failure::Fallible; use failure::{bail, Fallible};
use prettytable::{Row, Table}; use prettytable::{Row, Table};
use std::cmp::Ordering; use std::cmp::Ordering;
use taskchampion::{Replica, Task, Uuid}; 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<Report> {
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: WriteColor>( pub(super) fn display_report<W: WriteColor>(
w: &mut W, w: &mut W,
replica: &mut Replica, replica: &mut Replica,
report: &Report, report_name: String,
filter: Filter,
) -> Fallible<()> { ) -> Fallible<()> {
let mut t = Table::new(); let mut t = Table::new();
let report = get_report(report_name, filter)?;
let working_set = WorkingSet::new(replica)?; let working_set = WorkingSet::new(replica)?;
// Get the tasks from the filter // Get the tasks from the filter
let mut tasks: Vec<_> = filtered_tasks(replica, &report.filter)?.collect(); let mut tasks: Vec<_> = filtered_tasks(replica, &report.filter)?.collect();
// ..sort them as desired // ..sort them as desired
sort_tasks(&mut tasks, report, &working_set); sort_tasks(&mut tasks, &report, &working_set);
// ..set up the column titles // ..set up the column titles
t.set_format(table::format()); t.set_format(table::format());
@ -138,8 +183,8 @@ pub(super) fn display_report<W: WriteColor>(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::argparse::{Column, Property, Report, Sort, SortBy};
use crate::invocation::test::*; use crate::invocation::test::*;
use crate::report::Sort;
use std::convert::TryInto; use std::convert::TryInto;
use taskchampion::Status; use taskchampion::Status;
@ -371,4 +416,3 @@ mod test {
assert_eq!(task_column(&task, &column, &working_set), s!("")); assert_eq!(task_column(&task, &column, &working_set), s!(""));
} }
} }
// TODO: test task_column

View file

@ -38,6 +38,7 @@ mod macros;
mod argparse; mod argparse;
mod invocation; mod invocation;
mod report;
mod settings; mod settings;
mod table; mod table;
mod usage; mod usage;

View file

@ -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 /// A report specifies a filter as well as a sort order and information about which
/// task attributes to display /// task attributes to display