-
Notifications
You must be signed in to change notification settings - Fork 188
Make TryFrom and FromStr infallible if there's a default #476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cbb7302
d4779e8
a0811a3
d09d50d
66b2ffa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| use proc_macro2::TokenStream; | ||
| use quote::quote; | ||
| use syn::{parse_quote, Data, DeriveInput, Fields, Path}; | ||
| use syn::{Data, DeriveInput, Fields}; | ||
|
|
||
| use crate::helpers::{ | ||
| missing_parse_err_attr_error, non_enum_error, occurrence_error, HasInnerVariantProperties, | ||
|
|
@@ -18,26 +18,14 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> { | |
| let type_properties = ast.get_type_properties()?; | ||
| let strum_module_path = type_properties.crate_module_path(); | ||
|
|
||
| // It's an error to provide an err_fn but not an err_ty. | ||
| if type_properties.parse_err_fn.is_some() && type_properties.parse_err_ty.is_none() { | ||
| return Err(missing_parse_err_attr_error()); | ||
| } | ||
|
|
||
| let mut default_kw = None; | ||
| let (default_err_ty, mut default_match_arm) = match ( | ||
| type_properties.parse_err_ty, | ||
| type_properties.parse_err_fn, | ||
| ) { | ||
| (None, None) => ( | ||
| quote! { #strum_module_path::ParseError }, | ||
| quote! { ::core::result::Result::Err(#strum_module_path::ParseError::VariantNotFound) }, | ||
| ), | ||
| (Some(ty), Some(f)) => { | ||
| let ty_path: Path = parse_quote!(#ty); | ||
| let fn_path: Path = parse_quote!(#f); | ||
|
|
||
| ( | ||
| quote! { #ty_path }, | ||
| quote! { ::core::result::Result::Err(#fn_path(s)) }, | ||
| ) | ||
| } | ||
| _ => return Err(missing_parse_err_attr_error()), | ||
| }; | ||
| let mut default_match_arm = None; | ||
|
|
||
| let mut phf_exact_match_arms = Vec::new(); | ||
| let mut standard_match_arms = Vec::new(); | ||
| for variant in variants { | ||
|
|
@@ -57,15 +45,13 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> { | |
|
|
||
| match &variant.fields { | ||
| Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { | ||
| default_match_arm = quote! { | ||
| ::core::result::Result::Ok(#name::#ident(s.into())) | ||
| }; | ||
| default_match_arm = Some(quote! { | ||
| #name::#ident(s.into()) | ||
| }); | ||
| } | ||
| Fields::Named(ref f) if f.named.len() == 1 => { | ||
| let field_name = f.named.last().unwrap().ident.as_ref().unwrap(); | ||
| default_match_arm = quote! { | ||
| ::core::result::Result::Ok(#name::#ident { #field_name : s.into() } ) | ||
| }; | ||
| default_match_arm = Some(quote! { #name::#ident { #field_name : s.into() } }); | ||
| } | ||
| _ => { | ||
| return Err(syn::Error::new_spanned( | ||
|
|
@@ -133,85 +119,109 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> { | |
| phf_exact_match_arms.push(quote! { #upper => #name::#ident #params, }); | ||
| standard_match_arms.push(quote! { s if s.eq_ignore_ascii_case(#serialization) => #name::#ident #params, }); | ||
| } | ||
| } else if !is_ascii_case_insensitive { | ||
| standard_match_arms.push(quote! { #serialization => #name::#ident #params, }); | ||
| } else { | ||
| standard_match_arms.push(if !is_ascii_case_insensitive { | ||
| quote! { #serialization => #name::#ident #params, } | ||
| } else { | ||
| quote! { s if s.eq_ignore_ascii_case(#serialization) => #name::#ident #params, } | ||
| }); | ||
| standard_match_arms.push(quote! { s if s.eq_ignore_ascii_case(#serialization) => #name::#ident #params, }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let phf_body = if phf_exact_match_arms.is_empty() { | ||
| quote!() | ||
| // Determine the error type on FromStr and TryFrom based on what the user | ||
| // has configured and whether there is a default variant. | ||
| let is_infallible = default_match_arm.is_some(); | ||
| let has_custom_err_ty = type_properties.parse_err_ty.is_some(); | ||
| let err_ty = if let Some(ty) = type_properties.parse_err_ty { | ||
| quote! { #ty } | ||
| } else if is_infallible { | ||
| quote! { ::core::convert::Infallible } | ||
| } else { | ||
| quote! { #strum_module_path::ParseError } | ||
| }; | ||
|
|
||
| // Determine the default match arm behavior based on whether the user provided a "default" | ||
| // or if the user provided a custom error function. | ||
| let default_match_arm = if let Some(default_match_arm) = default_match_arm { | ||
| default_match_arm | ||
| } else if let Some(f) = type_properties.parse_err_fn { | ||
| quote! { return ::core::result::Result::Err(#f(s)) } | ||
| } else if has_custom_err_ty { | ||
| // The user defined a custom error type, but not a custom error function. This is an error | ||
| // if the method isn't infallible. | ||
| return Err(missing_parse_err_attr_error()); | ||
|
Comment on lines
+148
to
+151
|
||
| } else { | ||
| quote! { return ::core::result::Result::Err(#strum_module_path::ParseError::VariantNotFound) } | ||
| }; | ||
|
|
||
| let mut match_expression = if standard_match_arms.is_empty() { | ||
| default_match_arm | ||
| } else { | ||
| quote! { | ||
| match s { | ||
| #(#standard_match_arms)* | ||
| _ => #default_match_arm, | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| if !phf_exact_match_arms.is_empty() { | ||
| match_expression = quote! { | ||
| use #strum_module_path::_private_phf_reexport_for_macro_if_phf_feature as phf; | ||
| static PHF: phf::Map<&'static str, #name> = phf::phf_map! { | ||
| #(#phf_exact_match_arms)* | ||
| }; | ||
|
|
||
| if let Some(value) = PHF.get(s).cloned() { | ||
| return ::core::result::Result::Ok(value); | ||
| value | ||
| } else { | ||
| #match_expression | ||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| let standard_match_body = if standard_match_arms.is_empty() { | ||
| default_match_arm | ||
| let from_impl = if is_infallible && !has_custom_err_ty { | ||
| quote! { | ||
| #[allow(clippy::use_self)] | ||
| #[automatically_derived] | ||
| impl #impl_generics ::core::convert::From<&str> for #name #ty_generics #where_clause { | ||
| #[inline] | ||
| fn from(s: &str) -> #name #ty_generics { | ||
| #match_expression | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| quote! { | ||
| ::core::result::Result::Ok(match s { | ||
| #(#standard_match_arms)* | ||
| _ => return #default_match_arm, | ||
| }) | ||
| #[allow(clippy::use_self)] | ||
| #[automatically_derived] | ||
| impl #impl_generics ::core::convert::TryFrom<&str> for #name #ty_generics #where_clause { | ||
| type Error = #err_ty; | ||
|
|
||
| #[inline] | ||
| fn try_from(s: &str) -> ::core::result::Result< #name #ty_generics , <Self as ::core::convert::TryFrom<&str>>::Error> { | ||
| Ok({ | ||
| #match_expression | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| let from_str = quote! { | ||
| #[allow(clippy::use_self)] | ||
| #[automatically_derived] | ||
| impl #impl_generics ::core::str::FromStr for #name #ty_generics #where_clause { | ||
| type Err = #default_err_ty; | ||
| type Err = #err_ty; | ||
|
|
||
| #[inline] | ||
| fn from_str(s: &str) -> ::core::result::Result< #name #ty_generics , <Self as ::core::str::FromStr>::Err> { | ||
| #phf_body | ||
| #standard_match_body | ||
| <Self as ::core::convert::TryFrom<&str>>::try_from(s) | ||
| } | ||
| } | ||
| }; | ||
| let try_from_str = try_from_str( | ||
| name, | ||
| &impl_generics, | ||
| &ty_generics, | ||
| where_clause, | ||
| &default_err_ty, | ||
| ); | ||
|
|
||
| Ok(quote! { | ||
| #from_str | ||
| #try_from_str | ||
| #from_impl | ||
| }) | ||
| } | ||
|
|
||
| fn try_from_str( | ||
| name: &proc_macro2::Ident, | ||
| impl_generics: &syn::ImplGenerics, | ||
| ty_generics: &syn::TypeGenerics, | ||
| where_clause: Option<&syn::WhereClause>, | ||
| default_err_ty: &TokenStream, | ||
| ) -> TokenStream { | ||
| quote! { | ||
| #[allow(clippy::use_self)] | ||
| #[automatically_derived] | ||
| impl #impl_generics ::core::convert::TryFrom<&str> for #name #ty_generics #where_clause { | ||
| type Error = #default_err_ty; | ||
|
|
||
| #[inline] | ||
| fn try_from(s: &str) -> ::core::result::Result< #name #ty_generics , <Self as ::core::convert::TryFrom<&str>>::Error> { | ||
| ::core::str::FromStr::from_str(s) | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states that both
parse_err_tyandparse_err_fnattributes must be provided to override the error type. However, with the infallible parsing changes, if there's a#[strum(default)]variant, onlyparse_err_tyis required (see from_string.rs:148-151). Consider updating this to clarify thatparse_err_fnis only required when there's no default variant, e.g., "This can be overridden by applying theparse_err_tyattribute (andparse_err_fnwhen there's no default variant)."