Repository: Peternator7/strum
Pull request #254 (2023-02-26 16:47:50)

Add default_with strum macro
- My first shot at messing with Syn + Quote + Proc Macros in general
- Added the default_with macro, from #245 
- Added tests to cover new macro
- Will Add doc strings after verifying if my approach is ok


Patch:
diff --git a/strum_macros/src/helpers/inner_variant_props.rs b/strum_macros/src/helpers/inner_variant_props.rs
new file mode 100644
index 0000000..bc0fe79
--- /dev/null
+++ b/strum_macros/src/helpers/inner_variant_props.rs
@@ -0,0 +1,33 @@
+use super::metadata::{InnerVariantExt, InnerVariantMeta};
+use super::occurrence_error;
+use syn::{Field, LitStr};
+
+pub trait HasInnerVariantProperties {
+    fn get_variant_inner_properties(&self) -> syn::Result<StrumInnerVariantProperties>;
+}
+
+#[derive(Clone, Eq, PartialEq, Debug, Default)]
+pub struct StrumInnerVariantProperties {
+    pub default_with: Option<LitStr>,
+}
+
+impl HasInnerVariantProperties for Field {
+    fn get_variant_inner_properties(&self) -> syn::Result<StrumInnerVariantProperties> {
+        let mut output = StrumInnerVariantProperties { default_with: None };
+
+        let mut default_with_kw = None;
+        for meta in self.get_named_metadata()? {
+            match meta {
+                InnerVariantMeta::DefaultWith { kw, value } => {
+                    if let Some(fst_kw) = default_with_kw {
+                        return Err(occurrence_error(fst_kw, kw, "default_with"));
+                    }
+                    default_with_kw = Some(kw);
+                    output.default_with = Some(value);
+                }
+            }
+        }
+
+        Ok(output)
+    }
+}
diff --git a/strum_macros/src/helpers/metadata.rs b/strum_macros/src/helpers/metadata.rs
index 56e4c78..f026d4f 100644
--- a/strum_macros/src/helpers/metadata.rs
+++ b/strum_macros/src/helpers/metadata.rs
@@ -5,8 +5,8 @@ use syn::{
     parse2, parse_str,
     punctuated::Punctuated,
     spanned::Spanned,
-    Attribute, DeriveInput, Ident, Lit, LitBool, LitStr, Meta, MetaNameValue, Path, Token, Variant,
-    Visibility,
+    Attribute, DeriveInput, Field, Ident, Lit, LitBool, LitStr, Meta, MetaNameValue, Path, Token,
+    Variant, Visibility,
 };
 
 use super::case_style::CaseStyle;
@@ -31,6 +31,7 @@ pub mod kw {
     custom_keyword!(to_string);
     custom_keyword!(disabled);
     custom_keyword!(default);
+    custom_keyword!(default_with);
     custom_keyword!(props);
     custom_keyword!(ascii_case_insensitive);
 }
@@ -178,6 +179,10 @@ pub enum VariantMeta {
     },
     Disabled(kw::disabled),
     Default(kw::default),
+    DefaultWith {
+        kw: kw::default_with,
+        value: LitStr,
+    },
     AsciiCaseInsensitive {
         kw: kw::ascii_case_insensitive,
         value: bool,
@@ -215,6 +220,11 @@ impl Parse for VariantMeta {
             Ok(VariantMeta::Disabled(input.parse()?))
         } else if lookahead.peek(kw::default) {
             Ok(VariantMeta::Default(input.parse()?))
+        } else if lookahead.peek(kw::default_with) {
+            let kw = input.parse()?;
+            let _: Token![=] = input.parse()?;
+            let value = input.parse()?;
+            Ok(VariantMeta::DefaultWith { kw, value })
         } else if lookahead.peek(kw::ascii_case_insensitive) {
             let kw = input.parse()?;
             let value = if input.peek(Token![=]) {
@@ -266,6 +276,7 @@ impl Spanned for VariantMeta {
             VariantMeta::ToString { kw, .. } => kw.span,
             VariantMeta::Disabled(kw) => kw.span,
             VariantMeta::Default(kw) => kw.span,
+            VariantMeta::DefaultWith { kw, .. } => kw.span,
             VariantMeta::AsciiCaseInsensitive { kw, .. } => kw.span,
             VariantMeta::Props { kw, .. } => kw.span,
         }
@@ -307,3 +318,45 @@ fn get_metadata_inner<'a, T: Parse + Spanned>(
             Ok(vec)
         })
 }
