Repository: Peternator7/strum
Pull request #206 (2022-01-10 09:57:56)

Add get_documentation() to EnumMessage.
Implement #203.

Peternator7: Thanks for the PR. Just a couple of minor tweaks I think.

I'm a little worried that people will mis-use this feature, and negatively impact the quaility of their own documentation, but I also see the practical benefit of something like this so I think it's okay.
orenbenkiki: What are the minor tweaks you'd like to see?
Peternator7: I forgot to hit submit review :( sorry about that
orenbenkiki: Did a pass (trying) to address the review issues.
orenbenkiki: Ok, checks pass now.

Patch:
diff --git a/strum/src/additional_attributes.rs b/strum/src/additional_attributes.rs
index 0cb844d..5e32914 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 2693a4e..c007a22 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 5fa4df2..b9e8556 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 eb8b9d8..08ca384 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,15 @@ pub trait VariantExt {
 
 impl VariantExt for Variant {
     fn get_metadata(&self) -> syn::Result<Vec<VariantMeta>> {
-        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 8cf67dc..a39d3ea 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<bool>,
     pub message: Option<LitStr>,
     pub detailed_message: Option<LitStr>,
+    pub documentation: Vec<LitStr>,
     pub string_props: Vec<(LitStr, LitStr)>,
     serialize: Vec<LitStr>,
     to_string: Option<LitStr>,
@@ -83,6 +84,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 ac5683c..1fed8f0 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 0141799..c056108 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, Fields};
+use syn::{Data, DeriveInput, Fields, LitStr};
 
 use crate::helpers::{non_enum_error, HasStrumVariantProperties, HasTypeProperties};
 
@@ -17,12 +17,14 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
 
     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;
 
         let params = match variant.fields {
@@ -64,10 +66,34 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
 
         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();
+            // Strip a single leading space from each documentation line.
+            let documentation: Vec<LitStr> = documentation.iter().map(|lit_str| {
+                let line = lit_str.value();
+                if line.starts_with(' ') {
+                    LitStr::new(&line.as_str()[1..], 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")),*))
+                    });
+            }
+        }
     }
 
     if arms.len() < variants.len() {
@@ -78,6 +104,10 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
         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> {
@@ -92,6 +122,12 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
                 }
             }
 
+            fn get_documentation(&self) -> ::core::option::Option<&'static str> {
+                match self {
+                    #(#documentation_arms),*
+                }
+            }
+
             fn get_serializations(&self) -> &'static [&'static str] {
                 match self {
                     #(#serializations),*


Test patch:
diff --git a/strum_tests/tests/enum_message.rs b/strum_tests/tests/enum_message.rs
index b448ac0..a5f7bde 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 {
+    // This comment is not collected since it starts with "//" instead of "///".
     #[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,
+    /// This comment is not collected because it is explicitly disabled.
     #[strum(disabled)]
     Hamster,
 }
@@ -38,6 +45,23 @@ fn only_detailed_message() {
     );
 }
 
+#[test]
+fn documentation() {
+    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());
+}
+
+#[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());