Repository: JelteF/derive_more (rust)

Issue:
Serde like `rename` for Display & FromStr trait on enum
i.e. I would expect
```rs
#[derive(Display)]
#[display(rename="snake_case")]
enum Test{
   First
}

println!("{}", Test::First) // -> prints "first"
```

The serde behaviour is documented here:
https://serde.rs/container-attrs.html#rename_all

> ### #[serde(rename_all = "...")]
> Rename all [...] variants [...] according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", "SCREAMING-KEBAB-CASE".

Pull request description:
Support `#[from_str(rename_all = "...")]` attribute (#216)
Resolves #216   
Requires #468, #469

## Synopsis

We have `#[display(rename_all = "...")]` attribute for flat enums. The counterpart for `FromStr` derive is missing.

## Solution

This PR adds support `#[from_str(rename_all = "...")]` attribute on enums. Once this attribute is specified, the matching is always exact (for now, by default, matching could be fuzzy if there is no similarly named variants).

## Additionally

This PR fully reworks `FromStr` derive implementation to avoid using old `utils` machinery.

Adds support for unit structs (like `struct Unit;`) for the counterpart behavior of `Display`.

Adds `tests/compile_fail/from_str`.

## Checklist

- [x] Documentation is updated
- [x] Tests are added/updated
- [x] [CHANGELOG entry](/CHANGELOG.md) is added


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f599235..5b2db57 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 - Support `#[display(rename_all = "<casing>")]` attribute to change output for
   implicit naming of unit enum variants or unit structs when deriving `Display`.
   ([#443](https://github.com/JelteF/derive_more/pull/443))
+- Support `#[from_str(rename_all = "<casing>")]` attribute for unit enum variants
+  and unit structs when deriving `FromStr`.
+  ([#467](https://github.com/JelteF/derive_more/pull/467))
 - Support `Option` fields for `Error::source()` in `Error` derive.
   ([#459](https://github.com/JelteF/derive_more/pull/459))
 - Support structs with no fields in `FromStr` derive.
diff --git a/impl/Cargo.toml b/impl/Cargo.toml
index cf92927..21d653a 100644
--- a/impl/Cargo.toml
+++ b/impl/Cargo.toml
@@ -59,7 +59,7 @@ deref_mut = []
 display = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"]
 error = ["syn/extra-traits"]
 from = ["syn/extra-traits"]
-from_str = []
+from_str = ["dep:convert_case"]
 index = []
 index_mut = []
 into = ["syn/extra-traits"]
diff --git a/impl/doc/from_str.md b/impl/doc/from_str.md
index 3c11306..2e76a96 100644
--- a/impl/doc/from_str.md
+++ b/impl/doc/from_str.md
@@ -170,3 +170,61 @@ impl derive_more::core::str::FromStr for Foo {
     }
 }
 ```
+
+
+### The `rename_all` attribute
+
+To control the concrete string representation of the name verbatim,
+the `#[from_str(rename_all = "...")]` attribute can be placed on structs,
+enums and variants.
+
+The available casings are:
+- `lowercase`
+- `UPPERCASE`
+- `PascalCase`
+- `camelCase`
+- `snake_case`
+- `SCREAMING_SNAKE_CASE`
+- `kebab-case`
+- `SCREAMING-KEBAB-CASE`
+
+```rust
+# use derive_more::FromStr;
+#
+#[derive(FromStr, Debug, Eq, PartialEq)]
+#[from_str(rename_all = "lowercase")]
+enum Enum {
+    VariantOne,
+    #[from_str(rename_all = "kebab-case")] // overrides the top-level one
+    VariantTwo
+}
+
+assert_eq!("variantone".parse::<Enum>().unwrap(), Enum::VariantOne);
+assert_eq!("variant-two".parse::<Enum>().unwrap(), Enum::VariantTwo);
+```
+
+> **NOTE**: Using `#[from_str(rename_all = "...")]` attribute disables
+> any case-insensitivity where applied. This is also true for any enum
+> variant whose name or string representation is similar to the variant
+> being marked:
+> ```rust
+> # use derive_more::FromStr;
+> #
+> # #[allow(non_camel_case_types)]
+> #[derive(FromStr, Debug, Eq, PartialEq)]
+> enum Enum {
+>    Foo,  // case-insensitive
+>    #[from_str(rename_all = "SCREAMING_SNAKE_CASE")]
+>    BaR,  // case-sensitive (marked with attribute)
+>    Bar,  // case-sensitive (name is similar to the marked `BaR` variant)
+>    Ba_R, // case-sensitive (string representation is similar to the marked `BaR` variant)
+> }
+> #
+> # assert_eq!("Foo".parse::<Enum>().unwrap(), Enum::Foo);
+> # assert_eq!("FOO".parse::<Enum>().unwrap(), Enum::Foo);
+> # assert_eq!("foo".parse::<Enum>().unwrap(), Enum::Foo);
+> #
+> # assert_eq!("BA_R".parse::<Enum>().unwrap(), Enum::BaR);
+> # assert_eq!("Bar".parse::<Enum>().unwrap(), Enum::Bar);
+> # assert_eq!("Ba_R".parse::<Enum>().unwrap(), Enum::Ba_R);
+> ```
diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs
index 9905d38..2b0dbca 100644
--- a/impl/src/fmt/display.rs
+++ b/impl/src/fmt/display.rs
@@ -3,7 +3,6 @@
 #[cfg(doc)]
 use std::fmt;
 
-use convert_case::Casing;
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote};
 use syn::{
@@ -95,8 +94,8 @@ pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result<TokenSt
 /// while multiple `#[<attribute>(bound(...))]` are allowed.
 #[derive(Debug, Default)]
 struct ContainerAttributes {
-    /// [`RenameAllAttribute`] for case convertion.
-    rename_all: Option<RenameAllAttribute>,
+    /// [`attr::RenameAll`] for case convertion.
+    rename_all: Option<attr::RenameAll>,
 
     /// Common [`ContainerAttributes`].
     ///
@@ -180,80 +179,6 @@ impl attr::ParseMultiple for ContainerAttributes {
     }
 }
 
-/// Representation of a `rename_all` macro attribute.
-///
-/// ```rust,ignore
-/// #[<attribute>(rename_all = "...")]
-/// ```
-///
-/// Possible cases:
-/// - `lowercase`
-/// - `UPPERCASE`
-/// - `PascalCase`
-/// - `camelCase`
-/// - `snake_case`
-/// - `SCREAMING_SNAKE_CASE`
-/// - `kebab-case`
-/// - `SCREAMING-KEBAB-CASE`
-#[derive(Debug, Clone, Copy)]
-enum RenameAllAttribute {
-    Lower,
-    Upper,
-    Pascal,
-    Camel,
-    Snake,
-    ScreamingSnake,
-    Kebab,
-    ScreamingKebab,
-}
-
-impl RenameAllAttribute {
-    /// Converts the provided `name` into the case of this [`RenameAllAttribute`].
-    fn convert_case(&self, name: &str) -> String {
-        let case = match self {
-            Self::Lower => convert_case::Case::Flat,
-            Self::Upper => convert_case::Case::UpperFlat,
-            Self::Pascal => convert_case::Case::Pascal,
-            Self::Camel => convert_case::Case::Camel,
-            Self::Snake => convert_case::Case::Snake,
-            Self::ScreamingSnake => convert_case::Case::UpperSnake,
-            Self::Kebab => convert_case::Case::Kebab,
-            Self::ScreamingKebab => convert_case::Case::UpperKebab,
-        };
-        name.to_case(case)
-    }
-}
-
-impl Parse for RenameAllAttribute {
-    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-        let _ = input.parse::<syn::Path>().and_then(|p| {
-            if p.is_ident("rename_all") {
-                Ok(p)
-            } else {
-                Err(syn::Error::new(
-                    p.span(),
-                    "unknown attribute argument, expected `rename_all = \"...\"`",
-                ))
-            }
-        })?;
-
-        input.parse::<token::Eq>()?;
-
-        let value: LitStr = input.parse()?;
-        Ok(match value.value().replace(['-', '_'], "").to_lowercase().as_str() {
-            "lowercase" => Self::Lower,
-            "uppercase" => Self::Upper,
-            "pascalcase" => Self::Pascal,
-            "camelcase" => Self::Camel,
-            "snakecase" => Self::Snake,
-            "screamingsnakecase" => Self::ScreamingSnake,
-            "kebabcase" => Self::Kebab,
-            "screamingkebabcase" => Self::ScreamingKebab,
-            _ => return Err(syn::Error::new_spanned(value, "unexpected casing expected one of: \"lowercase\", \"UPPERCASE\", \"PascalCase\", \"camelCase\", \"snake_case\", \"SCREAMING_SNAKE_CASE\", \"kebab-case\", or \"SCREAMING-KEBAB-CASE\""))
-        })
-    }
-}
-
 /// Type alias for an expansion context:
 /// - [`ContainerAttributes`].
 /// - Type parameters. Slice of [`syn::Ident`].
diff --git a/impl/src/from_str.rs b/impl/src/from_str.rs
index d21c772..f477c27 100644
--- a/impl/src/from_str.rs
+++ b/impl/src/from_str.rs
@@ -5,10 +5,13 @@ use std::str::FromStr;
 use std::{collections::HashMap, iter};
 
 use proc_macro2::TokenStream;
-use quote::{quote, ToTokens};
+use quote::{format_ident, quote, ToTokens};
 use syn::{parse_quote, spanned::Spanned as _};
 
-use crate::utils::Either;
+use crate::utils::{
+    attr::{self, ParseMultiple as _},
+    Either, Spanning,
+};
 
 /// Expands a [`FromStr`] derive macro.
 pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStream> {
@@ -47,6 +50,16 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for ForwardExpansion<'i> {
                 "expected a struct for forward `FromStr` derive",
             ));
         };
+        if let Some(attr) = input
+            .attrs
+            .iter()
+            .find(|attr| attr.path().is_ident("from_str"))
+        {
+            return Err(syn::Error::new(
+                attr.path().span(),
+                "no attribute is allowed here",
+            ));
+        }
 
         // TODO: Unite these two conditions via `&&` once MSRV is bumped to 1.88 or above.
         if data.fields.len() != 1 {
@@ -106,19 +119,31 @@ struct FlatExpansion<'i> {
     /// [`syn::Ident`]: struct@syn::Ident
     self_ty: (&'i syn::Ident, &'i syn::Generics),
 
-    /// [`syn::Ident`]s along with the matched values (enum variants or struct itself).
+    /// [`syn::Ident`]s along with the matched values (enum variants or struct itself), and
+    /// a value-specific [`attr::RenameAll`] overriding [`FlatExpansion::rename_all`], if any.
     ///
     /// [`syn::Ident`]: struct@syn::Ident
     matches: Vec<(
         &'i syn::Ident,
         Either<&'i syn::DataStruct, &'i syn::Variant>,
+        Option<attr::RenameAll>,
     )>,
+
+    /// [`FlatExpansion::matches`] grouped by its similar representation for detecting whether their
+    /// case-insensitivity should be disabled.
+    similar_matches: HashMap<String, Vec<&'i syn::Ident>>,
+
+    /// Optional [`attr::RenameAll`] indicating the case convertion to be applied to all the matched
+    /// values (enum variants or struct itself).
+    rename_all: Option<attr::RenameAll>,
 }
 
 impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
     type Error = syn::Error;
 
     fn try_from(input: &'i syn::DeriveInput) -> syn::Result<Self> {
+        let attr_ident = &format_ident!("from_str");
+
         let matches = match &input.data {
             syn::Data::Struct(data) => {
                 if !data.fields.is_empty() {
@@ -127,7 +152,7 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
                         "only structs with no fields can derive `FromStr`",
                     ));
                 }
-                vec![(&input.ident, Either::Left(data))]
+                vec![(&input.ident, Either::Left(data), None)]
             }
             syn::Data::Enum(data) => data
                 .variants
@@ -139,7 +164,10 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
                             "only enums with no fields can derive `FromStr`",
                         ));
                     }
