Creating Derive Macro to Allow Parent-Child Struct References?

I have a fairly simple pattern that I have been using for building CLI applications using clap 4, but I'd like to improve it and maybe release it as a library if it proves useful to anyone.

I'm defining an async trait (using async_trait) to define what a CLI application is, and if possible, it would be very nice to be able to reference a parent command in order to access its fields. So far, I have been able to get away without doing this, but I've been wondering if it's possible perhaps now that we have generic associated types.

Here is my trait and a demo implementation as a demonstration. I did not check that the compiler can indeed compile this, so consider it pseudocode.

lib.rs

pub(crate) mod echo;

use async_trait::async_trait;
use clap::Parser;

#[async_trait]
pub trait CliCommand {
    async fn execute(&self) -> anyhow::Result<()>;
}

// an example implementation of the root command, ie entrypoint to the app
#[derive(Debug, Parser)]
pub struct App {
    #[arg(short = 'v')]
    pub verbose: bool,
    #[command(subcommands)]
    pub cmd: RootSubcommands,
}

#[async_trait]
impl CliCommand for App {
    async fn execute(&self) -> anyhow::Result<()> {
        // when the root command is executed, run any logic you'd like here, then
        // dispatch to the child command
        match &self.cmd {
            RootSubcommands::Echo(c) => c.execute().await,
        }
    }
}

#[derive(Debug, Parser)]
pub enum RootSubcommands {
    Echo(echo::EchoCommand),
}

And now for the definition of the echo command:

echo.rs

use crate::CliCommand;

use async_trait::async_trait;
use clap::Parser;

#[derive(Debug, Parser)]
pub struct EchoCommand {
    pub value: String,
}

#[async_trait]
impl CliCommand for EchoCommand {
    async fn execute(&self) -> anyhow::Result<()> {
        println!("{}", self.value);
        
        Ok(())
    }
}

Hopefully so far this is highly legible and intelligible.

  • There is a CliCommand trait which is an async_trait which defines an async execute function which does whatever the command needs to do and returns an anyhow::Result<()>.
  • For commands with subcommands, this usually breaks down into a match block, as the subcommand is an enum containing other CliCommand implementations, and execute is called on them if they are the specified command.
  • For now, anyhow::Result<()> is good enough for me but I may make a custom error which allows optionally specifying a return code.
  • The outcome of this pattern allows me to have commands as nested as I could possibly imagine, I've built really awesome utilities for things like program crypto generate ed25519 -f json. Each command by virtue of clap is self-discoverable, just pass -h and explore the full command hierarchy.

Now here is where the pain points begin.

Manual Implementation of Subcommand Dispatch

Manually writing match blocks feels super tedious. I feel like I could add other trait methods with default implementations which would allow (perhaps through code generation/derive macros) any struct with a #[command(subcommand)] field to automatically dispatch to the appropriate enum member so that I wouldn't need to define any functionality at all if I didn't need to. I could probably add hook functions like before and after to allow adding custom logic before and after execution if need be.

Q: Could I build a derive macro that could check for presence of the #[command(subcommand)] attribute on fields within a struct, and if these are present, automatically generate the match block in the implementation of the execute function by looking at the referenced enum's variants?

Related: is there a way to define a type constraint where each of an enum's variants must contain a struct that implements a given trait?

Giving Child Commands Strictly-Typed References to the Parent Command in execute

I also need help with the type system to determine if it is possible to optionally include a reference to the parent struct within the execute method so that each subcommand would receive execute(&self, parent: &ParentCommand) so that if desired, they could grab config parameters from the parent using well-defined types.

I could probably define a trait hierarchy which would share some code between all commands but distinguish between root commands (which have no parent) and child commands (which do have a parent).

Perhaps, this could look like:

  • trait CliCommand
  • trait RootCliCommand: CliCommand
  • trait ChildCliCommand: CliCommand

Q: Is it possible using traits or generics (or GATs) for strongly typing references to the parent command within the execute function? For example async fn execute(&self, parent: &ParentCommand)?

I assume that actually storing a reference to the parent in the child struct would be a no-go for many reasons, so a temporary pass of a reference to the parent makes the most sense within a method call.

sure, procedural macros can do this. I also recommend trait_enum crate, you can check out.

not to my knowledge.

not 100% sure, but I feel like it's doable. however, if sub-command is a field of parent, you are limited to only shared ref &self but not exclusive ref &mut self receiver, which may or may not be a problem.

I sense an OO smell here. the OO thinking that "objects" are encapsulated and can be independently developed/tested/reused is mostly an illusion. in reality you always need context to do something, which in OO worlds means you need reference to other "objects".

you want your sub command are "self-contained", but then you want sub commands to have access to "parent" command. they are not as self-contained as you might wanted after all. in such cases, I think it's better suited to have different methods on the parent command instead of "delegating" to the execute() method of sub commands:

// instead of:
impl ParentCommand {
    fn execute(&self) {
        match self.sub_command {
            SubCommand::A(cmd) => cmd.execute(self),
            SubCommand::B(cmd) => cmd.execute(self),
        }
    }
}
// I would prefer
impl ParentCommand {
    fn execute(&self) {
        match self.sub_command {
            SubCommand::A(opts) => self.do_a(opts)
            SubCommand::B(opts) => self.do_b(opts),
        }
    }
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.