Skip to content

Commit

Permalink
Merge pull request #129 from popematt/namespace
Browse files Browse the repository at this point in the history
Refactor namespace logic out of default command logic
  • Loading branch information
jobarr-amzn authored Jul 2, 2024
2 parents c47232b + acc364d commit e7d900f
Show file tree
Hide file tree
Showing 16 changed files with 151 additions and 76 deletions.
8 changes: 8 additions & 0 deletions src/bin/ion/commands/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ impl IonCliCommand for CatCommand {
"Prints all Ion input files to the specified output in the requested format."
}

fn is_stable(&self) -> bool {
true
}

fn is_porcelain(&self) -> bool {
false
}

fn configure_args(&self, command: Command) -> Command {
command
.alias("dump")
Expand Down
84 changes: 84 additions & 0 deletions src/bin/ion/commands/command_namespace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use crate::commands::{IonCliCommand, WithIonCliArgument, UNSTABLE_FLAG};
use clap::{ArgMatches, Command as ClapCommand};
use std::process;

/// A trait that handles the implementation of [IonCliCommand] for command namespaces.
pub trait IonCliNamespace {
fn name(&self) -> &'static str;
fn about(&self) -> &'static str;
fn subcommands(&self) -> Vec<Box<dyn IonCliCommand>>;
}

impl<T: IonCliNamespace> IonCliCommand for T {
// Namespaces can't be used on their own, so we'll pretend that they are all stable and
// let the leaf commands handle stability.
fn is_stable(&self) -> bool {
true
}

// Namespaces can't be used on their own, so we'll pretend that they are all plumbing and
// let the leaf commands handle plumbing vs porcelain.
fn is_porcelain(&self) -> bool {
false
}

fn name(&self) -> &'static str {
IonCliNamespace::name(self)
}

fn about(&self) -> &'static str {
IonCliNamespace::about(self)
}

fn configure_args(&self, command: ClapCommand) -> ClapCommand {
// Create a `ClapCommand` representing each of this command's subcommands.
let clap_subcommands: Vec<_> = self
.subcommands()
.iter()
.map(|s| s.clap_command())
.collect();

let mut command = command
.subcommand_required(true)
.subcommands(clap_subcommands);

// If there are subcommands, add them to the configuration and set 'subcommand_required'.
let has_unstable_subcommand = self.subcommands().iter().any(|sc| !sc.is_stable());
if has_unstable_subcommand {
command = command.show_unstable_flag();
}
command
}

fn run(&self, command_path: &mut Vec<String>, args: &ArgMatches) -> anyhow::Result<()> {
// Safe to unwrap because if this is a namespace are subcommands, then clap has already
// ensured that a known subcommand is present in args.
let (subcommand_name, subcommand_args) = args.subcommand().unwrap();
let subcommands = self.subcommands();
let subcommand: &dyn IonCliCommand = subcommands
.iter()
.find(|sc| sc.name() == subcommand_name)
.unwrap()
.as_ref();

match (subcommand.is_stable(), args.get_flag(UNSTABLE_FLAG)) {
// Warn if using an unnecessary `-X`
(true, true) => eprintln!(
"'{}' is stable and does not require opt-in",
subcommand_name
),
// Error if missing a required `-X`
(false, false) => {
eprintln!(
"'{}' is unstable and requires explicit opt-in",
subcommand_name
);
process::exit(1)
}
_ => {}
}

command_path.push(subcommand_name.to_owned());
subcommand.run(command_path, subcommand_args)
}
}
4 changes: 4 additions & 0 deletions src/bin/ion/commands/count.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ impl IonCliCommand for CountCommand {
false
}

fn is_porcelain(&self) -> bool {
false
}

fn configure_args(&self, command: Command) -> Command {
command.with_input()
}
Expand Down
11 changes: 10 additions & 1 deletion src/bin/ion/commands/from/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ impl IonCliCommand for FromJsonCommand {
}

fn is_stable(&self) -> bool {
false // TODO: Should this be true?
}

fn is_porcelain(&self) -> bool {
false
}

fn configure_args(&self, command: Command) -> Command {
command.with_input().with_output().with_format()
// Args must be identical to CatCommand so that we can safely delegate
command
.with_input()
.with_output()
.with_format()
.with_ion_version()
}

