Refactor command-line handling into modules per subcommands
This commit is contained in:
60
cli/src/cmd/add.rs
Normal file
60
cli/src/cmd/add.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::{format_err, Fallible};
|
||||
use taskchampion::Status;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
description: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("add").about("adds a task").arg(
|
||||
Arg::with_name("description")
|
||||
.help("task description")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("add", Some(matches)) => {
|
||||
// TODO: .unwrap() would be safe here as description is required above
|
||||
let description: String = match matches.value_of("description") {
|
||||
Some(v) => v.into(),
|
||||
None => return ArgMatchResult::Err(format_err!("no description provided")),
|
||||
};
|
||||
ArgMatchResult::Ok(Box::new(Invocation { description }))
|
||||
}
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let uuid = Uuid::new_v4();
|
||||
command
|
||||
.get_replica()
|
||||
.new_task(uuid, Status::Pending, self.description.clone())
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "add", "foo bar"], |inv: &Invocation| {
|
||||
assert_eq!(inv.description, "foo bar".to_string());
|
||||
});
|
||||
}
|
||||
}
|
||||
37
cli/src/cmd/gc.rs
Normal file
37
cli/src/cmd/gc.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(ClapSubCommand::with_name("gc").about("run garbage collection"))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("gc", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
command.get_replica().gc()?;
|
||||
println!("garbage collected.");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "gc"], |_inv| {});
|
||||
}
|
||||
}
|
||||
39
cli/src/cmd/list.rs
Normal file
39
cli/src/cmd/list.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(ClapSubCommand::with_name("list").about("lists tasks"))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("list", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
for (uuid, task) in command.get_replica().all_tasks().unwrap() {
|
||||
println!("{} - {:?}", uuid, task);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "list"], |_inv| {});
|
||||
}
|
||||
}
|
||||
45
cli/src/cmd/macros.rs
Normal file
45
cli/src/cmd/macros.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
/// Define a Command type implementing SubCommand with the enclosed methods (`decorate_app` and
|
||||
/// `arg_match`), along with a module-level `cmd` function as the parent module expects.
|
||||
macro_rules! define_subcommand {
|
||||
($($f:item) +) => {
|
||||
struct Command;
|
||||
|
||||
pub(super) fn cmd() -> Box<dyn crate::cmd::SubCommand> {
|
||||
Box::new(Command)
|
||||
}
|
||||
|
||||
impl crate::cmd::SubCommand for Command {
|
||||
$($f)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Define an Invocation type implementing SubCommandInvocation with the enclosed methods.
|
||||
macro_rules! subcommand_invocation {
|
||||
($($f:item) +) => {
|
||||
impl crate::cmd::SubCommandInvocation for Invocation {
|
||||
$($f)+
|
||||
|
||||
#[cfg(test)]
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Parse the first argument as a command line and convert the result to an Invocation (which must
|
||||
/// be in scope). If the conversion works, calls the second argument with it.
|
||||
#[cfg(test)]
|
||||
macro_rules! with_subcommand_invocation {
|
||||
($args:expr, $check:expr) => {
|
||||
let parsed = crate::parse_command_line($args).unwrap();
|
||||
let si = parsed
|
||||
.subcommand
|
||||
.as_any()
|
||||
.downcast_ref::<Invocation>()
|
||||
.expect("SubComand is not of the expected type");
|
||||
($check)(si);
|
||||
};
|
||||
}
|
||||
69
cli/src/cmd/mod.rs
Normal file
69
cli/src/cmd/mod.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use clap::{App, ArgMatches};
|
||||
use failure::{Error, Fallible};
|
||||
use std::path::Path;
|
||||
use taskchampion::{taskstorage, Replica};
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
mod add;
|
||||
mod gc;
|
||||
mod list;
|
||||
mod pending;
|
||||
|
||||
/// Get a list of all subcommands in this crate
|
||||
pub(crate) fn subcommands() -> Vec<Box<dyn SubCommand>> {
|
||||
vec![add::cmd(), gc::cmd(), list::cmd(), pending::cmd()]
|
||||
}
|
||||
|
||||
/// The result of a [`crate::cmd::SubCommand::arg_match`] call
|
||||
pub(crate) enum ArgMatchResult {
|
||||
/// No match
|
||||
None,
|
||||
|
||||
/// A good match
|
||||
Ok(Box<dyn SubCommandInvocation>),
|
||||
|
||||
/// A match, but an issue with the command line
|
||||
Err(Error),
|
||||
}
|
||||
|
||||
/// A subcommand represents a defined subcommand, and is typically a singleton.
|
||||
pub(crate) trait SubCommand {
|
||||
/// Decorate the given [`clap::App`] appropriately for this subcommand
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a>;
|
||||
|
||||
/// If this ArgMatches is for this command, return an appropriate invocation.
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult;
|
||||
}
|
||||
|
||||
/// A subcommand invocation is specialized to a subcommand
|
||||
pub(crate) trait SubCommandInvocation: std::fmt::Debug {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()>;
|
||||
|
||||
// tests use downcasting, which requires a function to cast to Any
|
||||
#[cfg(test)]
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
/// A command invocation contains all of the necessary regarding a single invocation of the CLI.
|
||||
#[derive(Debug)]
|
||||
pub struct CommandInvocation {
|
||||
pub(crate) subcommand: Box<dyn SubCommandInvocation>,
|
||||
}
|
||||
|
||||
impl CommandInvocation {
|
||||
pub(crate) fn new(subcommand: Box<dyn SubCommandInvocation>) -> Self {
|
||||
Self { subcommand }
|
||||
}
|
||||
|
||||
pub fn run(self) -> Fallible<()> {
|
||||
self.subcommand.run(&self)
|
||||
}
|
||||
|
||||
fn get_replica(&self) -> Replica {
|
||||
Replica::new(Box::new(
|
||||
taskstorage::KVStorage::new(Path::new("/tmp/tasks")).unwrap(),
|
||||
))
|
||||
}
|
||||
}
|
||||
49
cli/src/cmd/pending.rs
Normal file
49
cli/src/cmd/pending.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(ClapSubCommand::with_name("pending").about("lists pending tasks"))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("pending", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
// default to this command when no subcommand is given
|
||||
("", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let working_set = command.get_replica().working_set().unwrap();
|
||||
for i in 1..working_set.len() {
|
||||
if let Some(ref task) = working_set[i] {
|
||||
println!("{}: {} - {:?}", i, task.get_uuid(), task);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "pending"], |_inv| {});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_default() {
|
||||
with_subcommand_invocation!(vec!["task"], |_inv| {});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user