Search code examples
rustclap

How to use an internal library Enum for Clap Args


I am currently working on a Rust port of a security tool. Inline with Rust's guides, I want to segregate the core library into its own crate, so that we can create various tools (CLI, API, streams etc.) that interface with with the core library without coupling them together.

The core library exposes two public Enums, one of them being the PermutationMode (truncated):

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum PermutationMode {
    All,
    Addition,
    BitSquatting,
    Homoglyph,
}

When creating a CLI utility using Clap, I would like to extend this library Enum as part of the CLI like so:

use clap::Clap;

use twistrs::permutate::PermutationMode;

#[derive(Clap, PartialEq, Debug)]
#[clap(name = "twistrs-cli")]
struct Opts {
    #[clap(short, long)]
    registered_domains: bool,

    #[clap(arg_enum)]
    permutation_mode: PermutationMode,
}

So that when calling the CLI, we can pass the permutation mode from the user, to the CLI, to the library seamlessly and without having the CLI needing to be aware of the internal modes (in the event that the library adds more).

./twist-cli --registered-domains --permutation_mode=all example.com

Currently this does not seem to be possible (which makes sense). One attempt was to use type aliasing:

#[derive(Clap)]
type ArgPermutationMode = PermutationMode

However we cannot use derive macros for type-aliases. I tried also "cloning" the enum and trying to map to the libraries enum:

enum ArgPermutationMode {
    PermutationMode::All,
}

Which does not compile.


Question - Is it possible to extend an internal library Enum to use it as a Clap argument?


Solution

  • Unfortunately not. You would have to redefine the enum so that the arg_enum! macro can access the tokens.

    If you add a conversion function between the two then you can make sure that upstream changes to the library enum force you to update your CLI, by giving you a compilation error:

    arg_enum! {
        enum ArgPermutationMode {
            All,
            Addition,
            BitSquatting,
            Homoglyph,
        }
    }
    
    impl From<ArgPermutationMode> for PermutationMode {
        fn from(other: ArgPermutationMode) -> PermutationMode {
             match other {
                 ArgPermutationMode::All => PermutationMode::All,
                 ArgPermutationMode::Addition => PermutationMode::Addition,
                 ArgPermutationMode::BitSquatting => PermutationMode::BitSquatting,
                 ArgPermutationMode::Homoglyph => PermutationMode::Homoglyph,
             }
        }
    }
    
    impl From<PermutationMode> for ArgPermutationMode {
        fn from(other: PermutationMode) -> ArgPermutationMode {
             match other {
                 PermutationMode::All => ArgPermutationMode::All,
                 PermutationMode::Addition => ArgPermutationMode::Addition,
                 PermutationMode::BitSquatting => ArgPermutationMode::BitSquatting,
                 xPermutationMode::Homoglyph => ArgPermutationMode::Homoglyph,
             }
        }
    }
    

    You can reduce that boilerplate with a macro if you find yourself doing it a lot.


    Given that you have control over the other crate, you could compromise by trying one of a few other options for a workaround:

    • Define the actual enum variants in a separate file, and use include! to use the same source in both crates. This assumes your crates are in the same workspace.
    • Use a macro derive like EnumIter from strum_macros. This will let you iterate over the enum's variants so you can supply them to Clap, without having a Clap dependency in that crate. You'll have a strum_macros dependency instead, so it's up to you if that is really better.
    • Add the clap_args! call in the internal crate, but feature-gate it. Your application crate can enable this feature, but most users wouldn't.