fn run(&self, command_path: &mut Vec<String>, args: &ArgMatches) -> Result<()> {
Expand Down
3 changes: 2 additions & 1 deletion src/bin/ion/commands/from/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::commands::command_namespace::IonCliNamespace;
use crate::commands::IonCliCommand;

use crate::commands::from::json::FromJsonCommand;
Expand All @@ -6,7 +7,7 @@ pub mod json;

pub struct FromNamespace;

impl IonCliCommand for FromNamespace {
impl IonCliNamespace for FromNamespace {
fn name(&self) -> &'static str {
"from"
}
Expand Down
4 changes: 4 additions & 0 deletions src/bin/ion/commands/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ impl IonCliCommand for GenerateCommand {
false
}

fn is_porcelain(&self) -> bool {
false
}

fn configure_args(&self, command: Command) -> Command {
command
.arg(
Expand Down
8 changes: 8 additions & 0 deletions src/bin/ion/commands/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ impl IonCliCommand for HeadCommand {
"Prints the specified number of top-level values in the input stream."
}

fn is_stable(&self) -> bool {
true
}

fn is_porcelain(&self) -> bool {
false
}

fn configure_args(&self, command: Command) -> Command {
// Same flags as `cat`, but with an added `--values` flag to specify the number of values to
// write.
Expand Down
4 changes: 4 additions & 0 deletions src/bin/ion/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ between versions. Stable output for programmatic use cases is a
non-goal."
}

fn is_stable(&self) -> bool {
true
}

fn is_porcelain(&self) -> bool {
true
}
Expand Down
74 changes: 6 additions & 68 deletions src/bin/ion/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ use clap::builder::ValueParser;
use clap::{crate_authors, crate_version, Arg, ArgAction, ArgMatches, Command as ClapCommand};
use std::fs::File;
use std::io::Write;
use std::process;
use termcolor::{ColorChoice, StandardStream, StandardStreamLock};

pub mod cat;
mod command_namespace;
pub mod count;
pub mod from;
#[cfg(feature = "experimental-code-gen")]
Expand All @@ -22,13 +22,15 @@ pub mod schema;
pub mod symtab;
pub mod to;

pub(crate) use command_namespace::IonCliNamespace;

/// Behaviors common to all Ion CLI commands, including both namespaces (groups of commands)
/// and the commands themselves.
pub trait IonCliCommand {
/// Indicates whether this command is stable (as opposed to unstable or experimental).
/// Namespaces should almost always be stable.
fn is_stable(&self) -> bool {
true
false
}

/// Whether the output format is machine-readable.
Expand All @@ -39,7 +41,7 @@ pub trait IonCliCommand {
///
/// See https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain#_plumbing_porcelain
fn is_porcelain(&self) -> bool {
false
true
}

/// Returns the name of this command.
Expand All @@ -56,13 +58,6 @@ pub trait IonCliCommand {
/// Commands wishing to customize their `ClapCommand`'s arguments should override
/// [`Self::configure_args`].
fn clap_command(&self) -> ClapCommand {
// Create a `ClapCommand` representing each of this command's subcommands.
let clap_subcommands: Vec<_> = self
.subcommands()
.iter()
.map(|s| s.clap_command())
.collect();

// Configure a 'base' clap configuration that has the command's name, about message,
// version, and author.

Expand Down Expand Up @@ -97,19 +92,6 @@ pub trait IonCliCommand {
);
}

// If there are subcommands, add them to the configuration and set 'subcommand_required'.
if !clap_subcommands.is_empty() {
let has_unstable_subcommand = self.subcommands().iter().any(|sc| !sc.is_stable());

if has_unstable_subcommand {
base_command = base_command.show_unstable_flag()
}

base_command = base_command
.subcommand_required(true)
.subcommands(clap_subcommands)
}

self.configure_args(base_command)
}

Expand All @@ -120,57 +102,13 @@ pub trait IonCliCommand {
command
}

/// Returns a `Vec` containing all of this command's subcommands.
///
/// Namespaces should override the default implementation to specify their subcommands.
/// Commands should use the default implementation.
fn subcommands(&self) -> Vec<Box<dyn IonCliCommand>> {
Vec::new()
}

/// Returns the subcommand that corresponds to the specified name. If no matching subcommand
/// is found, returns `None`.
fn get_subcommand(&self, subcommand_name: &str) -> Option<Box<dyn IonCliCommand>> {
let mut subcommands = self.subcommands();
if let Some(index) = subcommands.iter().position(|s| s.name() == subcommand_name) {
Some(subcommands.swap_remove(index))
} else {
None
}
}

/// The core logic of the command.
///
/// The default implementation assumes this command is a namespace (i.e. a group of subcommands).
/// It looks for a subcommand in the arguments, then looks up and runs that subcommand.
///
/// Commands should override this implementation.
fn run(&self, command_path: &mut Vec<String>, args: &ArgMatches) -> anyhow::Result<()> {
// Safe to unwrap because if this is a namespace are subcommands, then clap has already
// ensured that a known subcommand is present in args.
let (subcommand_name, subcommand_args) = args.subcommand().unwrap();
let subcommand = self.get_subcommand(subcommand_name).unwrap();

match (subcommand.is_stable(), args.get_flag(UNSTABLE_FLAG)) {
// Warn if using an unnecessary `-X`
(true, true) => eprintln!(
"'{}' is stable and does not require opt-in",
subcommand_name
),
// Error if missing a required `-X`
(false, false) => {
eprintln!(
"'{}' is unstable and requires explicit opt-in",
subcommand_name
);
process::exit(1)
}
_ => {}
}

command_path.push(subcommand_name.to_owned());
subcommand.run(command_path, subcommand_args)
}
fn run(&self, command_path: &mut Vec<String>, args: &ArgMatches) -> anyhow::Result<()>;
}

/// Argument ID for the '--unstable' / '-X' flag
Expand Down
3 changes: 2 additions & 1 deletion src/bin/ion/commands/schema/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
pub mod load;
pub mod validate;

use crate::commands::command_namespace::IonCliNamespace;
use crate::commands::IonCliCommand;

use crate::commands::schema::load::LoadCommand;
use crate::commands::schema::validate::ValidateCommand;

pub struct SchemaNamespace;

impl IonCliCommand for SchemaNamespace {
impl IonCliNamespace for SchemaNamespace {
fn name(&self) -> &'static str {
"schema"
}
Expand Down
4 changes: 4 additions & 0 deletions src/bin/ion/commands/schema/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ impl IonCliCommand for ValidateCommand {
false
}

fn is_porcelain(&self) -> bool {
true // TODO: Should this command be made into plumbing?
}

fn configure_args(&self, command: Command) -> Command {
command
.arg(
Expand Down
4 changes: 4 additions & 0 deletions src/bin/ion/commands/symtab/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ impl IonCliCommand for SymtabFilterCommand {
false
}

fn is_porcelain(&self) -> bool {
false
}

fn configure_args(&self, command: Command) -> Command {
command.with_input()
.with_output()
Expand Down
3 changes: 2 additions & 1 deletion src/bin/ion/commands/symtab/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use crate::commands::command_namespace::IonCliNamespace;
use crate::commands::symtab::filter::SymtabFilterCommand;
use crate::commands::IonCliCommand;

pub mod filter;

pub struct SymtabNamespace;

impl IonCliCommand for SymtabNamespace {
impl IonCliNamespace for SymtabNamespace {
fn name(&self) -> &'static str {
"symtab"
}
Expand Down
4 changes: 4 additions & 0 deletions src/bin/ion/commands/to/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ impl IonCliCommand for ToJsonCommand {
false
}

fn is_porcelain(&self) -> bool {
false
}

fn configure_args(&self, command: Command) -> Command {
// NOTE: it may be necessary to add format-specific options. For example, a "pretty" option
// would make sense for JSON, but not binary formats like CBOR.
Expand Down
3 changes: 2 additions & 1 deletion src/bin/ion/commands/to/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::commands::command_namespace::IonCliNamespace;
use crate::commands::IonCliCommand;

use crate::commands::to::json::ToJsonCommand;
Expand All @@ -6,7 +7,7 @@ pub mod json;

pub struct ToNamespace;

impl IonCliCommand for ToNamespace {
impl IonCliNamespace for ToNamespace {
fn name(&self) -> &'static str {
"to"
}
Expand Down
Loading

0 comments on commit e7d900f

Please sign in to comment.