Skip to content
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

Add TryFrom<&str> to EnumString #186

Merged
merged 1 commit into from Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions strum_macros/Cargo.toml
Expand Up @@ -22,6 +22,7 @@ name = "strum_macros"
heck = "0.3"
proc-macro2 = "1.0"
quote = "1.0"
rustversion = "1.0"
syn = { version = "1.0", features = ["parsing", "extra-traits"] }

[dev-dependencies]
Expand Down
3 changes: 2 additions & 1 deletion strum_macros/src/lib.rs
Expand Up @@ -30,7 +30,8 @@ fn debug_print_generated(ast: &DeriveInput, toks: &TokenStream) {

/// Converts strings to enum variants based on their name.
///
/// auto-derives `std::str::FromStr` on the enum. Each variant of the enum will match on it's own name.
/// auto-derives `std::str::FromStr` on the enum (for Rust 1.34 and above, std::convert::TryFrom<&str>
/// will be derived as well). Each variant of the enum will match on it's own name.
/// This can be overridden using `serialize="DifferentName"` or `to_string="DifferentName"`
/// on the attribute as shown below.
/// Multiple deserializations can be added to the same variant. If the variant contains additional data,
Expand Down
45 changes: 44 additions & 1 deletion strum_macros/src/macros/strings/from_string.rs
Expand Up @@ -87,7 +87,7 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {

arms.push(default);

Ok(quote! {
let from_str = quote! {
#[allow(clippy::use_self)]
impl #impl_generics ::std::str::FromStr for #name #ty_generics #where_clause {
type Err = #strum_module_path::ParseError;
Expand All @@ -97,5 +97,48 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
}
}
}
};

let try_from_str = try_from_str(
name,
impl_generics,
ty_generics,
where_clause,
strum_module_path,
);

Ok(quote! {
#from_str
#try_from_str
})
}

#[rustversion::before(1.34)]
fn try_from_str(
_name: &proc_macro2::Ident,
_impl_generics: syn::ImplGenerics,
_ty_generics: syn::TypeGenerics,
_where_clause: Option<&syn::WhereClause>,
_strum_module_path: syn::Path,
) -> TokenStream {
Default::default()
}

#[rustversion::since(1.34)]
fn try_from_str(
name: &proc_macro2::Ident,
impl_generics: syn::ImplGenerics,
ty_generics: syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
strum_module_path: syn::Path,
) -> TokenStream {
quote! {
#[allow(clippy::use_self)]
impl #impl_generics ::std::convert::TryFrom<&str> for #name #ty_generics #where_clause {
Copy link

@toxeus toxeus Oct 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe using AsRef<str> would be even more useful?

impl<T: AsRef<str>> #impl_generics ::std::convert::TryFrom<T> for #name #ty_generics #where_clause {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs into the following interesting compiler bug:
rust-lang/rust#50133

See e.g. here for our very specific case: rust-lang/rust#50133 (comment)

Leaving it out for now, since the workaround looks annoying to use.

type Error = #strum_module_path::ParseError;
fn try_from(s: &str) -> ::std::result::Result< #name #ty_generics , Self::Error> {
::std::str::FromStr::from_str(s)
}
}
}
}
5 changes: 4 additions & 1 deletion strum_tests/Cargo.toml
Expand Up @@ -13,4 +13,7 @@ structopt = "0.2.18"
bitflags = "=1.2"

[build-dependencies]
version_check = "0.9.2"
version_check = "0.9.2"

[dev-dependencies]
rustversion = "1.0"
102 changes: 54 additions & 48 deletions strum_tests/tests/from_str.rs
Expand Up @@ -17,39 +17,56 @@ enum Color {
Black,
}

#[rustversion::since(1.34)]
fn assert_from_str<'a, T>(a: T, from: &'a str)
where
T: PartialEq + std::str::FromStr + std::convert::TryFrom<&'a str> + std::fmt::Debug,
<T as std::str::FromStr>::Err: std::fmt::Debug,
<T as std::convert::TryFrom<&'a str>>::Error: std::fmt::Debug,
{
assert_eq!(a, T::from_str(from).unwrap());
assert_eq!(a, std::convert::TryFrom::try_from(from).unwrap());
}

#[rustversion::before(1.34)]
fn assert_from_str<T>(a: T, from: &str)
where
T: PartialEq + std::str::FromStr + std::fmt::Debug,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
assert_eq!(a, T::from_str(from).unwrap());
}

#[test]
fn color_simple() {
assert_eq!(Color::Red, Color::from_str("Red").unwrap());
assert_from_str(Color::Red, "Red");
}

#[test]
fn color_value() {
assert_eq!(Color::Blue { hue: 0 }, Color::from_str("Blue").unwrap());
assert_from_str(Color::Blue { hue: 0 }, "Blue");
}

#[test]
fn color_serialize() {
assert_eq!(Color::Yellow, Color::from_str("y").unwrap());
assert_eq!(Color::Yellow, Color::from_str("yellow").unwrap());
assert_from_str(Color::Yellow, "y");
assert_from_str(Color::Yellow, "yellow");
}