+
+#[derive(Debug)]
+pub enum InnerVariantMeta {
+    DefaultWith { kw: kw::default_with, value: LitStr },
+}
+
+impl Spanned for InnerVariantMeta {
+    fn span(&self) -> Span {
+        match self {
+            InnerVariantMeta::DefaultWith { kw, .. } => kw.span(),
+        }
+    }
+}
+
+impl Parse for InnerVariantMeta {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let lookahead = input.lookahead1();
+        if lookahead.peek(kw::default_with) {
+            let kw = input.parse()?;
+            let _: Token![=] = input.parse()?;
+            let value = input.parse()?;
+            Ok(InnerVariantMeta::DefaultWith { kw, value })
+        } else {
+            Err(lookahead.error())
+        }
+    }
+}
+
+pub trait InnerVariantExt {
+    /// Get all the metadata associated with an enum variant inner.
+    fn get_named_metadata(&self) -> syn::Result<Vec<InnerVariantMeta>>;
+}
+
+impl InnerVariantExt for Field {
+    fn get_named_metadata(&self) -> syn::Result<Vec<InnerVariantMeta>> {
+        let result = get_metadata_inner("strum", &self.attrs)?;
+        self.attrs
+            .iter()
+            .filter(|attr| attr.path.is_ident("default_with"))
+            .try_fold(result, |vec, _attr| Ok(vec))
+    }
+}
diff --git a/strum_macros/src/helpers/mod.rs b/strum_macros/src/helpers/mod.rs
index 11aebc8..c9f1c2d 100644
--- a/strum_macros/src/helpers/mod.rs
+++ b/strum_macros/src/helpers/mod.rs
@@ -1,8 +1,10 @@
 pub use self::case_style::CaseStyleHelpers;
+pub use self::inner_variant_props::HasInnerVariantProperties;
 pub use self::type_props::HasTypeProperties;
 pub use self::variant_props::HasStrumVariantProperties;
 
 pub mod case_style;
+pub mod inner_variant_props;
 mod metadata;
 pub mod type_props;
 pub mod variant_props;