-                    Ok((&variant.ident, Either::Right(variant)))
+                    let attr =
+                        attr::RenameAll::parse_attrs(&variant.attrs, attr_ident)?
+                            .map(Spanning::into_inner);
+                    Ok((&variant.ident, Either::Right(variant), attr))
                 })
                 .collect::<syn::Result<_>>()?,
             syn::Data::Union(_) => {
@@ -150,9 +178,64 @@ impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
             }
         };
 
+        let rename_all = attr::RenameAll::parse_attrs(&input.attrs, attr_ident)?
+            .map(Spanning::into_inner);
+
+        let mut similar_matches = <HashMap<_, Vec<_>>>::new();
+        if rename_all.is_none() {
+            for (ident, _, renaming) in &matches {
+                let name = ident.to_string();
+                let lowercased = name.to_lowercase();
+                if let Some(rename) = renaming {
+                    let renamed_lowercased = rename.convert_case(&name);
+                    if renamed_lowercased != lowercased {
+                        similar_matches
+                            .entry(renamed_lowercased)
+                            .or_default()
+                            .push(*ident);
+                    }
+                }
+                similar_matches.entry(lowercased).or_default().push(*ident);
+            }
+        }
+
+        let mut exact_matches = <HashMap<String, Vec<String>>>::new();
+        for (ident, _, renaming) in &matches {
+            let name = ident.to_string();
+            let exact = if let Some(default_renaming) = &rename_all {
+                renaming
+                    .as_ref()
+                    .unwrap_or(default_renaming)
+                    .convert_case(&name)
+            } else if let Some(renaming) = renaming {
+                renaming.convert_case(&name)
+            } else {
+                let lowercased = name.to_lowercase();
+                if similar_matches[&lowercased].len() > 1 {
+                    name.clone()
+                } else {
+                    lowercased
+                }
+            };
+            exact_matches.entry(exact).or_default().push(name);
+        }
+        if let Some((string, variants)) =
+            exact_matches.into_iter().find(|(_, vs)| vs.len() > 1)
+        {
+            return Err(syn::Error::new(
+                input.ident.span(),
+                format!(
+                    "`{}` variants cannot have the same \"{string}\" string representation",
+                    variants.join("`, `"),
+                ),
+            ));
+        }
+
         Ok(Self {
             self_ty: (&input.ident, &input.generics),
             matches,
+            similar_matches,
+            rename_all,
         })
     }
 }
