From dbe61933a94b68fb36e88213e6df840388c13602 Mon Sep 17 00:00:00 2001 From: Eric McBride Date: Sun, 26 Feb 2023 10:43:34 -0600 Subject: [PATCH 1/3] Initial Push --- .../src/helpers/inner_variant_props.rs | 28 +++++++++ strum_macros/src/helpers/metadata.rs | 57 ++++++++++++++++++- strum_macros/src/helpers/mod.rs | 2 + strum_macros/src/helpers/variant_props.rs | 12 +++- strum_macros/src/macros/enum_messages.rs | 19 ++++--- .../src/macros/strings/from_string.rs | 42 ++++++++++---- strum_tests/tests/from_str.rs | 52 +++++++++++++++++ 7 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 strum_macros/src/helpers/inner_variant_props.rs 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 00000000..408a2b6a --- /dev/null +++ b/strum_macros/src/helpers/inner_variant_props.rs @@ -0,0 +1,28 @@ +use super::metadata::{InnerVariantExt, InnerVariantMeta}; +use syn::{Field, LitStr}; + +pub trait HasInnerVariantProperties { + fn get_variant_inner_properties(&self) -> syn::Result; +} + +#[derive(Clone, Eq, PartialEq, Debug, Default)] +pub struct StrumInnerVariantProperties { + pub default_with: Option, + // ident: Option, +} + +impl HasInnerVariantProperties for Field { + fn get_variant_inner_properties(&self) -> syn::Result { + let mut output = StrumInnerVariantProperties { default_with: None }; + + for meta in self.get_named_metadata()? { + match meta { + InnerVariantMeta::DefaultWith { kw: _, value } => { + output.default_with = Some(value); + } + } + } + + Ok(output) + } +} diff --git a/strum_macros/src/helpers/metadata.rs b/strum_macros/src/helpers/metadata.rs index 56e4c78b..f026d4f1 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 { + 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>; +} + +impl InnerVariantExt for Field { + fn get_named_metadata(&self) -> syn::Result> { + 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 11aebc83..c9f1c2d5 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 a39d3ea1..9eb750bb 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, pub default: Option, + pub default_with: Option, pub ascii_case_insensitive: Option, pub message: Option, pub detailed_message: Option, @@ -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 c0561085..2dad4117 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 { if !documentation.is_empty() { let params = params.clone(); // Strip a single leading space from each documentation line. - let documentation: Vec = 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 = 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 2d255917..de32cbbe 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 { @@ -45,7 +46,6 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result { )) } } - 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 { 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 { 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 { } } }; + 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 { } } }; - let try_from_str = try_from_str( name, &impl_generics, diff --git a/strum_tests/tests/from_str.rs b/strum_tests/tests/from_str.rs index 845768cc..ad88006d 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); + } + } +} From 453b76a0859866bd8fd7bb1950d4d411a2e96da3 Mon Sep 17 00:00:00 2001 From: Eric McBride Date: Sun, 26 Feb 2023 10:51:03 -0600 Subject: [PATCH 2/3] remove uneeded comment --- strum_macros/src/helpers/inner_variant_props.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/strum_macros/src/helpers/inner_variant_props.rs b/strum_macros/src/helpers/inner_variant_props.rs index 408a2b6a..92ceab41 100644 --- a/strum_macros/src/helpers/inner_variant_props.rs +++ b/strum_macros/src/helpers/inner_variant_props.rs @@ -8,7 +8,6 @@ pub trait HasInnerVariantProperties { #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct StrumInnerVariantProperties { pub default_with: Option, - // ident: Option, } impl HasInnerVariantProperties for Field { From 2d47a853ffe0d7b7c972eece5a257e9f913fc88a Mon Sep 17 00:00:00 2001 From: Eric McBride Date: Sun, 26 Feb 2023 11:53:34 -0600 Subject: [PATCH 3/3] add occurence errros --- strum_macros/src/helpers/inner_variant_props.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/strum_macros/src/helpers/inner_variant_props.rs b/strum_macros/src/helpers/inner_variant_props.rs index 92ceab41..bc0fe799 100644 --- a/strum_macros/src/helpers/inner_variant_props.rs +++ b/strum_macros/src/helpers/inner_variant_props.rs @@ -1,4 +1,5 @@ use super::metadata::{InnerVariantExt, InnerVariantMeta}; +use super::occurrence_error; use syn::{Field, LitStr}; pub trait HasInnerVariantProperties { @@ -14,9 +15,14 @@ impl HasInnerVariantProperties for Field { fn get_variant_inner_properties(&self) -> syn::Result { 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 } => { + 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); } }