diff --git a/strum_macros/src/helpers/variant_props.rs b/strum_macros/src/helpers/variant_props.rs
index a39d3ea..9eb750b 100644
--- a/strum_macros/src/helpers/variant_props.rs
+++ b/strum_macros/src/helpers/variant_props.rs
@@ -13,6 +13,7 @@ pub trait HasStrumVariantProperties {
 pub struct StrumVariantProperties {
     pub disabled: Option<kw::disabled>,
     pub default: Option<kw::default>,
+    pub default_with: Option<LitStr>,
     pub ascii_case_insensitive: Option<bool>,
     pub message: Option<LitStr>,
     pub detailed_message: Option<LitStr>,
@@ -62,9 +63,10 @@ impl HasStrumVariantProperties for Variant {
 
         let mut message_kw = None;
         let mut detailed_message_kw = None;
-        let mut to_string_kw = None;
         let mut disabled_kw = None;
         let mut default_kw = None;
+        let mut default_with_kw = None;
+        let mut to_string_kw = None;
         let mut ascii_case_insensitive_kw = None;
         for meta in self.get_metadata()? {
             match meta {
@@ -114,6 +116,14 @@ impl HasStrumVariantProperties for Variant {
                     default_kw = Some(kw);
                     output.default = Some(kw);
                 }
+                VariantMeta::DefaultWith { kw, value } => {
+                    if let Some(fst_kw) = default_with_kw {
+                        return Err(occurrence_error(fst_kw, kw, "default_with"));
+                    }
+
+                    default_with_kw = Some(kw);
+                    output.default_with = Some(value);
+                }
                 VariantMeta::AsciiCaseInsensitive { kw, value } => {
                     if let Some(fst_kw) = ascii_case_insensitive_kw {
                         return Err(occurrence_error(fst_kw, kw, "ascii_case_insensitive"));
diff --git a/strum_macros/src/macros/enum_messages.rs b/strum_macros/src/macros/enum_messages.rs
index c056108..2dad411 100644
--- a/strum_macros/src/macros/enum_messages.rs
+++ b/strum_macros/src/macros/enum_messages.rs
@@ -74,14 +74,17 @@ pub fn enum_message_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
         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();
+            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
diff --git a/strum_macros/src/macros/strings/from_string.rs b/strum_macros/src/macros/strings/from_string.rs
index 2d25591..de32cbb 100644
--- a/strum_macros/src/macros/strings/from_string.rs
+++ b/strum_macros/src/macros/strings/from_string.rs
@@ -3,7 +3,8 @@ use quote::quote;
 use syn::{Data, DeriveInput, Fields};
 
 use crate::helpers::{
-    non_enum_error, occurrence_error, HasStrumVariantProperties, HasTypeProperties,
+    non_enum_error, occurrence_error, HasInnerVariantProperties, HasStrumVariantProperties,
+    HasTypeProperties,
 };
 
 pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
@@ -45,7 +46,6 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
                     ))
                 }
             }
-
             default_kw = Some(kw);
             default = quote! {
                 ::core::result::Result::Ok(#name::#ident(s.into()))
@@ -56,16 +56,34 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
         let params = match &variant.fields {
             Fields::Unit => quote! {},
             Fields::Unnamed(fields) => {
-                let defaults =
-                    ::core::iter::repeat(quote!(Default::default())).take(fields.unnamed.len());
-                quote! { (#(#defaults),*) }
+                if let Some(ref value) = variant_properties.default_with {
+                    let func = proc_macro2::Ident::new(&value.value(), value.span());
+                    let defaults = vec![quote! { #func() }];
+                    quote! { (#(#defaults),*) }
+                } else {
+                    let defaults =
+                        ::core::iter::repeat(quote!(Default::default())).take(fields.unnamed.len());
+                    quote! { (#(#defaults),*) }
+                }
             }
             Fields::Named(fields) => {
-                let fields = fields
-                    .named
-                    .iter()
-                    .map(|field| field.ident.as_ref().unwrap());
-                quote! { {#(#fields: Default::default()),*} }
+                let mut defaults = vec![];
+                for field in &fields.named {
+                    let meta = field.get_variant_inner_properties()?;
+                    let field = field.ident.as_ref().unwrap();
+
+                    if let Some(default_with) = meta.default_with {
+                        let func =
+                            proc_macro2::Ident::new(&default_with.value(), default_with.span());
+                        defaults.push(quote! {
+                            #field: #func()
+                        });
+                    } else {
+                        defaults.push(quote! { #field: Default::default() });
+                    }
+                }
+
+                quote! { {#(#defaults),*} }
             }
         };
 
@@ -79,7 +97,7 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
                 phf_exact_match_arms.push(quote! { #serialization => #name::#ident #params, });
 
                 if is_ascii_case_insensitive {
-                    // Store the lowercase and UPPERCASE variants in the phf map to capture 
+                    // Store the lowercase and UPPERCASE variants in the phf map to capture
                     let ser_string = serialization.value();
 
                     let lower =
@@ -113,6 +131,7 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
             }
         }
     };
+
     let standard_match_body = if standard_match_arms.is_empty() {
         default
     } else {
@@ -134,7 +153,6 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
             }
         }
     };
-
     let try_from_str = try_from_str(
         name,
         &impl_generics,


Test patch:
diff --git a/strum_tests/tests/from_str.rs b/strum_tests/tests/from_str.rs
index 845768c..ad88006 100644
--- a/strum_tests/tests/from_str.rs
+++ b/strum_tests/tests/from_str.rs
@@ -15,6 +15,15 @@ enum Color {
     Purple,
     #[strum(serialize = "blk", serialize = "Black", ascii_case_insensitive)]
     Black,
+    Pink {
+        #[strum(default_with = "test_default")]
+        test_no_default: NoDefault,
+
+        #[strum(default_with = "string_test")]
+        string_test: String,
+    },
+    #[strum(default_with = "to_white")]
+    White(String),
 }
 
 #[rustversion::since(1.34)]
@@ -175,3 +184,46 @@ fn case_insensitive_enum_case_insensitive() {
     assert_from_str(CaseInsensitiveEnum::CaseInsensitive, "CaseInsensitive");
     assert_from_str(CaseInsensitiveEnum::CaseInsensitive, "caseinsensitive");
 }
+
+#[derive(Eq, PartialEq, Debug)]
+struct NoDefault(String);
+
+fn test_default() -> NoDefault {
+    NoDefault(String::from("test"))
+}
+
+fn to_white() -> String {
+    String::from("white-test")
+}
+
+fn string_test() -> String {
+    String::from("This is a string test")
+}
+
+#[test]
+fn color_default_with() {
+    match Color::from_str("Pink").unwrap() {
+        Color::Pink {
+            test_no_default,
+            string_test,
+        } => {
+            assert_eq!(test_no_default, test_default());
+            assert_eq!(string_test, String::from("This is a string test"));
+        }
+        other => {
+            panic!("Failed to get correct enum value {:?}", other);
+        }
+    }
+}
+
+#[test]
+fn color_default_with_white() {
+    match Color::from_str("White").unwrap() {
+        Color::White(inner) => {
+            assert_eq!(inner, String::from("white-test"));
+        }
+        other => {
+            panic!("Failed t o get  correct enum value {:?}", other);
+        }
+    }
+}