#[test]
fn color_to_string() {
assert_eq!(Color::Purple, Color::from_str("purp").unwrap());
assert_from_str(Color::Purple, "purp");
}

#[test]
fn color_default() {
assert_eq!(
Color::Green(String::from("not found")),
Color::from_str("not found").unwrap()
);
assert_from_str(Color::Green(String::from("not found")), "not found");
}

#[test]
fn color_ascii_case_insensitive() {
assert_eq!(Color::Black, Color::from_str("BLK").unwrap());
assert_eq!(Color::Black, Color::from_str("bLaCk").unwrap());
assert_from_str(Color::Black, "BLK");
assert_from_str(Color::Black, "bLaCk");
}

#[derive(Debug, Eq, PartialEq, EnumString)]
Expand All @@ -65,18 +82,9 @@ enum Brightness {

#[test]
fn brightness_serialize_all() {
assert_eq!(
Brightness::DarkBlack,
Brightness::from_str("dark_black").unwrap()
);
assert_eq!(
Brightness::Dim { glow: 0 },
Brightness::from_str("dim").unwrap()
);
assert_eq!(
Brightness::BrightWhite,
Brightness::from_str("Bright").unwrap()
);
assert_from_str(Brightness::DarkBlack, "dark_black");
assert_from_str(Brightness::Dim { glow: 0 }, "dim");
assert_from_str(Brightness::BrightWhite, "Bright");
}

#[derive(Debug, Eq, PartialEq, EnumString)]
Expand All @@ -100,13 +108,13 @@ fn week_not_found() {

#[test]
fn week_found() {
assert_eq!(Result::Ok(Week::Sunday), Week::from_str("Sunday"));
assert_eq!(Result::Ok(Week::Monday), Week::from_str("Monday"));
assert_eq!(Result::Ok(Week::Tuesday), Week::from_str("Tuesday"));
assert_eq!(Result::Ok(Week::Wednesday), Week::from_str("Wednesday"));
assert_eq!(Result::Ok(Week::Thursday), Week::from_str("Thursday"));
assert_eq!(Result::Ok(Week::Friday), Week::from_str("Friday"));
assert_eq!(Result::Ok(Week::Saturday), Week::from_str("Saturday"));
assert_from_str(Week::Sunday, "Sunday");
assert_from_str(Week::Monday, "Monday");
assert_from_str(Week::Tuesday, "Tuesday");
assert_from_str(Week::Wednesday, "Wednesday");
assert_from_str(Week::Thursday, "Thursday");
assert_from_str(Week::Friday, "Friday");
assert_from_str(Week::Saturday, "Saturday");
}

#[derive(Debug, Eq, PartialEq, EnumString)]
Expand All @@ -117,7 +125,7 @@ enum Lifetime<'a> {

#[test]
fn lifetime_test() {
assert_eq!(Lifetime::Life(""), Lifetime::from_str("Life").unwrap());
assert_from_str(Lifetime::Life(""), "Life");
}

#[derive(Debug, Eq, PartialEq, EnumString)]
Expand All @@ -128,7 +136,7 @@ enum Generic<T: Default> {

#[test]
fn generic_test() {
assert_eq!(Generic::Gen(""), Generic::from_str("Gen").unwrap());
assert_from_str(Generic::Gen(""), "Gen");
}

#[derive(Debug, Eq, PartialEq, EnumString)]
Expand All @@ -143,29 +151,27 @@ enum CaseInsensitiveEnum {

#[test]
fn case_insensitive_enum_no_attr() {
assert_eq!(
CaseInsensitiveEnum::NoAttr,
CaseInsensitiveEnum::from_str("noattr").unwrap()
);
assert_from_str(CaseInsensitiveEnum::NoAttr, "noattr");
}

#[test]
fn case_insensitive_enum_no_case_insensitive() {
assert_eq!(
CaseInsensitiveEnum::NoCaseInsensitive,
CaseInsensitiveEnum::from_str("NoCaseInsensitive").unwrap(),
);
assert_from_str(CaseInsensitiveEnum::NoCaseInsensitive, "NoCaseInsensitive");
assert!(CaseInsensitiveEnum::from_str("nocaseinsensitive").is_err());
}

#[rustversion::since(1.34)]
#[test]
fn case_insensitive_enum_case_insensitive() {
assert_eq!(
CaseInsensitiveEnum::CaseInsensitive,
CaseInsensitiveEnum::from_str("CaseInsensitive").unwrap(),
);
assert_eq!(
CaseInsensitiveEnum::CaseInsensitive,
CaseInsensitiveEnum::from_str("caseinsensitive").unwrap(),
fn case_insensitive_enum_no_case_insensitive_try_from() {
assert_from_str(CaseInsensitiveEnum::NoCaseInsensitive, "NoCaseInsensitive");
assert!(
<CaseInsensitiveEnum as std::convert::TryFrom<&str>>::try_from("nocaseinsensitive")
.is_err()
);
}

#[test]
fn case_insensitive_enum_case_insensitive() {
assert_from_str(CaseInsensitiveEnum::CaseInsensitive, "CaseInsensitive");
assert_from_str(CaseInsensitiveEnum::CaseInsensitive, "caseinsensitive");
}