@@ -165,25 +248,42 @@ impl ToTokens for FlatExpansion<'_> {
             self.self_ty.1.split_for_impl();
         let ty_name = ty.to_string();
 
-        let similar_lowercased = self
-            .matches
-            .iter()
-            .map(|(v, _)| v.to_string().to_lowercase())
-            .fold(<HashMap<_, u8>>::new(), |mut counts, v| {
-                *counts.entry(v).or_default() += 1;
-                counts
-            });
-
-        let match_arms = self.matches.iter().map(|(ident, value)| {
-            let name = ident.to_string();
-            let lowercased = name.to_lowercase();
-
-            let exact_guard =
-                (similar_lowercased[&lowercased] > 1).then(|| quote! { if s == #name });
-            let constructor = value.self_constructor_empty();
+        let scrutinee_lowercased = self
+            .rename_all
+            .is_none()
+            .then(|| quote! { .to_lowercase().as_str() });
+        let match_arms = if let Some(default_renaming) = self.rename_all {
+            self.matches
+                .iter()
+                .map(|(ident, value, renaming)| {
+                    let converted = renaming
+                        .unwrap_or(default_renaming)
+                        .convert_case(&ident.to_string());
+                    let constructor = value.self_constructor_empty();
 
-            quote! { #lowercased #exact_guard => #constructor, }
-        });
+                    quote! { #converted => #constructor, }
+                })
+                .collect::<Vec<_>>()
+        } else {
+            self.matches
+                .iter()
+                .map(|(ident, value, renaming)| {
+                    let name = ident.to_string();
+                    let constructor = value.self_constructor_empty();
+                    if let Some(rename) = renaming {
+                        let exact_name = rename.convert_case(&name);
+
+                        quote! { _ if s == #exact_name => #constructor, }
+                    } else {
+                        let lowercased = name.to_lowercase();
+                        let exact_guard = (self.similar_matches[&lowercased].len() > 1)
+                            .then(|| quote! { if s == #name });
+
+                        quote! { #lowercased #exact_guard => #constructor, }
+                    }
+                })
+                .collect()
+        };
 
         quote! {
             #[allow(unreachable_code)] // for empty enums
@@ -194,7 +294,7 @@ impl ToTokens for FlatExpansion<'_> {
                 fn from_str(
                     s: &str,
                 ) -> derive_more::core::result::Result<Self, derive_more::FromStrError> {
-                    derive_more::core::result::Result::Ok(match s.to_lowercase().as_str() {
+                    derive_more::core::result::Result::Ok(match s #scrutinee_lowercased {
                         #( #match_arms )*
                         _ => return derive_more::core::result::Result::Err(
                             derive_more::FromStrError::new(#ty_name),
diff --git a/impl/src/lib.rs b/impl/src/lib.rs
index eeccdb0..8a40cad 100644
--- a/impl/src/lib.rs
+++ b/impl/src/lib.rs
@@ -188,7 +188,7 @@ create_derive!("error", error, Error, error_derive, error);
 
 create_derive!("from", from, From, from_derive, from);
 
-create_derive!("from_str", from_str, FromStr, from_str_derive);
+create_derive!("from_str", from_str, FromStr, from_str_derive, from_str);
 
 create_derive!("index", index, Index, index_derive, index);
 
diff --git a/impl/src/utils.rs b/impl/src/utils.rs
index cc5f35b..d178c33 100644
--- a/impl/src/utils.rs
+++ b/impl/src/utils.rs
@@ -31,6 +31,7 @@ pub(crate) use self::generics_search::GenericsSearch;
     feature = "debug",
     feature = "display",
     feature = "from",
+    feature = "from_str",
     feature = "into",
     feature = "try_from",
 ))]
@@ -1373,6 +1374,7 @@ mod either {
     feature = "debug",
     feature = "display",
     feature = "from",
+    feature = "from_str",
     feature = "into",
     feature = "try_from",
 ))]
@@ -1468,6 +1470,7 @@ mod spanning {
     feature = "debug",
     feature = "display",
     feature = "from",
+    feature = "from_str",
     feature = "into",
     feature = "try_from",
 ))]
@@ -1488,6 +1491,8 @@ pub(crate) mod attr {
         feature = "try_from"
     ))]
     pub(crate) use self::empty::Empty;
+    #[cfg(any(feature = "display", feature = "from_str"))]
+    pub(crate) use self::rename_all::RenameAll;
     #[cfg(any(
         feature = "as_ref",
         feature = "debug",
@@ -2157,6 +2162,105 @@ pub(crate) mod attr {
             }
         }
     }
+
+    #[cfg(any(feature = "display", feature = "from_str"))]
+    mod rename_all {
+        use syn::{
+            parse::{Parse, ParseStream},
+            spanned::Spanned as _,
+            token,
+        };
+
+        use super::ParseMultiple;
+
+        /// Representation of a `rename_all` macro attribute.
+        ///
+        /// ```rust,ignore
+        /// #[<attribute>(rename_all = "...")]
+        /// ```
+        ///
+        /// Possible cases:
+        /// - `lowercase`
+        /// - `UPPERCASE`
+        /// - `PascalCase`
+        /// - `camelCase`
+        /// - `snake_case`
+        /// - `SCREAMING_SNAKE_CASE`
+        /// - `kebab-case`
+        /// - `SCREAMING-KEBAB-CASE`
+        #[derive(Clone, Copy, Debug)]
+        pub(crate) enum RenameAll {
+            Lower,
+            Upper,
+            Pascal,
+            Camel,
+            Snake,
+            ScreamingSnake,
+            Kebab,
+            ScreamingKebab,
+        }
+
+        impl RenameAll {
+            /// Converts the provided `name` into the case of this [`RenameAll`].
+            pub(crate) fn convert_case(&self, name: &str) -> String {
+                use convert_case::Casing as _;
+
+                let case = match self {
+                    Self::Lower => convert_case::Case::Flat,
+                    Self::Upper => convert_case::Case::UpperFlat,
+                    Self::Pascal => convert_case::Case::Pascal,
+                    Self::Camel => convert_case::Case::Camel,
+                    Self::Snake => convert_case::Case::Snake,
+                    Self::ScreamingSnake => convert_case::Case::UpperSnake,
+                    Self::Kebab => convert_case::Case::Kebab,
+                    Self::ScreamingKebab => convert_case::Case::UpperKebab,
+                };
+                name.to_case(case)
+            }
+        }
+
+        impl Parse for RenameAll {
+            fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+                let _ = input.parse::<syn::Path>().and_then(|p| {
+                    if p.is_ident("rename_all") {
+                        Ok(p)
+                    } else {
+                        Err(syn::Error::new(
+                            p.span(),
+                            "unknown attribute argument, expected `rename_all = \"...\"`",
+                        ))
+                    }
+                })?;
+
+                input.parse::<token::Eq>()?;
+
+                let lit: syn::LitStr = input.parse()?;
+                Ok(
+                    match lit.value().replace(['-', '_'], "").to_lowercase().as_str() {
+                        "lowercase" => Self::Lower,
+                        "uppercase" => Self::Upper,
+                        "pascalcase" => Self::Pascal,
+                        "camelcase" => Self::Camel,
+                        "snakecase" => Self::Snake,
+                        "screamingsnakecase" => Self::ScreamingSnake,
+                        "kebabcase" => Self::Kebab,
+                        "screamingkebabcase" => Self::ScreamingKebab,
+                        _ => {
+                            return Err(syn::Error::new_spanned(
+                                lit,
+                                "unexpected casing expected one of: \
+                         \"lowercase\", \"UPPERCASE\", \"PascalCase\", \"camelCase\", \
+                         \"snake_case\", \"SCREAMING_SNAKE_CASE\", \"kebab-case\", or \
+                         \"SCREAMING-KEBAB-CASE\"",
+                            ))
+                        }
+                    },
+                )
+            }
+        }
+
+        impl ParseMultiple for RenameAll {}
+    }
 }
 
 #[cfg(any(feature = "from", feature = "into"))]


Test patch:
diff --git a/tests/compile_fail/from_str/enum_invalid_casing.rs b/tests/compile_fail/from_str/enum_invalid_casing.rs
new file mode 100644
index 0000000..b00bd13
--- /dev/null
+++ b/tests/compile_fail/from_str/enum_invalid_casing.rs
@@ -0,0 +1,7 @@
+#[derive(derive_more::FromStr)]
+#[from_str(rename_all = "Whatever")]
+enum Enum {
+    UnitVariant,
+}
+
+fn main() {}
diff --git a/tests/compile_fail/from_str/enum_invalid_casing.stderr b/tests/compile_fail/from_str/enum_invalid_casing.stderr
new file mode 100644
index 0000000..71c4b9a
--- /dev/null
+++ b/tests/compile_fail/from_str/enum_invalid_casing.stderr
@@ -0,0 +1,5 @@
+error: unexpected casing expected one of: "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", or "SCREAMING-KEBAB-CASE"
+ --> tests/compile_fail/from_str/enum_invalid_casing.rs:2:25
+  |
+2 | #[from_str(rename_all = "Whatever")]
+  |                         ^^^^^^^^^^
diff --git a/tests/compile_fail/from_str/enum_invalid_variant_casing.rs b/tests/compile_fail/from_str/enum_invalid_variant_casing.rs
new file mode 100644
index 0000000..e388db4
--- /dev/null
+++ b/tests/compile_fail/from_str/enum_invalid_variant_casing.rs
@@ -0,0 +1,7 @@
+#[derive(derive_more::FromStr)]
+enum Enum {
+    #[from_str(rename_all = "Whatever")]
+    UnitVariant,
+}
+
+fn main() {}
diff --git a/tests/compile_fail/from_str/enum_invalid_variant_casing.stderr b/tests/compile_fail/from_str/enum_invalid_variant_casing.stderr
new file mode 100644
index 0000000..d50ae90
--- /dev/null
+++ b/tests/compile_fail/from_str/enum_invalid_variant_casing.stderr
@@ -0,0 +1,5 @@
+error: unexpected casing expected one of: "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", or "SCREAMING-KEBAB-CASE"
+ --> tests/compile_fail/from_str/enum_invalid_variant_casing.rs:3:29
+  |
+3 |     #[from_str(rename_all = "Whatever")]
+  |                             ^^^^^^^^^^
diff --git a/tests/compile_fail/from_str/enum_same_string_representation.rs b/tests/compile_fail/from_str/enum_same_string_representation.rs
new file mode 100644
index 0000000..19b9ec0
--- /dev/null
+++ b/tests/compile_fail/from_str/enum_same_string_representation.rs
@@ -0,0 +1,17 @@
+#![allow(non_camel_case_types)]
+
+#[derive(derive_more::FromStr)]
+enum Enum {
+    abc,
+    #[from_str(rename_all = "lowercase")]
+    AB_C,
+}
+
+#[derive(derive_more::FromStr)]
+#[from_str(rename_all = "SCREAMING_SNAKE_CASE")]
+enum EnumTop {
+    BaR,
+    BA_R,
+}
+
+fn main() {}
diff --git a/tests/compile_fail/from_str/enum_same_string_representation.stderr b/tests/compile_fail/from_str/enum_same_string_representation.stderr
new file mode 100644
index 0000000..5822047
--- /dev/null
+++ b/tests/compile_fail/from_str/enum_same_string_representation.stderr
@@ -0,0 +1,11 @@
+error: `abc`, `AB_C` variants cannot have the same "abc" string representation
+ --> tests/compile_fail/from_str/enum_same_string_representation.rs:4:6
+  |
+4 | enum Enum {
+  |      ^^^^
+
+error: `BaR`, `BA_R` variants cannot have the same "BA_R" string representation
+  --> tests/compile_fail/from_str/enum_same_string_representation.rs:12:6
+   |
+12 | enum EnumTop {
+   |      ^^^^^^^
diff --git a/tests/compile_fail/from_str/enum_unknown_attribute.rs b/tests/compile_fail/from_str/enum_unknown_attribute.rs
new file mode 100644
index 0000000..27c85f2
--- /dev/null
+++ b/tests/compile_fail/from_str/enum_unknown_attribute.rs
@@ -0,0 +1,7 @@
+#[derive(derive_more::FromStr)]
+#[from_str(unknown = "unknown")]
+pub enum Foo {
+    Bar,
+}
+
+fn main() {}
diff --git a/tests/compile_fail/from_str/enum_unknown_attribute.stderr b/tests/compile_fail/from_str/enum_unknown_attribute.stderr
new file mode 100644
index 0000000..bde3c7d
--- /dev/null
+++ b/tests/compile_fail/from_str/enum_unknown_attribute.stderr
@@ -0,0 +1,5 @@
+error: unknown attribute argument, expected `rename_all = "..."`
+ --> tests/compile_fail/from_str/enum_unknown_attribute.rs:2:12
+  |
+2 | #[from_str(unknown = "unknown")]
+  |            ^^^^^^^
diff --git a/tests/compile_fail/from_str/struct_flat_unknown_attribute.rs b/tests/compile_fail/from_str/struct_flat_unknown_attribute.rs
new file mode 100644
index 0000000..97f393f
--- /dev/null
+++ b/tests/compile_fail/from_str/struct_flat_unknown_attribute.rs
@@ -0,0 +1,5 @@
+#[derive(derive_more::FromStr)]
+#[from_str(unknown = "unknown")]
+struct Foo;
+
+fn main() {}
diff --git a/tests/compile_fail/from_str/struct_flat_unknown_attribute.stderr b/tests/compile_fail/from_str/struct_flat_unknown_attribute.stderr
new file mode 100644
index 0000000..c79e095
--- /dev/null
+++ b/tests/compile_fail/from_str/struct_flat_unknown_attribute.stderr
@@ -0,0 +1,5 @@
+error: unknown attribute argument, expected `rename_all = "..."`
+ --> tests/compile_fail/from_str/struct_flat_unknown_attribute.rs:2:12
+  |
+2 | #[from_str(unknown = "unknown")]
+  |            ^^^^^^^
diff --git a/tests/compile_fail/from_str/struct_invalid_casing.rs b/tests/compile_fail/from_str/struct_invalid_casing.rs
new file mode 100644
index 0000000..9f110a9
--- /dev/null
+++ b/tests/compile_fail/from_str/struct_invalid_casing.rs
@@ -0,0 +1,5 @@
+#[derive(derive_more::FromStr)]
+#[from_str(rename_all = "Whatever")]
+struct Foo;
+
+fn main() {}
diff --git a/tests/compile_fail/from_str/struct_invalid_casing.stderr b/tests/compile_fail/from_str/struct_invalid_casing.stderr
new file mode 100644
index 0000000..6ee8fd4
--- /dev/null
+++ b/tests/compile_fail/from_str/struct_invalid_casing.stderr
@@ -0,0 +1,5 @@
+error: unexpected casing expected one of: "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", or "SCREAMING-KEBAB-CASE"
+ --> tests/compile_fail/from_str/struct_invalid_casing.rs:2:25
+  |
+2 | #[from_str(rename_all = "Whatever")]
+  |                         ^^^^^^^^^^
diff --git a/tests/compile_fail/from_str/struct_non_flat_with_attribute.rs b/tests/compile_fail/from_str/struct_non_flat_with_attribute.rs
new file mode 100644
index 0000000..318be69
--- /dev/null
+++ b/tests/compile_fail/from_str/struct_non_flat_with_attribute.rs
@@ -0,0 +1,7 @@
+#[derive(derive_more::FromStr)]
+#[from_str(rename_all = "lowercase")]
+pub struct Foo {
+    bar: i32,
+}
+
+fn main() {}
diff --git a/tests/compile_fail/from_str/struct_non_flat_with_attribute.stderr b/tests/compile_fail/from_str/struct_non_flat_with_attribute.stderr
new file mode 100644
index 0000000..1ba0cf8
--- /dev/null
+++ b/tests/compile_fail/from_str/struct_non_flat_with_attribute.stderr
@@ -0,0 +1,5 @@
+error: no attribute is allowed here
+ --> tests/compile_fail/from_str/struct_non_flat_with_attribute.rs:2:3
+  |
+2 | #[from_str(rename_all = "lowercase")]
+  |   ^^^^^^^^
diff --git a/tests/from_str.rs b/tests/from_str.rs
index ef06877..383ed04 100644
--- a/tests/from_str.rs
+++ b/tests/from_str.rs
@@ -183,6 +183,66 @@ mod structs {
                 "Invalid `Foo` string representation",
             );
         }
+
+        mod rename_all {
+            use super::*;
+
+            #[test]
+            fn case_sensitive() {
+                #[derive(Debug, Eq, FromStr, PartialEq)]
+                #[from_str(rename_all = "PascalCase")]
+                struct Foo;
+
+                assert_eq!("Foo".parse::<Foo>().unwrap(), Foo);
+
+                assert_eq!(
+                    "foo".parse::<Foo>().unwrap_err().to_string(),
+                    "Invalid `Foo` string representation",
+                );
+                assert_eq!(
+                    "FOO".parse::<Foo>().unwrap_err().to_string(),
+                    "Invalid `Foo` string representation",
+                );
+                assert_eq!(
+                    "FoO".parse::<Foo>().unwrap_err().to_string(),
+                    "Invalid `Foo` string representation",
+                );
+                assert_eq!(
+                    "other".parse::<Foo>().unwrap_err().to_string(),
+                    "Invalid `Foo` string representation",
+                );
+            }
+
+            mod casing {
+                use super::*;
+
+                macro_rules! casing_test {
+                    ($name:ident, $casing:literal, $input:literal) => {
+                        mod $name {
+                            use super::*;
+
+                            #[test]
+                            fn top_level() {
+                                #[derive(Debug, Eq, FromStr, PartialEq)]
+                                #[from_str(rename_all = $casing)]
+                                struct FooBar;
+
+                                assert_eq!($input.parse::<FooBar>().unwrap(), FooBar);
+                            }
+                        }
+                    };
+                }
+
+                casing_test!(lower_case, "lowercase", "foobar");
+                casing_test!(upper_case, "UPPERCASE", "FOOBAR");
+                casing_test!(pascal_case, "PascalCase", "FooBar");
+                casing_test!(camel_case, "camelCase", "fooBar");
+                casing_test!(snake_case, "snake_case", "foo_bar");
+                casing_test!(screaming_snake_case, "SCREAMING_SNAKE_CASE", "FOO_BAR");
+                casing_test!(kebab_case, "kebab-case", "foo-bar");
+                casing_test!(screaming_kebab_case, "SCREAMING-KEBAB-CASE", "FOO-BAR");
+            }
+        }
     }
 }
 
@@ -325,5 +385,216 @@ mod enums {
                 "Invalid `Enum` string representation",
             );
         }
