Skip to content

Support #[from_str(rename_all = "...")] attribute (#216) #467

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

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8d20948
New forward struct impl
tyranron May 6, 2025
c8f70ce
New flat enum impl
tyranron May 6, 2025
dd6cb42
Remove old implementation
tyranron May 6, 2025
026554c
Fix tests
tyranron May 6, 2025
f32886b
Fix generic case for forward impl
tyranron May 6, 2025
b2efb01
Move `RenameAllAttribute` to `utils::attr`
tyranron May 6, 2025
26ae42e
Remove `dead_code` remnants from old `utils` machinery
tyranron May 6, 2025
5f63f12
Fix dat doc
tyranron May 6, 2025
370b5dd
Impl `#[from_str(rename_all = "...")]` attribute
tyranron May 6, 2025
42f986f
Add test for new attribute
tyranron May 6, 2025
54c9acb
Add `compile_fail` tests for `FromStr`
tyranron May 6, 2025
e3bf23d
Extend tests
tyranron May 6, 2025
f5b3528
Support variant level renaming
tyranron May 6, 2025
65a5c4f
Strip redundant changes
tyranron May 7, 2025
ca3a44a
Polish impl
tyranron May 7, 2025
1b1e144
Add `compile_fail` tests for `FromStr`
tyranron May 6, 2025
1dc2827
Upd tests
tyranron May 7, 2025
8b97bef
Fix fmt
tyranron May 7, 2025
6d28478
Add forwarding tests for generics
tyranron May 7, 2025
9be47ee
Bootstrap impl
tyranron May 7, 2025
88088b2
Add tests
tyranron May 7, 2025
ae3cf79
Fix implementation
tyranron May 7, 2025
2756e35
Extend test for enums
tyranron May 7, 2025
e19f010
Fix fmt and docs
tyranron May 7, 2025
7bd0d98
Mention in CHANGELOG
tyranron May 7, 2025
32e81af
Upd existing docs
tyranron May 7, 2025
2ecc396
Fix
tyranron May 7, 2025
b5274ef
Fix
tyranron May 7, 2025
3d67aea
Add empty structs to docs
tyranron May 7, 2025
0210098
Merge branch 'master' into fromstr-flat-struct
tyranron May 9, 2025
e1af4c2
Merge branch 'fromstr-flat-struct' into rework-fromstr-impl
tyranron May 9, 2025
0b3c0d6
Merge branch 'master' into rework-fromstr-impl
tyranron May 14, 2025
256a86c
Bikeshed tests
tyranron May 14, 2025
c2bb4e0
Add tests for structs
tyranron May 14, 2025
efeadf6
Polish impl
tyranron May 14, 2025
a8a99d9
Tune up `compile_fail` tests
tyranron May 14, 2025
fa8c1c6
Mention in CHANGELOG
tyranron May 14, 2025
5ec8b2e
Add docs
tyranron May 14, 2025
cc0fd06
Add compilation error on string representation collision
tyranron May 14, 2025
b52fba2
Merge branch 'master' into rework-fromstr-impl
tyranron May 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
58 changes: 58 additions & 0 deletions impl/doc/from_str.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
> ```
79 changes: 2 additions & 77 deletions impl/src/fmt/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#[cfg(doc)]
use std::fmt;

use convert_case::Casing;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
Expand Down Expand Up @@ -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`].
///
Expand Down Expand Up @@ -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`].
Expand Down
Loading