From 96daaf4021e36f0ac9769dd1bba370d68220313f Mon Sep 17 00:00:00 2001 From: Pascal Hertleif Date: Mon, 15 Jul 2019 19:06:33 +0200 Subject: [PATCH] Add `EnumVariantNames` (#56) * Add `EnumVariantNames` This derive adds a static `variants()` methods yielding the names of the enum variants. This happens to be exactly what clap [wants][1], and is the last puzzle piece to use strum with clap/structopt. [1]: https://docs.rs/clap/2.33.0/clap/macro.arg_enum.html * Expand readme section for `EnumVariantNames` * Return slice instead of array This reduces the risk of breaking the public API by accident when adding a new variant. * Fix typo in Readme * Add generic doc comment to `variants()` method * Add test case using `variants` in clap/structopt --- README.md | 50 ++++++++++++++ strum_macros/Cargo.toml | 1 + strum_macros/src/enum_variant_names.rs | 36 ++++++++++ strum_macros/src/lib.rs | 11 +++ strum_tests/Cargo.toml | 4 +- strum_tests/tests/enum_variant_names.rs | 90 +++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 strum_macros/src/enum_variant_names.rs create mode 100644 strum_tests/tests/enum_variant_names.rs diff --git a/README.md b/README.md index 3b696a3d..031c2c1e 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Strum has implemented the following macros: | [Display](#Display) | Converts enum variants to strings | | [AsRefStr](#AsRefStr) | Converts enum variants to `&'static str` | | [IntoStaticStr](#IntoStaticStr) | Implements `From for &'static str` on an enum | +| [EnumVariantNames](#EnumVariantNames) | Adds a `variants` method returning an array of discriminant names | | [EnumIter](#EnumIter) | Creates a new type that iterates of the variants of an enum. | | [EnumProperty](#EnumProperty) | Add custom properties to enum variants. | | [EnumMessage](#EnumMessage) | Add a verbose message to an enum variant. | @@ -191,6 +192,55 @@ fn main() { } ``` +## EnumVariantNames + +Adds an `impl` block for the `enum` that adds a static `variants()` method that returns an array of `&'static str` that are the discriminant names. +This will respect the `serialize_all` attribute on the `enum` (like `#[strum(serialize_all = "snake_case")]`, see **Additional Attributes** below). + +**Note:** This is compatible with the format [clap](https://docs.rs/clap/2) expects for `enums`, meaning this works: + +```rust +use strum::{EnumString, EnumVariantNames}; + +#[derive(EnumString, EnumVariantNames)] +#[strum(serialize_all = "kebab_case")] +enum Color { + Red, + Blue, + Yellow, + RebeccaPurple, +} + +fn main() { + // This is what you get: + assert_eq!( + &Color::variants(), + &["red", "blue", "yellow", "rebecca-purple"] + ); + + // Use it with clap like this: + let args = clap::App::new("app") + .arg(Arg::with_name("color") + .long("color") + .possible_values(&Color::variants()) + .case_insensitive(true)) + .get_matches(); + + // ... +} +``` + +This also works with [structopt](https://docs.rs/structopt/0.2) (assuming the same definition of `Color` as above): + +```rust +#[derive(Debug, StructOpt)] +struct Cli { + /// The main color + #[structopt(long = "color", default_value = "Color::Blue", raw(possible_values = "&Color::variants()"))] + color: Color, +} +``` + ## EnumIter Iterate over the variants of an Enum. Any additional data on your variants will be set to `Default::default()`. diff --git a/strum_macros/Cargo.toml b/strum_macros/Cargo.toml index af3d40df..65c03dd7 100644 --- a/strum_macros/Cargo.toml +++ b/strum_macros/Cargo.toml @@ -25,6 +25,7 @@ syn = { version = "0.15", features = ["parsing", "extra-traits"] } [features] verbose-enumstring-name = [] verbose-asrefstr-name = [] +verbose-variant-names = [] verbose-asstaticstr-name = [] verbose-intostaticstr-name = [] verbose-tostring-name = [] diff --git a/strum_macros/src/enum_variant_names.rs b/strum_macros/src/enum_variant_names.rs new file mode 100644 index 00000000..d3b36458 --- /dev/null +++ b/strum_macros/src/enum_variant_names.rs @@ -0,0 +1,36 @@ +use proc_macro2::TokenStream; +use syn; + +use case_style::CaseStyle; +use helpers::{convert_case, extract_meta, unique_attr}; + +pub fn enum_variant_names_inner(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + + let variants = match ast.data { + syn::Data::Enum(ref v) => &v.variants, + _ => panic!("EnumVariantNames only works on Enums"), + }; + + // Derives for the generated enum + let type_meta = extract_meta(&ast.attrs); + let case_style = unique_attr(&type_meta, "strum", "serialize_all") + .map(|style| CaseStyle::from(style.as_ref())); + + let names = variants + .iter() + .map(|v| convert_case(&v.ident, case_style)) + .collect::>(); + + quote! { + impl #name { + /// Return a slice containing the names of the variants of this enum + #[allow(dead_code)] + pub fn variants() -> &'static [&'static str] { + &[ + #(#names),* + ] + } + } + } +} diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index a9925efd..9abd193f 100644 --- a/strum_macros/src/lib.rs +++ b/strum_macros/src/lib.rs @@ -22,6 +22,7 @@ mod case_style; mod display; mod enum_count; mod enum_discriminants; +mod enum_variant_names; mod enum_iter; mod enum_messages; mod enum_properties; @@ -65,6 +66,16 @@ pub fn as_ref_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream { toks.into() } +#[cfg_attr(not(feature = "verbose-variant-names"), proc_macro_derive(EnumVariantNames, attributes(strum)))] +#[cfg_attr(feature = "verbose-variant-names", proc_macro_derive(StrumEnumVariantNames, attributes(strum)))] +pub fn variant_names(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse(input).unwrap(); + + let toks = enum_variant_names::enum_variant_names_inner(&ast); + debug_print_generated(&ast, &toks); + toks.into() +} + #[cfg_attr(feature = "verbose-asstaticstr-name", proc_macro_derive(StrumAsStaticStr, attributes(strum)))] #[cfg_attr(not(feature = "verbose-asstaticstr-name"), proc_macro_derive(AsStaticStr, attributes(strum)))] pub fn as_static_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream { diff --git a/strum_tests/Cargo.toml b/strum_tests/Cargo.toml index 9458b9e7..105ca710 100644 --- a/strum_tests/Cargo.toml +++ b/strum_tests/Cargo.toml @@ -5,4 +5,6 @@ authors = ["Peter Glotfelty "] [dependencies] strum = { path = "../strum" } -strum_macros = { path = "../strum_macros", features = [] } \ No newline at end of file +strum_macros = { path = "../strum_macros", features = [] } +clap = "2.33.0" +structopt = "0.2.18" diff --git a/strum_tests/tests/enum_variant_names.rs b/strum_tests/tests/enum_variant_names.rs new file mode 100644 index 00000000..957c14b3 --- /dev/null +++ b/strum_tests/tests/enum_variant_names.rs @@ -0,0 +1,90 @@ +#[macro_use] +extern crate strum_macros; +#[macro_use] +extern crate structopt; +extern crate strum; + +#[test] +fn simple() { + #[allow(dead_code)] + #[derive(EnumVariantNames)] + enum Color { + Red, + Blue, + Yellow, + } + + assert_eq!(&Color::variants(), &["Red", "Blue", "Yellow"]); +} + +#[test] +fn plain_kebab() { + #[allow(dead_code)] + #[derive(EnumVariantNames)] + #[strum(serialize_all = "kebab_case")] + enum Color { + Red, + Blue, + Yellow, + RebeccaPurple, + } + + assert_eq!( + &Color::variants(), + &["red", "blue", "yellow", "rebecca-purple"] + ); +} + +#[test] +fn non_plain_camel() { + #[allow(dead_code)] + #[derive(EnumVariantNames)] + #[strum(serialize_all = "kebab_case")] + enum Color { + DeepPink, + GreenYellow, + CornflowerBlue, + Other { r: u8, g: u8, b: u8 }, + } + + assert_eq!( + &Color::variants(), + &["deep-pink", "green-yellow", "cornflower-blue", "other"] + ); +} + +#[test] +fn clap_and_structopt() { + #[derive(Debug, EnumString, EnumVariantNames)] + #[strum(serialize_all = "kebab_case")] + enum Color { + Red, + Blue, + Yellow, + RebeccaPurple, + } + + assert_eq!( + &Color::variants(), + &["red", "blue", "yellow", "rebecca-purple"] + ); + + let _clap_example = clap::App::new("app").arg( + clap::Arg::with_name("color") + .long("color") + .possible_values(Color::variants()) + .case_insensitive(true), + ); + + #[derive(Debug, StructOpt)] + #[allow(unused)] + struct StructOptExample { + /// The main color + #[structopt( + long = "color", + default_value = "Color::Blue", + raw(possible_values = "Color::variants()") + )] + color: Color, + } +}