Trait Type Constraints for Generic Errors

I'm trying to finally flesh out something I've used in many projects in the past, a way to build hierarchies of CLI commands. The thought is to provide a CLI interface like this:

# operations on tasks
my-util task create
my-util task list
my-util task delete
# operations on task comments
my-util task comment create

I'm using clap with derive, but in my crate that I'm working on, that's not going to be a requirement. Usage with clap makes things really easy, from your main function you simply call Args::parse().run() and everything should work as you might expect.

The basic form of the Command trait looks like this:

pub trait Command {

    fn pre_exec(&self) -> sysexits::Result<()> {
        Ok(())
    }

    fn exec(&self) -> sysexits::Result<()>;

    fn post_exec(&self, exec_outcome: sysexits::Result<()>) -> sysexits::Result<()> {
        exec_outcome
    }

    fn run(&self) -> sysexits::Result<()> {
        self.pre_exec()?;

        let outcome = self.exec();

        self.post_exec(outcome)
    }
}

Building hierarchies of commands looks like this:

use clap::Parser;

#[derive(Debug, Clone, Parser)]
pub struct RootCommand {
    #[clap(subcommand)]
    pub cmd: SubCommands,
}

#[derive(Debug, Clone, Parser)]
pub enum SubCommands {
   Tasks(TasksCommand),
}

impl Command for RootCommand {
    fn exec(&self) -> sysexits::Result<()> {
        match &self.cmd {
            SubCommands::Tasks(c) => c.run(),
        }
    }
}

#[derive(Debug, Clone, Parser)]
pub struct TasksCommand;

impl Command for TasksCommand {
    fn exec(&self) -> sysexits::Result<()> {
        eprintln!("tasks command!");
        Ok(())
    }
}

This works fine, but since I'm now making this into a crate, I'd like the error type to be user-supplied so people can use whatever error type they'd like:

  • anyhow makes things super easy if you don't care much about strict error types
  • sysexits makes sense if you want explicit exit codes for certain conditions
  • thiserror allows you to be very specific about your errors
  • std::error::Error for masochists

Now that I'm adding a type to Command, I'm not sure how to specify the constraint so that it plays nicely with whatever you want to be using.

I've tried:

pub trait Command {
    type Err: AsRef<dyn std::error::Error>;
}

This works for anyhow but does not work for sysexits.

Should I just abandon adding a constraint on the Err type? Or is there a better way to constrain the type that will play nicely with most error crates?

Users will pretty much never need to have a dyn Command as everything will be done statically and I'll be writing a proc-macro to automate away the match block stuff for parent commands.

Well, what do you need the constraint for? What code are you trying to provide in the library that needs certain capabilities from the error type? I don't think the code you've shared has any requirements on the error type beyond Sized.

(I do believe that all the specific types you mentioned implement Into<Box<dyn Error>> for whatever that is worth.)

Actually, yeah that's fair. I don't technically need Err to be anything at all.

If FromStr can do it like this:

pub trait FromStr: Sized {
    type Err;

    // Required method
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

Then I can absolutely just not constrain it.