Trait Type Constraints for Generic Errors

⚓ rust    📅 2025-05-17    👤 surdeus    👁️ 5      

surdeus

Warning

This post was published 43 days ago. The information described in this article may have changed.

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.

3 posts - 2 participants

Read full topic

🏷️ rust_feed