Skip to content

Commit

Permalink
provide nicer interface for cli extensions
Browse files Browse the repository at this point in the history
this relieves the burden of implementing `argh::DynamicSubCommand` from
the user of `fud_core`. Now a user can simply forward the parsing
implementations for standard `argh::SubCommand`s to extend the fud cli.
  • Loading branch information
sgpthomas committed Aug 16, 2024
1 parent 35e2a37 commit 0064a13
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 135 deletions.
59 changes: 14 additions & 45 deletions fud2/fud-core/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::cli_ext::EmptyCliExt;
pub use crate::cli_ext::{CliExt, FakeCli, FromArgFn, RedactArgFn};
use crate::config;
use crate::exec::{plan, Driver, Request, StateRef};
use crate::run::Run;
use anyhow::{anyhow, bail};
use argh::{DynamicSubCommand, FromArgs};
use argh::FromArgs;
use camino::Utf8PathBuf;
use std::fmt::Display;
use std::fmt::{Debug, Display};
use std::str::FromStr;

enum Mode {
Expand Down Expand Up @@ -104,45 +106,12 @@ pub struct GetResource {
#[argh(subcommand, name = "list")]
pub struct ListCommand {}

pub trait FakeCliExt: DynamicSubCommand {
fn run(&self, driver: &Driver) -> anyhow::Result<()>;
}

/// no extra command
struct EmptyCliExt {}

impl DynamicSubCommand for EmptyCliExt {
fn commands() -> &'static [&'static argh::CommandInfo] {
&[]
}

fn try_redact_arg_values(
_command_name: &[&str],
_args: &[&str],
) -> Option<Result<Vec<String>, argh::EarlyExit>> {
None
}

fn try_from_args(
_command_name: &[&str],
_args: &[&str],
) -> Option<Result<Self, argh::EarlyExit>> {
None
}
}

impl FakeCliExt for EmptyCliExt {
fn run(&self, _driver: &Driver) -> anyhow::Result<()> {
Ok(())
}
}

/// supported subcommands
#[derive(FromArgs, PartialEq, Debug)]
#[derive(FromArgs)]
#[argh(subcommand)]
pub enum Subcommand<T>
where
T: FakeCliExt,
T: CliExt,
{
/// edit the configuration file
EditConfig(EditConfig),
Expand All @@ -154,14 +123,14 @@ where
List(ListCommand),

#[argh(dynamic)]
Extended(T),
Extended(FakeCli<T>),
}

#[derive(FromArgs)]
/// A generic compiler driver.
pub struct FakeArgs<T>
where
T: FakeCliExt,
T: CliExt,
{
#[argh(subcommand)]
pub sub: Option<Subcommand<T>>,
Expand Down Expand Up @@ -247,7 +216,7 @@ fn get_states_with_errors(
Ok(states)
}

fn from_states<T: FakeCliExt>(
fn from_states<T: CliExt>(
driver: &Driver,
args: &FakeArgs<T>,
) -> anyhow::Result<Vec<StateRef>> {
Expand All @@ -261,7 +230,7 @@ fn from_states<T: FakeCliExt>(
)
}

fn to_state<T: FakeCliExt>(
fn to_state<T: CliExt>(
driver: &Driver,
args: &FakeArgs<T>,
) -> anyhow::Result<Vec<StateRef>> {
Expand All @@ -275,7 +244,7 @@ fn to_state<T: FakeCliExt>(
)
}

fn get_request<T: FakeCliExt>(
fn get_request<T: CliExt>(
driver: &Driver,
args: &FakeArgs<T>,
) -> anyhow::Result<Request> {
Expand Down Expand Up @@ -367,7 +336,7 @@ pub fn config_from_cli(name: &str) -> anyhow::Result<figment::Figment> {
config_from_cli_ext::<EmptyCliExt>(name)
}

pub fn config_from_cli_ext<T: FakeCliExt>(
pub fn config_from_cli_ext<T: CliExt>(
name: &str,
) -> anyhow::Result<figment::Figment> {
let args: FakeArgs<T> = argh::from_env();
Expand All @@ -391,7 +360,7 @@ pub fn cli(driver: &Driver, config: &figment::Figment) -> anyhow::Result<()> {
cli_ext::<EmptyCliExt>(driver, config)
}

pub fn cli_ext<T: FakeCliExt>(
pub fn cli_ext<T: CliExt>(
driver: &Driver,
config: &figment::Figment,
) -> anyhow::Result<()> {
Expand All @@ -416,7 +385,7 @@ pub fn cli_ext<T: FakeCliExt>(
return Ok(());
}
Some(Subcommand::Extended(cmd)) => {
return cmd.run(driver);
return cmd.0.run(driver);
}
None => {}
}
Expand Down
140 changes: 140 additions & 0 deletions fud2/fud-core/src/cli_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use std::sync::OnceLock;

use crate::Driver;

/// Fn type representing the redact_arg function required for implementing `argh::FromArgs`
pub type RedactArgFn =
fn(&[&str], &[&str]) -> Result<Vec<String>, argh::EarlyExit>;

/// Fn type representing the from_arg function required for implementing `argh::FromArgs`
pub type FromArgFn<T> = fn(&[&str], &[&str]) -> Result<T, argh::EarlyExit>;

/// Trait for extending the cli provided by `fud_core::cli`.
///
/// Below is an example of how to use this trait to add a subcommand named `test` to
/// the `fud_core` cli.
///
/// ```rust
/// /// some test command
/// #[derive(FromArgs)]
/// #[argh(subcommand, name = "test")]
/// pub struct TestCommand {
/// /// some arg
/// #[argh(positional)]
/// arg: String,
/// }
///
/// pub enum TestExt {
/// Test(TestCommand)
/// }
///
/// impl CliExt for Fud2CliExt {
/// fn run(&self, driver: &fud_core::Driver) -> anyhow::Result<()> {
/// match &self {
/// Fud2CliExt::Test(cmd) => {
/// println!("hi there: {}", cmd.arg);
/// Ok(())
/// }
/// }
/// }
///
/// fn inner_command_info() -> Vec<CommandInfo> {
/// vec![CommandInfo {
/// name: "test",
/// description: "test command",
/// }]
/// }
///
/// fn inner_redact_arg_values() -> Vec<(&'static str, RedactArgFn)> {
/// vec![("test", TestCommand::redact_arg_values)]
/// }
///
/// fn inner_from_args() -> Vec<(&'static str, FromArgFn<Self>)> {
/// vec![("test", |cmd_name, args| {
/// TestCommand::from_args(cmd_name, args).map(Self::Test)
/// })]
/// }
/// }
/// ```
pub trait CliExt: Sized {
/// Action to execute when this subcommand is provided to the cli
fn run(&self, driver: &Driver) -> anyhow::Result<()>;

/// Provides the command names and descriptions for all subcommands in the cli
/// extension.
fn inner_command_info() -> Vec<argh::CommandInfo>;

/// Forward `redact_arg_values` parsing of subcommands to `fud_core::cli` parsing.
fn inner_redact_arg_values() -> Vec<(&'static str, RedactArgFn)>;

/// Forward `from_args` parsing of subcommands to `fud_core::cli` parsing.
fn inner_from_args() -> Vec<(&'static str, FromArgFn<Self>)>;
}

/// Wrapper type over types that implement `CliExt`. This is needed so that we can
/// implement the foreign trait `argh::DynamicSubCommand` on a user provided `CliExt`.
pub struct FakeCli<T: CliExt>(pub T);

impl<T: CliExt> argh::DynamicSubCommand for FakeCli<T> {
fn commands() -> &'static [&'static argh::CommandInfo] {
static RET: OnceLock<Vec<&'static argh::CommandInfo>> = OnceLock::new();
RET.get_or_init(|| {
T::inner_command_info()
.into_iter()
.map(|cmd_info| &*Box::leak(Box::new(cmd_info)))
.collect()
})
}

fn try_redact_arg_values(
command_name: &[&str],
args: &[&str],
) -> Option<Result<Vec<String>, argh::EarlyExit>> {
for (reg_name, f) in T::inner_redact_arg_values() {
if let Some(&name) = command_name.last() {
if name == reg_name {
return Some(f(command_name, args));
}
}
}
None
}

fn try_from_args(
command_name: &[&str],
args: &[&str],
) -> Option<Result<Self, argh::EarlyExit>> {
for (reg_name, f) in T::inner_from_args() {
if let Some(&name) = command_name.last() {
if name == reg_name {
return Some(f(command_name, args).map(FakeCli));
}
}
}
None
}
}

/// The default CliExt used if none is provided. This doesn't define any new
/// commands.
pub struct EmptyCliExt {}

impl CliExt for EmptyCliExt {
fn inner_command_info() -> Vec<argh::CommandInfo> {
vec![]
}

fn inner_redact_arg_values(
) -> Vec<(&'static str, crate::cli_ext::RedactArgFn)> {
vec![]
}

fn inner_from_args() -> Vec<(&'static str, crate::cli_ext::FromArgFn<Self>)>
{
vec![]
}

fn run(&self, _driver: &Driver) -> anyhow::Result<()> {
Ok(())
}
}
1 change: 1 addition & 0 deletions fud2/fud-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod cli;
mod cli_ext;
pub mod config;
pub mod exec;
pub mod run;
Expand Down
Loading

0 comments on commit 0064a13

Please sign in to comment.