+
+        mod rename_all {
+            use super::*;
+
+            #[test]
+            fn variant_case_sensitive() {
+                #[derive(Debug, Eq, FromStr, PartialEq)]
+                enum Enum {
+                    Foo,
+                    #[from_str(rename_all = "UPPERCASE")]
+                    Bar,
+                }
+
+                assert_eq!("foo".parse::<Enum>().unwrap(), Enum::Foo);
+                assert_eq!("Foo".parse::<Enum>().unwrap(), Enum::Foo);
+                assert_eq!("FOO".parse::<Enum>().unwrap(), Enum::Foo);
+
+                assert_eq!("BAR".parse::<Enum>().unwrap(), Enum::Bar);
+
+                assert_eq!(
+                    "Bar".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "bar".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "other".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+            }
+
+            #[test]
+            fn similar_variant_both_case_sensitive() {
+                #[derive(Debug, Eq, FromStr, PartialEq)]
+                enum Enum {
+                    Xyz,
+                    #[from_str(rename_all = "kebab-case")]
+                    XyZ,
+                }
+
+                assert_eq!("Xyz".parse::<Enum>().unwrap(), Enum::Xyz);
+                assert_eq!("xy-z".parse::<Enum>().unwrap(), Enum::XyZ);
+
+                assert_eq!(
+                    "xyz".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "XyZ".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "other".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+            }
+
+            #[test]
+            fn similar_string_both_case_sensitive() {
+                #[allow(non_camel_case_types)]
+                #[derive(Debug, Eq, FromStr, PartialEq)]
+                enum Enum {
+                    Abc,
+                    #[from_str(rename_all = "lowercase")]
+                    AB_C,
+                }
+
+                assert_eq!("Abc".parse::<Enum>().unwrap(), Enum::Abc);
+                assert_eq!("abc".parse::<Enum>().unwrap(), Enum::AB_C);
+
+                assert_eq!(
+                    "ABC".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "AB_C".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "other".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+            }
+
+            #[test]
+            fn variant_overrides_top_level() {
+                #[derive(Debug, Eq, FromStr, PartialEq)]
+                #[from_str(rename_all = "snake_case")]
+                enum Enum {
+                    Foo,
+                    #[from_str(rename_all = "UPPERCASE")]
+                    Bar,
+                    Baz,
+                    BaZ,
+                }
+
+                assert_eq!("foo".parse::<Enum>().unwrap(), Enum::Foo);
+                assert_eq!("BAR".parse::<Enum>().unwrap(), Enum::Bar);
+                assert_eq!("baz".parse::<Enum>().unwrap(), Enum::Baz);
+                assert_eq!("ba_z".parse::<Enum>().unwrap(), Enum::BaZ);
+
+                assert_eq!(
+                    "Foo".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "Bar".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "bar".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "Baz".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "BaZ".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+                assert_eq!(
+                    "other".parse::<Enum>().unwrap_err().to_string(),
+                    "Invalid `Enum` string representation",
+                );
+            }
+
+            mod casing {
+                use super::*;
+
+                macro_rules! casing_test {
+                    ($name:ident, $casing:literal, $VariantOne:literal, $Two:literal) => {
+                        mod $name {
+                            use super::*;
+
+                            #[test]
+                            fn top_level() {
+                                #[derive(Debug, Eq, FromStr, PartialEq)]
+                                #[from_str(rename_all = $casing)]
+                                enum Enum {
+                                    VariantOne,
+                                    Two,
+                                }
+
+                                assert_eq!(
+                                    $VariantOne.parse::<Enum>().unwrap(),
+                                    Enum::VariantOne,
+                                );
+                                assert_eq!($Two.parse::<Enum>().unwrap(), Enum::Two);
+                            }
+
+                            #[test]
+                            fn variant_level() {
+                                #[derive(Debug, Eq, FromStr, PartialEq)]
+                                #[from_str(rename_all = "lowercase")] // ignored
+                                enum Enum {
+                                    #[from_str(rename_all = $casing)]
+                                    VariantOne,
+                                    #[from_str(rename_all = $casing)]
+                                    Two,
+                                }
+
+                                assert_eq!(
+                                    $VariantOne.parse::<Enum>().unwrap(),
+                                    Enum::VariantOne,
+                                );
+                                assert_eq!($Two.parse::<Enum>().unwrap(), Enum::Two);
+                            }
+
+                            #[test]
+                            fn only_variant_level() {
+                                #[derive(Debug, Eq, FromStr, PartialEq)]
+                                enum Enum {
+                                    #[from_str(rename_all = $casing)]
+                                    VariantOne,
+                                    #[from_str(rename_all = $casing)]
+                                    Two,
+                                }
+
+                                assert_eq!(
+                                    $VariantOne.parse::<Enum>().unwrap(),
+                                    Enum::VariantOne,
+                                );
+                                assert_eq!($Two.parse::<Enum>().unwrap(), Enum::Two);
+                            }
+                        }
+                    };
+                }
+
+                casing_test!(lower_case, "lowercase", "variantone", "two");
+                casing_test!(upper_case, "UPPERCASE", "VARIANTONE", "TWO");
+                casing_test!(pascal_case, "PascalCase", "VariantOne", "Two");
+                casing_test!(camel_case, "camelCase", "variantOne", "two");
+                casing_test!(snake_case, "snake_case", "variant_one", "two");
+                casing_test!(
+                    screaming_snake_case,
+                    "SCREAMING_SNAKE_CASE",
+                    "VARIANT_ONE",
+                    "TWO"
+                );
+                casing_test!(kebab_case, "kebab-case", "variant-one", "two");
+                casing_test!(
+                    screaming_kebab_case,
+                    "SCREAMING-KEBAB-CASE",
+                    "VARIANT-ONE",
+                    "TWO"
+                );
+            }
+        }
     }
 }