diff --git a/Cargo.lock b/Cargo.lock index 27dbdba..72ca9c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "kinded" version = "0.4.0" @@ -53,6 +59,27 @@ dependencies = [ "kinded", ] +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.28" @@ -69,6 +96,7 @@ name = "test_suite" version = "0.1.0" dependencies = [ "kinded", + "strum", ] [[package]] diff --git a/README.md b/README.md index a660931..439f80a 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,34 @@ let mut drink_kinds = HashSet::new(); drink_kinds.insert(DrinkKind::Mate); ``` +#### Variant Attributes + +Some traits require or allow additional configuration via attributes on variants. +These can be passed through using `#[kinded(...)]`. +You can only pass one attribute per `#[kinded(...)]`, though you can use multiple of these attributes per variant. + +```rs +use kinded::Kinded; + +#[derive(Kinded)] +#[kinded(derive(Default, Hash, Default))] +enum Drink { + #[kinded(default)] + #[kinded(doc = "Not suitable for small children. Please talk to your IT-person about the harmful effects of mate addiction.")] + Mate, + #[kinded(doc = "Not suitable for small children!")] + Coffee(String), + #[kinded(default)] + #[kinded(doc = "Only suitable for small children if caffeine-free.")] + Tea { variety: String, caffeine: bool } +} + +// Now we can access default without having to implement it ourself. +// Also the drink kinds now have a small doc! +let default_drink_kind = DrinkKind::default(); +``` +This can also be used to configure derivations of [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) and [`Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html) of [`serde`](https://docs.rs/serde/latest/serde/index.html) or +[`EnumMessage`](https://docs.rs/strum/latest/strum/trait.EnumMessage.html) from [`strum`](https://docs.rs/strum/latest/strum/) via `#[kinded(strum(message = "Important message"))]` ### Display trait Implementation of `Display` trait can be customized in the `serde` fashion: diff --git a/kinded_macros/src/generate/kind_enum.rs b/kinded_macros/src/generate/kind_enum.rs index d0e0ded..49b2c1e 100644 --- a/kinded_macros/src/generate/kind_enum.rs +++ b/kinded_macros/src/generate/kind_enum.rs @@ -22,12 +22,18 @@ fn gen_definition(meta: &Meta) -> TokenStream { let vis = &meta.vis; let kind_name = meta.kind_name(); let variant_names: Vec<&Ident> = meta.variants.iter().map(|v| &v.ident).collect(); + let variant_attr: Vec<&[TokenStream]> = meta + .variants + .iter() + .map(|v| v.kinded_variant_attrs.as_slice()) + .collect(); let traits = meta.derive_traits(); quote!( - #[derive(#(#traits),*)] // #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[derive(#(#traits),*)] // #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #vis enum #kind_name { // pub enum DrinkKind { - #(#variant_names),* // Mate, Coffee, Tea + #( #(#[#variant_attr])* // + #variant_names),* // Mate, Coffee, #[default] Tea } // } impl #kind_name { // impl DrinkKind { diff --git a/kinded_macros/src/models.rs b/kinded_macros/src/models.rs index e41e6e3..4711c3a 100644 --- a/kinded_macros/src/models.rs +++ b/kinded_macros/src/models.rs @@ -61,6 +61,10 @@ impl Meta { pub struct Variant { pub ident: Ident, pub fields_type: FieldsType, + /// Attributes specified with `#[kinded(CONTENT)]` above the variants of the original enum. + /// The token streams in here only contain the `CONTENT` from the example above. + /// These can be pasted as is into the kind enum. + pub kinded_variant_attrs: Vec, } /// This mimics syn::Fields, but without payload. diff --git a/kinded_macros/src/parse.rs b/kinded_macros/src/parse.rs index b30b060..d841ae1 100644 --- a/kinded_macros/src/parse.rs +++ b/kinded_macros/src/parse.rs @@ -38,6 +38,7 @@ fn parse_variant(variant: &syn::Variant) -> Variant { Variant { ident: variant.ident.clone(), fields_type: parse_fields_type(&variant.fields), + kinded_variant_attrs: parse_kinded_variant_attrs(&variant), } } @@ -49,6 +50,30 @@ fn parse_fields_type(fields: &syn::Fields) -> FieldsType { } } +/// This function takes a variant and returns a vector of token stream. +/// The token streams were previously the content of a `#[kinded(...)]` attribute on this variant. +/// Other attributes are ignored as they are (probably) meant for the main enum. +/// +/// Multiple `#[kinded(...)]` attributes can be present on one variant, therefore a vector will collect them. +fn parse_kinded_variant_attrs(variant: &'_ syn::Variant) -> Vec { + variant + .attrs + .iter() + .filter_map(|attr| { + // Only keep attributes of the form `path(...)` + match &attr.meta { + syn::Meta::List(meta_list) => Some(meta_list), + _ => None, + } + }) + .filter_map(|meta_list| + // and only if the path is "kinded" + // then will probably save a teeny tiny amount of time + // as it skips the cloning in cases were the path does not match. + meta_list.path.is_ident("kinded").then(|| meta_list.tokens.clone())) + .collect() +} + /// Find `#[kinded(..)]` attribute on the enum. fn find_kinded_attr(input: &DeriveInput) -> Result, syn::Error> { let kinded_attrs: Vec<_> = input diff --git a/test_suite/Cargo.toml b/test_suite/Cargo.toml index 14c37b8..e9aaf38 100644 --- a/test_suite/Cargo.toml +++ b/test_suite/Cargo.toml @@ -7,3 +7,4 @@ edition = "2024" [dependencies] kinded = { path = "../kinded" } +strum = { version = "0.27.2", features = ["derive"] } diff --git a/test_suite/src/lib.rs b/test_suite/src/lib.rs index fe35949..52cd47c 100644 --- a/test_suite/src/lib.rs +++ b/test_suite/src/lib.rs @@ -5,6 +5,56 @@ extern crate alloc; use kinded::Kinded; +mod variant_attributes { + //! This test module uses the [`EnumMessage`]-Trait because if variant attributes do not work, + //! the derive-macro will still finish successfully. + //! The corresponding methods will just return `None`. + + use super::*; + use alloc::string::String; + use strum::EnumMessage; + + #[derive(Kinded)] + #[kinded(derive(Hash, Default, EnumMessage))] + enum Drink { + #[kinded(doc = "Not suitable for small children. Please talk to your IT-person about the harmful effects of mate addiction.")] + #[kinded(strum( + message = "Made from fermented leaves.", + detailed_message="Caffeinated beverage from South America. Not suitable for small children because of caffeine." + ))] + Mate, + #[kinded(doc = "Not suitable for small children!")] + #[kinded(strum( + message = "Made from roasted, ground beans.", + detailed_message="Beverage made from roasted, ground beans that originated somewhere around the read sea. Not suitable for small children, especially Espresso." + ))] + Coffee(String), + #[kinded(default)] + #[kinded(doc = "Only suitable for small children if caffeine-free.")] + #[kinded(strum( + message = "Made from fermented leaves.", + detailed_message="Beverage made from fermented leaves that originated from china. Some contain caffeine and are not suitable for small children." + ))] + Tea { variety: String, caffeine: bool } + } + #[test] + pub fn test_message() { + let mate = DrinkKind::Mate; + assert_eq!(Some("Made from fermented leaves."), mate.get_message()); + assert_eq!(Some("Caffeinated beverage from South America. Not suitable for small children because of caffeine."), mate.get_detailed_message()); + assert_eq!(Some("Not suitable for small children. Please talk to your IT-person about the harmful effects of mate addiction."), mate.get_documentation()); + + let coffee = DrinkKind::Coffee; + assert_eq!(Some("Made from roasted, ground beans."), coffee.get_message()); + assert_eq!(Some("Beverage made from roasted, ground beans that originated somewhere around the read sea. Not suitable for small children, especially Espresso."), coffee.get_detailed_message()); + assert_eq!(Some("Not suitable for small children!"), coffee.get_documentation()); + + let tea = DrinkKind::Tea; + assert_eq!(Some("Made from fermented leaves."), tea.get_message()); + assert_eq!(Some("Beverage made from fermented leaves that originated from china. Some contain caffeine and are not suitable for small children."), tea.get_detailed_message()); + assert_eq!(Some("Only suitable for small children if caffeine-free."), tea.get_documentation()); + } +} #[derive(Kinded)] enum Role {