Skip to content

Commit e648219

Browse files
authored
Support #[from_str(rename_all = "...")] attribute (#467, #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).
1 parent 2e9b180 commit e648219

22 files changed

+660
-103
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1212
- Support `#[display(rename_all = "<casing>")]` attribute to change output for
1313
implicit naming of unit enum variants or unit structs when deriving `Display`.
1414
([#443](https://github.com/JelteF/derive_more/pull/443))
15+
- Support `#[from_str(rename_all = "<casing>")]` attribute for unit enum variants
16+
and unit structs when deriving `FromStr`.
17+
([#467](https://github.com/JelteF/derive_more/pull/467))
1518
- Support `Option` fields for `Error::source()` in `Error` derive.
1619
([#459](https://github.com/JelteF/derive_more/pull/459))
1720
- Support structs with no fields in `FromStr` derive.

impl/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ deref_mut = []
5959
display = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"]
6060
error = ["syn/extra-traits"]
6161
from = ["syn/extra-traits"]
62-
from_str = []
62+
from_str = ["dep:convert_case"]
6363
index = []
6464
index_mut = []
6565
into = ["syn/extra-traits"]

impl/doc/from_str.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,61 @@ impl derive_more::core::str::FromStr for Foo {
170170
}
171171
}
172172
```
173+
174+
175+
### The `rename_all` attribute
176+
177+
To control the concrete string representation of the name verbatim,
178+
the `#[from_str(rename_all = "...")]` attribute can be placed on structs,
179+
enums and variants.
180+
181+
The available casings are:
182+
- `lowercase`
183+
- `UPPERCASE`
184+
- `PascalCase`
185+
- `camelCase`
186+
- `snake_case`
187+
- `SCREAMING_SNAKE_CASE`
188+
- `kebab-case`
189+
- `SCREAMING-KEBAB-CASE`
190+
191+
```rust
192+
# use derive_more::FromStr;
193+
#
194+
#[derive(FromStr, Debug, Eq, PartialEq)]
195+
#[from_str(rename_all = "lowercase")]
196+
enum Enum {
197+
VariantOne,
198+
#[from_str(rename_all = "kebab-case")] // overrides the top-level one
199+
VariantTwo
200+
}
201+
202+
assert_eq!("variantone".parse::<Enum>().unwrap(), Enum::VariantOne);
203+
assert_eq!("variant-two".parse::<Enum>().unwrap(), Enum::VariantTwo);
204+
```
205+
206+
> **NOTE**: Using `#[from_str(rename_all = "...")]` attribute disables
207+
> any case-insensitivity where applied. This is also true for any enum
208+
> variant whose name or string representation is similar to the variant
209+
> being marked:
210+
> ```rust
211+
> # use derive_more::FromStr;
212+
> #
213+
> # #[allow(non_camel_case_types)]
214+
> #[derive(FromStr, Debug, Eq, PartialEq)]
215+
> enum Enum {
216+
> Foo, // case-insensitive
217+
> #[from_str(rename_all = "SCREAMING_SNAKE_CASE")]
218+
> BaR, // case-sensitive (marked with attribute)
219+
> Bar, // case-sensitive (name is similar to the marked `BaR` variant)
220+
> Ba_R, // case-sensitive (string representation is similar to the marked `BaR` variant)
221+
> }
222+
> #
223+
> # assert_eq!("Foo".parse::<Enum>().unwrap(), Enum::Foo);
224+
> # assert_eq!("FOO".parse::<Enum>().unwrap(), Enum::Foo);
225+
> # assert_eq!("foo".parse::<Enum>().unwrap(), Enum::Foo);
226+
> #
227+
> # assert_eq!("BA_R".parse::<Enum>().unwrap(), Enum::BaR);
228+
> # assert_eq!("Bar".parse::<Enum>().unwrap(), Enum::Bar);
229+
> # assert_eq!("Ba_R".parse::<Enum>().unwrap(), Enum::Ba_R);
230+
> ```

impl/src/fmt/display.rs

Lines changed: 2 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#[cfg(doc)]
44
use std::fmt;
55

6-
use convert_case::Casing;
76
use proc_macro2::TokenStream;
87
use quote::{format_ident, quote};
98
use syn::{
@@ -95,8 +94,8 @@ pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result<TokenSt
9594
/// while multiple `#[<attribute>(bound(...))]` are allowed.
9695
#[derive(Debug, Default)]
9796
struct ContainerAttributes {
98-
/// [`RenameAllAttribute`] for case convertion.
99-
rename_all: Option<RenameAllAttribute>,
97+
/// [`attr::RenameAll`] for case convertion.
98+
rename_all: Option<attr::RenameAll>,
10099

101100
/// Common [`ContainerAttributes`].
102101
///
@@ -180,80 +179,6 @@ impl attr::ParseMultiple for ContainerAttributes {
180179
}
181180
}
182181

183-
/// Representation of a `rename_all` macro attribute.
184-
///
185-
/// ```rust,ignore
186-
/// #[<attribute>(rename_all = "...")]
187-
/// ```
188-
///
189-
/// Possible cases:
190-
/// - `lowercase`
191-
/// - `UPPERCASE`
192-
/// - `PascalCase`
193-
/// - `camelCase`
194-
/// - `snake_case`
195-
/// - `SCREAMING_SNAKE_CASE`
196-
/// - `kebab-case`
197-
/// - `SCREAMING-KEBAB-CASE`
198-
#[derive(Debug, Clone, Copy)]
199-
enum RenameAllAttribute {
200-
Lower,
201-
Upper,
202-
Pascal,
203-
Camel,
204-
Snake,
205-
ScreamingSnake,
206-
Kebab,
207-
ScreamingKebab,
208-
}
209-
210-
impl RenameAllAttribute {
211-
/// Converts the provided `name` into the case of this [`RenameAllAttribute`].
212-
fn convert_case(&self, name: &str) -> String {
213-
let case = match self {
214-
Self::Lower => convert_case::Case::Flat,
215-
Self::Upper => convert_case::Case::UpperFlat,
216-
Self::Pascal => convert_case::Case::Pascal,
217-
Self::Camel => convert_case::Case::Camel,
218-
Self::Snake => convert_case::Case::Snake,
219-
Self::ScreamingSnake => convert_case::Case::UpperSnake,
220-
Self::Kebab => convert_case::Case::Kebab,
221-
Self::ScreamingKebab => convert_case::Case::UpperKebab,
222-
};
223-
name.to_case(case)
224-
}
225-
}
226-
227-
impl Parse for RenameAllAttribute {
228-
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
229-
let _ = input.parse::<syn::Path>().and_then(|p| {
230-
if p.is_ident("rename_all") {
231-
Ok(p)
232-
} else {
233-
Err(syn::Error::new(
234-
p.span(),
235-
"unknown attribute argument, expected `rename_all = \"...\"`",
236-
))
237-
}
238-
})?;
239-
240-
input.parse::<token::Eq>()?;
241-
242-
let value: LitStr = input.parse()?;
243-
Ok(match value.value().replace(['-', '_'], "").to_lowercase().as_str() {
244-
"lowercase" => Self::Lower,
245-
"uppercase" => Self::Upper,
246-
"pascalcase" => Self::Pascal,
247-
"camelcase" => Self::Camel,
248-
"snakecase" => Self::Snake,
249-
"screamingsnakecase" => Self::ScreamingSnake,
250-
"kebabcase" => Self::Kebab,
251-
"screamingkebabcase" => Self::ScreamingKebab,
252-
_ => 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\""))
253-
})
254-
}
255-
}
256-
257182
/// Type alias for an expansion context:
258183
/// - [`ContainerAttributes`].
259184
/// - Type parameters. Slice of [`syn::Ident`].

0 commit comments

Comments
 (0)