From 3aed72b3e265ce45475e2fef7189ff6f6338da55 Mon Sep 17 00:00:00 2001 From: Oren Ben-kiki Date: Mon, 10 Jan 2022 11:53:17 +0200 Subject: [PATCH 1/3] Add get_documentation() to EnumMessage. --- strum/src/additional_attributes.rs | 2 ++ strum/src/lib.rs | 2 ++ strum_macros/Cargo.toml | 2 +- strum_macros/src/helpers/metadata.rs | 14 +++++++++++-- strum_macros/src/helpers/variant_props.rs | 4 ++++ strum_macros/src/lib.rs | 11 ++++++++++- strum_macros/src/macros/enum_messages.rs | 21 +++++++++++++++++++- strum_tests/tests/enum_message.rs | 24 +++++++++++++++++++++++ 8 files changed, 75 insertions(+), 5 deletions(-) diff --git a/strum/src/additional_attributes.rs b/strum/src/additional_attributes.rs index 399ca320..fb66fa48 100644 --- a/strum/src/additional_attributes.rs +++ b/strum/src/additional_attributes.rs @@ -88,4 +88,6 @@ //! - `detailed_message=".."`: Adds a more detailed message to a variant. If this value is omitted, then //! `message` will be used in it's place. //! +//! - Structured documentation, as in `/// ...`: If using `EnumMessage`, is accessible via get_documentation(). +//! //! - `props(key="value")`: Enables associating additional information with a given variant. diff --git a/strum/src/lib.rs b/strum/src/lib.rs index 912cdb0f..d411972c 100644 --- a/strum/src/lib.rs +++ b/strum/src/lib.rs @@ -113,6 +113,7 @@ pub trait IntoEnumIterator: Sized { /// #[strum(message="I have a dog")] /// #[strum(detailed_message="My dog's name is Spots")] /// Dog, +/// /// I am documented. /// #[strum(message="I don't have a cat")] /// Cat, /// } @@ -123,6 +124,7 @@ pub trait IntoEnumIterator: Sized { pub trait EnumMessage { fn get_message(&self) -> Option<&'static str>; fn get_detailed_message(&self) -> Option<&'static str>; + fn get_documentation(&self) -> Option<&'static str>; fn get_serializations(&self) -> &'static [&'static str]; } diff --git a/strum_macros/Cargo.toml b/strum_macros/Cargo.toml index 140f2e9e..c9569b69 100644 --- a/strum_macros/Cargo.toml +++ b/strum_macros/Cargo.toml @@ -26,4 +26,4 @@ rustversion = "1.0" syn = { version = "1.0", features = ["parsing", "extra-traits"] } [dev-dependencies] -strum = "0.20" +strum = { path = "../strum" } diff --git a/strum_macros/src/helpers/metadata.rs b/strum_macros/src/helpers/metadata.rs index 6279e6a5..c5a87123 100644 --- a/strum_macros/src/helpers/metadata.rs +++ b/strum_macros/src/helpers/metadata.rs @@ -5,7 +5,7 @@ use syn::{ parse2, parse_str, punctuated::Punctuated, spanned::Spanned, - Attribute, DeriveInput, Ident, LitBool, LitStr, Path, Token, Variant, Visibility, + Attribute, DeriveInput, Ident, Lit, LitBool, LitStr, Meta, MetaNameValue, Path, Token, Variant, Visibility, }; use super::case_style::CaseStyle; @@ -164,6 +164,9 @@ pub enum VariantMeta { kw: kw::serialize, value: LitStr, }, + Documentation { + value: LitStr, + }, ToString { kw: kw::to_string, value: LitStr, @@ -253,6 +256,7 @@ impl Spanned for VariantMeta { match self { VariantMeta::Message { kw, .. } => kw.span, VariantMeta::DetailedMessage { kw, .. } => kw.span, + VariantMeta::Documentation { value } => value.span(), VariantMeta::Serialize { kw, .. } => kw.span, VariantMeta::ToString { kw, .. } => kw.span, VariantMeta::Disabled(kw) => kw.span, @@ -270,7 +274,13 @@ pub trait VariantExt { impl VariantExt for Variant { fn get_metadata(&self) -> syn::Result> { - get_metadata_inner("strum", &self.attrs) + let result = get_metadata_inner("strum", &self.attrs)?; + self.attrs.iter().filter(|attr| attr.path.is_ident("doc")).try_fold(result, |mut vec, attr| { + if let Meta::NameValue(MetaNameValue { lit: Lit::Str(value), .. }) = attr.parse_meta()? { + vec.push(VariantMeta::Documentation { value }) + } + Ok(vec) + }) } } diff --git a/strum_macros/src/helpers/variant_props.rs b/strum_macros/src/helpers/variant_props.rs index a7e94862..f51fc201 100644 --- a/strum_macros/src/helpers/variant_props.rs +++ b/strum_macros/src/helpers/variant_props.rs @@ -16,6 +16,7 @@ pub struct StrumVariantProperties { pub ascii_case_insensitive: Option, pub message: Option, pub detailed_message: Option, + pub documentation: Vec, pub string_props: Vec<(LitStr, LitStr)>, serialize: Vec, to_string: Option, @@ -85,6 +86,9 @@ impl HasStrumVariantProperties for Variant { detailed_message_kw = Some(kw); output.detailed_message = Some(value); } + VariantMeta::Documentation { value } => { + output.documentation.push(value); + } VariantMeta::Serialize { value, .. } => { output.serialize.push(value); } diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index 73185fe9..1b0eb236 100644 --- a/strum_macros/src/lib.rs +++ b/strum_macros/src/lib.rs @@ -479,6 +479,7 @@ pub fn from_repr(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// #[derive(strum_macros::EnumMessage, Debug)] /// #[allow(dead_code)] /// enum Color { +/// /// Danger color. /// #[strum(message = "Red", detailed_message = "This is very red")] /// Red, /// #[strum(message = "Simply Green")] @@ -506,6 +507,13 @@ pub fn from_repr(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// } /// } /// +/// fn get_documentation(&self) -> ::std::option::Option<&'static str> { +/// match self { +/// &Color::Red => ::std::option::Option::Some("Danger color."), +/// _ => None +/// } +/// } +/// /// fn get_serializations(&self) -> &'static [&'static str] { /// match self { /// &Color::Red => { @@ -528,6 +536,7 @@ pub fn from_repr(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// let c = Color::Red; /// assert_eq!("Red", c.get_message().unwrap()); /// assert_eq!("This is very red", c.get_detailed_message().unwrap()); +/// assert_eq!("Danger color.", c.get_documentation().unwrap()); /// assert_eq!(["Red"], c.get_serializations()); /// ``` #[proc_macro_derive(EnumMessage, attributes(strum))] @@ -613,7 +622,7 @@ pub fn enum_properties(input: proc_macro::TokenStream) -> proc_macro::TokenStrea /// // Bring trait into scope /// use std::str::FromStr; /// use strum::{IntoEnumIterator, EnumMessage}; -/// use strum_macros::{EnumDiscriminants, EnumIter, EnumString, EnumMessage}; +/// use strum_macros::{EnumDiscriminants, EnumIter, EnumString}; /// /// #[derive(Debug)] /// struct NonDefault; diff --git a/strum_macros/src/macros/enum_messages.rs b/strum_macros/src/macros/enum_messages.rs index 6e599d02..efa6f8d7 100644 --- a/strum_macros/src/macros/enum_messages.rs +++ b/strum_macros/src/macros/enum_messages.rs @@ -17,12 +17,14 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result { let mut arms = Vec::new(); let mut detailed_arms = Vec::new(); + let mut documentation_arms = Vec::new(); let mut serializations = Vec::new(); for variant in variants { let variant_properties = variant.get_variant_properties()?; let messages = variant_properties.message.as_ref(); let detailed_messages = variant_properties.detailed_message.as_ref(); + let documentation = &variant_properties.documentation; let ident = &variant.ident; use syn::Fields::*; @@ -65,10 +67,17 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result { if let Some(msg) = detailed_messages { let params = params.clone(); - // Push the simple message. + // Push the detailed message. detailed_arms .push(quote! { &#name::#ident #params => ::core::option::Option::Some(#msg) }); } + + if !documentation.is_empty() { + let params = params.clone(); + // Push the documentation. + documentation_arms + .push(quote! { &#name::#ident #params => ::core::option::Option::Some(concat!(#( #documentation ),*).trim()) }); + } } if arms.len() < variants.len() { @@ -79,6 +88,10 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result { detailed_arms.push(quote! { _ => ::core::option::Option::None }); } + if documentation_arms.len() < variants.len() { + documentation_arms.push(quote! { _ => ::core::option::Option::None }); + } + Ok(quote! { impl #impl_generics #strum_module_path::EnumMessage for #name #ty_generics #where_clause { fn get_message(&self) -> ::core::option::Option<&'static str> { @@ -93,6 +106,12 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result { } } + fn get_documentation(&self) -> ::core::option::Option<&'static str> { + match self { + #(#documentation_arms),* + } + } + fn get_serializations(&self) -> &'static [&'static str] { match self { #(#serializations),* diff --git a/strum_tests/tests/enum_message.rs b/strum_tests/tests/enum_message.rs index b448ac05..69b0fcc7 100644 --- a/strum_tests/tests/enum_message.rs +++ b/strum_tests/tests/enum_message.rs @@ -2,14 +2,21 @@ use strum::EnumMessage; #[derive(Debug, Eq, PartialEq, EnumMessage)] enum Pets { + // I'm a silent dog. #[strum(message = "I'm a dog")] Dog, + /// I eat birds. + /// + /// And fish. #[strum(message = "I'm a cat")] #[strum(detailed_message = "I'm a very exquisite striped cat")] Cat, + /// I'm a fish. #[strum(detailed_message = "My fish is named Charles McFish")] Fish, + /// I'm a bird. Bird, + /// I'm disabled. #[strum(disabled)] Hamster, } @@ -38,6 +45,23 @@ fn only_detailed_message() { ); } +#[test] +fn documentation() { + assert_eq!("I eat birds. And fish.", (Pets::Cat).get_documentation().unwrap()); + assert_eq!("I'm a fish.", (Pets::Fish).get_documentation().unwrap()); + assert_eq!("I'm a bird.", (Pets::Bird).get_documentation().unwrap()); +} + +#[test] +fn no_documentation() { + assert_eq!(None, (Pets::Dog).get_documentation()); +} + +#[test] +fn disabled_documentation() { + assert_eq!(None, (Pets::Hamster).get_documentation()); +} + #[test] fn no_message() { assert_eq!(None, (Pets::Bird).get_message()); From 37b2b291d94bdf4477ebf10adbae09f27ffa3e48 Mon Sep 17 00:00:00 2001 From: orenbenkiki Date: Sun, 6 Feb 2022 13:41:37 +0200 Subject: [PATCH 2/3] Address 1st review comments. --- strum_macros/src/helpers/metadata.rs | 4 +++- strum_macros/src/macros/enum_messages.rs | 24 ++++++++++++++++++++---- strum_tests/tests/enum_message.rs | 6 +++--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/strum_macros/src/helpers/metadata.rs b/strum_macros/src/helpers/metadata.rs index c5a87123..68ff7312 100644 --- a/strum_macros/src/helpers/metadata.rs +++ b/strum_macros/src/helpers/metadata.rs @@ -275,7 +275,9 @@ pub trait VariantExt { impl VariantExt for Variant { fn get_metadata(&self) -> syn::Result> { let result = get_metadata_inner("strum", &self.attrs)?; - self.attrs.iter().filter(|attr| attr.path.is_ident("doc")).try_fold(result, |mut vec, attr| { + self.attrs.iter() + .filter(|attr| attr.path.is_ident("doc")) + .try_fold(result, |mut vec, attr| { if let Meta::NameValue(MetaNameValue { lit: Lit::Str(value), .. }) = attr.parse_meta()? { vec.push(VariantMeta::Documentation { value }) } diff --git a/strum_macros/src/macros/enum_messages.rs b/strum_macros/src/macros/enum_messages.rs index efa6f8d7..3d0f0743 100644 --- a/strum_macros/src/macros/enum_messages.rs +++ b/strum_macros/src/macros/enum_messages.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Data, DeriveInput}; +use syn::{Data, DeriveInput, LitStr}; use crate::helpers::{non_enum_error, HasStrumVariantProperties, HasTypeProperties}; @@ -74,9 +74,25 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result { if !documentation.is_empty() { let params = params.clone(); - // Push the documentation. - documentation_arms - .push(quote! { &#name::#ident #params => ::core::option::Option::Some(concat!(#( #documentation ),*).trim()) }); + // Strip a single leading space from each documentation line. + let documentation: Vec = documentation.iter().map(|lit_str| { + if let Some(suffix) = lit_str.value().strip_prefix(' ') { + LitStr::new(suffix, lit_str.span()) + } else { + lit_str.clone() + } + }).collect(); + if documentation.len() == 1 { + let text = &documentation[0]; + documentation_arms + .push(quote! { &#name::#ident #params => ::core::option::Option::Some(#text) }); + } else { + // Push the documentation. + documentation_arms + .push(quote! { + &#name::#ident #params => ::core::option::Option::Some(concat!(#(concat!(#documentation, "\n")),*)) + }); + } } } diff --git a/strum_tests/tests/enum_message.rs b/strum_tests/tests/enum_message.rs index 69b0fcc7..a5f7bde4 100644 --- a/strum_tests/tests/enum_message.rs +++ b/strum_tests/tests/enum_message.rs @@ -2,7 +2,7 @@ use strum::EnumMessage; #[derive(Debug, Eq, PartialEq, EnumMessage)] enum Pets { - // I'm a silent dog. + // This comment is not collected since it starts with "//" instead of "///". #[strum(message = "I'm a dog")] Dog, /// I eat birds. @@ -16,7 +16,7 @@ enum Pets { Fish, /// I'm a bird. Bird, - /// I'm disabled. + /// This comment is not collected because it is explicitly disabled. #[strum(disabled)] Hamster, } @@ -47,7 +47,7 @@ fn only_detailed_message() { #[test] fn documentation() { - assert_eq!("I eat birds. And fish.", (Pets::Cat).get_documentation().unwrap()); + assert_eq!("I eat birds.\n\nAnd fish.\n", (Pets::Cat).get_documentation().unwrap()); assert_eq!("I'm a fish.", (Pets::Fish).get_documentation().unwrap()); assert_eq!("I'm a bird.", (Pets::Bird).get_documentation().unwrap()); } From 55ee32cf50d6b46aa0e385eaa8d5612ad5dc6582 Mon Sep 17 00:00:00 2001 From: orenbenkiki Date: Mon, 7 Feb 2022 08:59:35 +0200 Subject: [PATCH 3/3] Remove usage of strip_prefix to support older Rust versions. --- strum_macros/src/macros/enum_messages.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/strum_macros/src/macros/enum_messages.rs b/strum_macros/src/macros/enum_messages.rs index 3d0f0743..69c98937 100644 --- a/strum_macros/src/macros/enum_messages.rs +++ b/strum_macros/src/macros/enum_messages.rs @@ -76,8 +76,9 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result { let params = params.clone(); // Strip a single leading space from each documentation line. let documentation: Vec = documentation.iter().map(|lit_str| { - if let Some(suffix) = lit_str.value().strip_prefix(' ') { - LitStr::new(suffix, lit_str.span()) + let line = lit_str.value(); + if line.starts_with(' ') { + LitStr::new(&line.as_str()[1..], lit_str.span()) } else { lit_str.clone() }