Repository: JelteF/derive_more
Pull request #469 (2025-05-07 17:30:57)

Support flat structs in `FromStr`
Required for #467  
Requires #468

## Synopsis

We support flat structs for `Display`:
```rust
#[derive(Display)]
struct Foo;
```
But `FromStr` doesn't support it.

## Solution

Allow using `#[derive(FromStr)]` on structs with no fields.

Similarly to enums it will use fuzzy matching. Exact matching would be possible to enable in #469 by using `#[from_str(rename_all = "...")]` attribute.

## Checklist

- [x] Documentation is updated
- [x] Tests are added/updated
- [x] [CHANGELOG entry](/CHANGELOG.md) is added


tyranron: ping @JelteF 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ae4a38..f599235 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
   ([#443](https://github.com/JelteF/derive_more/pull/443))
 - 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.
+  ([#469](https://github.com/JelteF/derive_more/pull/469))
 
 ### Changed
 
diff --git a/impl/doc/from_str.md b/impl/doc/from_str.md
index 792fce4..3c11306 100644
--- a/impl/doc/from_str.md
+++ b/impl/doc/from_str.md
@@ -1,85 +1,69 @@
 # What `#[derive(FromStr)]` generates
 
-Deriving `FromStr` only works for enums with no fields
-or newtypes, i.e structs with only a single
-field. The result is that you will be able to call the `parse()` method on a
-string to convert it to your newtype. This only works when the type that is
-contained in the type implements `FromStr`.
+Deriving `FromStr` only works for enums/structs with no fields
+or newtypes (structs with only a single field). The result is
+that you will be able to call the `parse()` method on a string
+to convert it to your newtype. This only works when the wrapped
+type implements `FromStr` itself.
 
 
 
 
-## Example usage
-
-```rust
-# use derive_more::FromStr;
-#
-#[derive(FromStr, Debug, Eq, PartialEq)]
-struct MyInt(i32);
-
-#[derive(FromStr, Debug, Eq, PartialEq)]
-struct Point1D{
-    x: i32,
-}
-
-assert_eq!(MyInt(5), "5".parse().unwrap());
-assert_eq!(Point1D{x: 100}, "100".parse().unwrap());
-```
-
+## Forwarding
 
+Deriving forwarding implementation is only supported for newtypes
+(structs with only a single field).
 
 
-## Tuple structs
+### Tuple structs
 
 When deriving `FromStr` for a tuple struct with one field:
-
 ```rust
 # use derive_more::FromStr;
 #
-#[derive(FromStr)]
+#[derive(FromStr, Debug, Eq, PartialEq)]
 struct MyInt(i32);
-```
 
-Code like this will be generated:
+assert_eq!("5".parse::<MyInt>().unwrap(), MyInt(5));
+```
 
+Code like this is generated:
 ```rust
 # struct MyInt(i32);
 impl derive_more::core::str::FromStr for MyInt {
     type Err = <i32 as derive_more::core::str::FromStr>::Err;
-    fn from_str(src: &str) -> Result<Self, <i32 as derive_more::core::str::FromStr>::Err> {
-        return Ok(MyInt(i32::from_str(src)?));
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(Self(i32::from_str(s)?))
     }
 }
 ```
 
 
-
-
-## Regular structs
+### Regular structs
 
 When deriving `FromStr` for a regular struct with one field:
-
 ```rust
 # use derive_more::FromStr;
 #
-#[derive(FromStr)]
+#[derive(FromStr, Debug, Eq, PartialEq)]
 struct Point1D {
     x: i32,
 }
-```
 
-Code like this will be generated:
+assert_eq!("100".parse::<Point1D>().unwrap(), Point1D { x: 100 });
+```
 
+Code like this is generated:
 ```rust
 # struct Point1D {
 #     x: i32,
 # }
 impl derive_more::core::str::FromStr for Point1D {
     type Err = <i32 as derive_more::core::str::FromStr>::Err;
-    fn from_str(src: &str) -> Result<Self, <i32 as derive_more::core::str::FromStr>::Err> {
-        return Ok(Point1D {
-            x: i32::from_str(src)?,
-        });
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(Self {
+            x: i32::from_str(s)?,
+        })
     }
 }
 ```
@@ -87,49 +71,102 @@ impl derive_more::core::str::FromStr for Point1D {
 
 
 
-## Enums
+## Flat representation
 
-When deriving `FromStr` for an enums with variants with no fields it will
-generate a `from_str` method that converts strings that match the variant name
-to the variant. If using a case insensitive match would give a unique variant
-(i.e you dont have both a `MyEnum::Foo` and a `MyEnum::foo` variant) then case
-insensitive matching will be used, otherwise it will fall back to exact string
-matching.
+Deriving flat string representation is only supported for empty enums and
+structs (with no fields).
 
-Since the string may not match any variants an error type is needed, so the
-`derive_more::FromStrError` will be used for that purpose.
 
-e.g. Given the following enum:
+### Empty enums
+
+When deriving `FromStr` for enums with empty variants, it will generate a
+`from_str()` method converting strings matching the variant name to the variant.
+If using a case-insensitive match would give a unique variant (i.e. you don't have
+both `MyEnum::Foo` and `MyEnum::foo` variants), then case-insensitive matching will
+be used, otherwise it will fall back to exact string matching.
 
+Since the string may not match any variants an error type is needed, so the
+`derive_more::FromStrError` is used for that purpose.
+
+Given the following enum:
 ```rust
 # use derive_more::FromStr;
 #
-#[derive(FromStr)]
+#[derive(FromStr, Debug, Eq, PartialEq)]
 enum EnumNoFields {
     Foo,
     Bar,
     Baz,
+    BaZ,
 }
-```
 
-Code like this will be generated:
+assert_eq!("foo".parse::<EnumNoFields>().unwrap(), EnumNoFields::Foo);
+assert_eq!("Foo".parse::<EnumNoFields>().unwrap(), EnumNoFields::Foo);
+assert_eq!("FOO".parse::<EnumNoFields>().unwrap(), EnumNoFields::Foo);
+
+assert_eq!("Bar".parse::<EnumNoFields>().unwrap(), EnumNoFields::Bar);
+assert_eq!("bar".parse::<EnumNoFields>().unwrap(), EnumNoFields::Bar);
+
+assert_eq!("Baz".parse::<EnumNoFields>().unwrap(), EnumNoFields::Baz);
+assert_eq!("BaZ".parse::<EnumNoFields>().unwrap(), EnumNoFields::BaZ);
+assert_eq!(
+    "other".parse::<EnumNoFields>().unwrap_err().to_string(),
+    "Invalid `EnumNoFields` string representation",
+);
+```
 
+Code like this is generated:
 ```rust
 # enum EnumNoFields {
 #     Foo,
 #     Bar,
 #     Baz,
+#     BaZ,
 # }
 #
 impl derive_more::core::str::FromStr for EnumNoFields {
     type Err = derive_more::FromStrError;
-    fn from_str(src: &str) -> Result<Self, derive_more::FromStrError> {
-        Ok(match src.to_lowercase().as_str() {
-            "foo" => EnumNoFields::Foo,
-            "bar" => EnumNoFields::Bar,
-            "baz" => EnumNoFields::Baz,
+    fn from_str(s: &str) -> Result<Self, derive_more::FromStrError> {
+        Ok(match s.to_lowercase().as_str() {
+            "foo" => Self::Foo,
+            "bar" => Self::Bar,
+            "baz" if s == "Baz" => Self::Baz,
+            "baz" if s == "BaZ" => Self::BaZ,
             _ => return Err(derive_more::FromStrError::new("EnumNoFields")),
         })
     }
 }
 ```
+
+
+### Empty structs
+
+Deriving `FromStr` for structs with no fields is similar to enums,
+but involves only case-insensitive matching by now.
+
+Given the following struct:
+```rust
+# use derive_more::FromStr;
+#
+#[derive(FromStr, Debug, Eq, PartialEq)]
+struct Foo;
+
+assert_eq!("foo".parse::<Foo>().unwrap(), Foo);
+assert_eq!("Foo".parse::<Foo>().unwrap(), Foo);
+assert_eq!("FOO".parse::<Foo>().unwrap(), Foo);
+```
+
+Code like this is generated:
+```rust
+# struct Foo;
+#
+impl derive_more::core::str::FromStr for Foo {
+    type Err = derive_more::FromStrError;
+    fn from_str(s: &str) -> Result<Self, derive_more::FromStrError> {
+        Ok(match s.to_lowercase().as_str() {
+            "foo" => Self,
+            _ => return Err(derive_more::FromStrError::new("Foo")),
+        })
+    }
+}
+```
diff --git a/impl/src/from_str.rs b/impl/src/from_str.rs
index ef9db55..d21c772 100644
--- a/impl/src/from_str.rs
+++ b/impl/src/from_str.rs
@@ -1,22 +1,24 @@
 //! Implementation of a [`FromStr`] derive macro.
 
-use std::collections::HashMap;
 #[cfg(doc)]
 use std::str::FromStr;
+use std::{collections::HashMap, iter};
 
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
 use syn::{parse_quote, spanned::Spanned as _};
 
+use crate::utils::Either;
+
 /// Expands a [`FromStr`] derive macro.
 pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStream> {
     match &input.data {
-        syn::Data::Struct(_) => {
-            Ok(ForwardExpansion::try_from(input)?.into_token_stream())
-        }
-        syn::Data::Enum(_) => {
-            Ok(EnumFlatExpansion::try_from(input)?.into_token_stream())
-        }
+        syn::Data::Struct(data) => Ok(if data.fields.is_empty() {
+            FlatExpansion::try_from(input)?.into_token_stream()
+        } else {
+            ForwardExpansion::try_from(input)?.into_token_stream()
+        }),
+        syn::Data::Enum(_) => Ok(FlatExpansion::try_from(input)?.into_token_stream()),
         syn::Data::Union(data) => Err(syn::Error::new(
             data.union_token.span(),
             "`FromStr` cannot be derived for unions",
@@ -81,11 +83,7 @@ impl ToTokens for ForwardExpansion<'_> {
         }
         let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
 
-        let constructor = if let Some(name) = &self.inner.ident {
-            quote! { Self { #name: v } }
-        } else {
-            quote! { Self(v) }
-        };
+        let constructor = self.inner.self_constructor([parse_quote! { v }]);
 
         quote! {
             #[automatically_derived]
@@ -101,52 +99,65 @@ impl ToTokens for ForwardExpansion<'_> {
     }
 }
 
-/// Expansion of a macro for generating a flat [`FromStr`] implementation of an enum.
-struct EnumFlatExpansion<'i> {
-    /// [`syn::Ident`] and [`syn::Generics`] of the enum.
+/// Expansion of a macro for generating a flat [`FromStr`] implementation of an enum or a struct.
+struct FlatExpansion<'i> {
+    /// [`syn::Ident`] and [`syn::Generics`] of the enum/struct.
     ///
     /// [`syn::Ident`]: struct@syn::Ident
     self_ty: (&'i syn::Ident, &'i syn::Generics),
 
-    /// [`syn::Ident`]s of the enum variants.
+    /// [`syn::Ident`]s along with the matched values (enum variants or struct itself).
     ///
     /// [`syn::Ident`]: struct@syn::Ident
-    variants: Vec<&'i syn::Ident>,
+    matches: Vec<(
+        &'i syn::Ident,
+        Either<&'i syn::DataStruct, &'i syn::Variant>,
+    )>,
 }
 
-impl<'i> TryFrom<&'i syn::DeriveInput> for EnumFlatExpansion<'i> {
+impl<'i> TryFrom<&'i syn::DeriveInput> for FlatExpansion<'i> {
     type Error = syn::Error;
 
     fn try_from(input: &'i syn::DeriveInput) -> syn::Result<Self> {
-        let syn::Data::Enum(data) = &input.data else {
-            return Err(syn::Error::new(
-                input.span(),
-                "expected an enum for flat `FromStr` derive",
-            ));
-        };
-
-        let variants = data
-            .variants
-            .iter()
-            .map(|variant| {
-                if !variant.fields.is_empty() {
+        let matches = match &input.data {
+            syn::Data::Struct(data) => {
+                if !data.fields.is_empty() {
                     return Err(syn::Error::new(
-                        variant.fields.span(),
-                        "only enums with no fields can derive `FromStr`",
+                        data.fields.span(),
+                        "only structs with no fields can derive `FromStr`",
                     ));
                 }
-                Ok(&variant.ident)
-            })
-            .collect::<syn::Result<_>>()?;
+                vec![(&input.ident, Either::Left(data))]
+            }
+            syn::Data::Enum(data) => data
+                .variants
+                .iter()
+                .map(|variant| {
+                    if !variant.fields.is_empty() {
+                        return Err(syn::Error::new(
+                            variant.fields.span(),
+                            "only enums with no fields can derive `FromStr`",
+                        ));
+                    }
+                    Ok((&variant.ident, Either::Right(variant)))
+                })
+                .collect::<syn::Result<_>>()?,
+            syn::Data::Union(_) => {
+                return Err(syn::Error::new(
+                    input.span(),
+                    "expected an enum or a struct for flat `FromStr` derive",
+                ))
+            }
+        };
 
         Ok(Self {
             self_ty: (&input.ident, &input.generics),
-            variants,
+            matches,
         })
     }
 }
 
-impl ToTokens for EnumFlatExpansion<'_> {
+impl ToTokens for FlatExpansion<'_> {
     /// Expands a flat [`FromStr`] implementations for an enum.
     fn to_tokens(&self, tokens: &mut TokenStream) {
         let ty = self.self_ty.0;
@@ -155,24 +166,27 @@ impl ToTokens for EnumFlatExpansion<'_> {
         let ty_name = ty.to_string();
 
         let similar_lowercased = self
-            .variants
+            .matches
             .iter()
-            .map(|v| v.to_string().to_lowercase())
+            .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.variants.iter().map(|variant| {
-            let name = variant.to_string();
+        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();
 
-            quote! { #lowercased #exact_guard => Self::#variant, }
+            quote! { #lowercased #exact_guard => #constructor, }
         });
 
         quote! {
+            #[allow(unreachable_code)] // for empty enums
             #[automatically_derived]
             impl #impl_generics derive_more::core::str::FromStr for #ty #ty_generics #where_clause {
                 type Err = derive_more::FromStrError;
@@ -191,3 +205,135 @@ impl ToTokens for EnumFlatExpansion<'_> {
         }.to_tokens(tokens);
     }
 }
+
+/// Extension of [`syn::Fields`] used by this expansion.
+trait FieldsExt {
+    /// Generates a `name`d constructor with the provided `values` assigned to these
+    /// [`syn::Fields`].
+    ///
+    /// # Panics
+    ///
+    /// If number of provided `values` doesn't match number of these [`syn::Fields`].
+    fn constructor(
+        &self,
+        name: &syn::Path,
+        values: impl IntoIterator<Item = syn::Ident>,
+    ) -> TokenStream;
+
+    /// Generates a `Self` type constructor with the provided `values` assigned to these
+    /// [`syn::Fields`].
+    ///
+    /// # Panics
+    ///
+    /// If number of provided `values` doesn't match number of these [`syn::Fields`].
+    fn self_constructor(
+        &self,
+        values: impl IntoIterator<Item = syn::Ident>,
+    ) -> TokenStream {
+        self.constructor(&self.self_ty(), values)
+    }
+
+    /// Generates a `Self` type constructor with no fields.
+    ///
+    /// # Panics
+    ///
+    /// If these [`syn::Fields`] are not [empty].
+    ///
+    /// [empty]: syn::Fields::is_empty
+    fn self_constructor_empty(&self) -> TokenStream {
+        self.self_constructor(iter::empty())
+    }
+
+    /// Returns a [`syn::Path`] representing a `Self` type of these [`syn::Fields`].
+    fn self_ty(&self) -> syn::Path {
+        parse_quote! { Self }
+    }
+}
+
+impl FieldsExt for syn::Fields {
+    fn constructor(
+        &self,
+        name: &syn::Path,
+        values: impl IntoIterator<Item = syn::Ident>,
+    ) -> TokenStream {
+        let values = values.into_iter();
+        let fields = match self {
+            Self::Named(fields) => {
+                let initializers = fields.named.iter().zip(values).map(|(f, value)| {
+                    let ident = &f.ident;
+                    quote! { #ident: #value }
+                });
+                Some(quote! { { #( #initializers, )*} })
+            }
+            Self::Unnamed(_) => Some(quote! { ( #( #values, )* ) }),
+            Self::Unit => None,
+        };
+        quote! { #name #fields }
+    }
+}
+
+impl FieldsExt for syn::Field {
+    fn constructor(
+        &self,
+        name: &syn::Path,
+        values: impl IntoIterator<Item = syn::Ident>,
+    ) -> TokenStream {
+        let mut values = values.into_iter();
+        let value = values.next().expect("expected a single value");
+        if values.next().is_some() {
+            panic!("expected a single value");
+        }
+
+        if let Some(ident) = &self.ident {
+            quote! { #name { #ident: #value } }
+        } else {
+            quote! { #name(#value) }
+        }
+    }
+}
+
+impl FieldsExt for syn::Variant {
+    fn constructor(
+        &self,
+        name: &syn::Path,
+        values: impl IntoIterator<Item = syn::Ident>,
+    ) -> TokenStream {
+        self.fields.constructor(name, values)
+    }
+
+    fn self_ty(&self) -> syn::Path {
+        let variant = &self.ident;
+
+        parse_quote! { Self::#variant }
+    }
+}
+
+impl FieldsExt for syn::DataStruct {
+    fn constructor(
+        &self,
+        name: &syn::Path,
+        values: impl IntoIterator<Item = syn::Ident>,
+    ) -> TokenStream {
+        self.fields.constructor(name, values)
+    }
+}
+
+impl<L: FieldsExt, R: FieldsExt> FieldsExt for Either<&L, &R> {
+    fn constructor(
+        &self,
+        name: &syn::Path,
+        values: impl IntoIterator<Item = syn::Ident>,
+    ) -> TokenStream {
+        match self {
+            Self::Left(l) => l.constructor(name, values),
+            Self::Right(r) => r.constructor(name, values),
+        }
+    }
+
+    fn self_ty(&self) -> syn::Path {
+        match self {
+            Self::Left(l) => l.self_ty(),
+            Self::Right(r) => r.self_ty(),
+        }
+    }
+}
diff --git a/impl/src/utils.rs b/impl/src/utils.rs
index d95c91a..cc5f35b 100644
--- a/impl/src/utils.rs
+++ b/impl/src/utils.rs
@@ -17,6 +17,7 @@ use syn::{
     feature = "debug",
     feature = "display",
     feature = "from",
+    feature = "from_str",
     feature = "into",
     feature = "try_from",
 ))]
@@ -1300,6 +1301,7 @@ pub fn is_type_parameter_used_in_type(
     feature = "debug",
     feature = "display",
     feature = "from",
+    feature = "from_str",
     feature = "into",
     feature = "try_from",
 ))]


Test patch:
diff --git a/tests/from_str.rs b/tests/from_str.rs
index 4f647b9..ef06877 100644
--- a/tests/from_str.rs
+++ b/tests/from_str.rs
@@ -137,6 +137,53 @@ mod structs {
             }
         }
     }
+
+    mod flat {
+        use super::*;
+
+        #[test]
+        fn unit() {
+            #[derive(Debug, Eq, FromStr, PartialEq)]
+            struct Foo;
+
+            assert_eq!("Foo".parse::<Foo>().unwrap(), Foo);
+        }
+
+        #[test]
+        fn empty_tuple() {
+            #[derive(Debug, Eq, FromStr, PartialEq)]
+            struct Bar();
+
+            assert_eq!("Bar".parse::<Bar>().unwrap(), Bar());
+        }
+
+        #[test]
+        fn empty_struct() {
+            #[derive(Debug, Eq, FromStr, PartialEq)]
+            struct Baz {}
+
+            assert_eq!("Baz".parse::<Baz>().unwrap(), Baz {});
+        }
+
+        #[test]
+        fn case_insensitive() {
+            #[derive(Debug, Eq, FromStr, PartialEq)]
+            struct Foo;
+
+            assert_eq!("Foo".parse::<Foo>().unwrap(), Foo);
+            assert_eq!("FOO".parse::<Foo>().unwrap(), Foo);
+            assert_eq!("foo".parse::<Foo>().unwrap(), Foo);
+
+            assert_eq!(
+                "baz".parse::<Foo>().unwrap_err().to_string(),
+                "Invalid `Foo` string representation",
+            );
+            assert_eq!(
+                "other".parse::<Foo>().unwrap_err().to_string(),
+                "Invalid `Foo` string representation",
+            );
+        }
+    }
 }
 
 mod enums {
@@ -151,6 +198,80 @@ mod enums {
             Err,
         }
 
+        #[test]
+        fn empty() {
+            #[derive(Debug, Eq, FromStr, PartialEq)]
+            enum Enum {}
+
+            assert_eq!(
+                "other".parse::<Enum>().unwrap_err().to_string(),
+                "Invalid `Enum` string representation",
+            );
+        }
+
+        #[test]
+        fn unit() {
+            #[derive(Debug, Eq, FromStr, PartialEq)]
+            enum Enum {
+                Foo,
+            }
+
+            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!(
+                "baz".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 empty_tuple() {
+            #[derive(Debug, Eq, FromStr, PartialEq)]
+            enum Enum {
+                Foo(),
+            }
+
+            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!(
+                "baz".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 empty_struct() {
+            #[derive(Debug, Eq, FromStr, PartialEq)]
+            enum Enum {
+                Foo {},
+            }
+
+            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!(
+                "baz".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 case_insensitive() {
             #[derive(Debug, Eq, FromStr, PartialEq)]