Skip to content

Commit 3b4683f

Browse files
committed
impl rename_all for #[derive(Display)] (JelteF#216)
1 parent c5e5e82 commit 3b4683f

File tree

10 files changed

+260
-19
lines changed

10 files changed

+260
-19
lines changed

impl/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ add = []
5353
add_assign = []
5454
as_ref = ["syn/extra-traits", "syn/visit"]
5555
constructor = []
56-
debug = ["syn/extra-traits", "dep:unicode-xid"]
56+
debug = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"]
5757
deref = []
5858
deref_mut = []
59-
display = ["syn/extra-traits", "dep:unicode-xid"]
59+
display = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"]
6060
error = ["syn/extra-traits"]
6161
from = ["syn/extra-traits"]
6262
from_str = []

impl/src/fmt/display.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::utils::{attr::ParseMultiple as _, Spanning};
1111

1212
use super::{
1313
trait_name_to_attribute_name, ContainerAttributes, ContainsGenericsExt as _,
14-
FmtAttribute,
14+
FmtAttribute, RenameAllAttribute,
1515
};
1616

1717
/// Expands a [`fmt::Display`]-like derive macro.
@@ -29,9 +29,13 @@ pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result<TokenSt
2929
let trait_name = normalize_trait_name(trait_name);
3030
let attr_name = format_ident!("{}", trait_name_to_attribute_name(trait_name));
3131

32-
let attrs = ContainerAttributes::parse_attrs(&input.attrs, &attr_name)?
33-
.map(Spanning::into_inner)
34-
.unwrap_or_default();
32+
let attrs = ContainerAttributes::parse_attrs(&input.attrs, &attr_name)?;
33+
if let Some(attrs) = &attrs {
34+
if matches!(input.data, syn::Data::Struct(_)) {
35+
attrs.validate_for_struct(&attr_name)?;
36+
}
37+
}
38+
let attrs = attrs.map(Spanning::into_inner).unwrap_or_default();
3539
let trait_ident = format_ident!("{trait_name}");
3640
let ident = &input.ident;
3741

@@ -97,6 +101,7 @@ fn expand_struct(
97101
) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
98102
let s = Expansion {
99103
shared_attr: None,
104+
rename_all: None,
100105
attrs,
101106
fields: &s.fields,
102107
type_params,
@@ -148,14 +153,18 @@ fn expand_enum(
148153
let (bounds, match_arms) = e.variants.iter().try_fold(
149154
(Vec::new(), TokenStream::new()),
150155
|(mut bounds, mut arms), variant| {
151-
let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)?
152-
.map(Spanning::into_inner)
153-
.unwrap_or_default();
156+
let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)?;
157+
if let Some(attrs) = &attrs {
158+
attrs.validate_for_struct(attr_name)?;
159+
};
160+
let attrs = attrs.map(Spanning::into_inner).unwrap_or_default();
161+
154162
let ident = &variant.ident;
155163

156164
if attrs.fmt.is_none()
157165
&& variant.fields.is_empty()
158166
&& attr_name != "display"
167+
&& container_attrs.rename_all.is_none()
159168
{
160169
return Err(syn::Error::new(
161170
e.variants.span(),
@@ -168,6 +177,7 @@ fn expand_enum(
168177

169178
let v = Expansion {
170179
shared_attr: container_attrs.fmt.as_ref(),
180+
rename_all: container_attrs.rename_all,
171181
attrs: &attrs,
172182
fields: &variant.fields,
173183
type_params,
@@ -234,6 +244,11 @@ struct Expansion<'a> {
234244
/// [`None`] for a struct.
235245
shared_attr: Option<&'a FmtAttribute>,
236246

247+
/// [`RenameAllAttribute`] placed on enum.
248+
///
249+
/// [`None`] for a struct.
250+
rename_all: Option<RenameAllAttribute>,
251+
237252
/// Derive macro [`ContainerAttributes`].
238253
attrs: &'a ContainerAttributes,
239254

@@ -305,7 +320,11 @@ impl Expansion<'_> {
305320
None => {
306321
if shared_attr_is_wrapping || !has_shared_attr {
307322
body = if self.fields.is_empty() {
308-
let ident_str = self.ident.unraw().to_string();
323+
let ident_str = if let Some(rename_all) = &self.rename_all {
324+
rename_all.convert_case(self.ident)
325+
} else {
326+
self.ident.unraw().to_string()
327+
};
309328

310329
if shared_attr_is_wrapping {
311330
quote! { #ident_str }

impl/src/fmt/mod.rs

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ pub(crate) mod debug;
88
pub(crate) mod display;
99
mod parsing;
1010

11+
use std::fmt::Display;
12+
13+
use convert_case::{Case, Casing as _};
1114
use proc_macro2::TokenStream;
1215
use quote::{format_ident, quote, ToTokens};
1316
use syn::{
@@ -16,7 +19,7 @@ use syn::{
1619
parse_quote,
1720
punctuated::Punctuated,
1821
spanned::Spanned as _,
19-
token,
22+
token, LitStr, Token,
2023
};
2124

2225
use crate::{
@@ -88,6 +91,62 @@ impl BoundsAttribute {
8891
}
8992
}
9093

94+
/// Representation of a `rename_all` macro attribute.
95+
///
96+
/// ```rust,ignore
97+
/// #[<attribute>(rename_all = "...")]
98+
/// ```
99+
///
100+
/// Possible Cases:
101+
/// - `lowercase`
102+
/// - `UPPERCASE`
103+
/// - `PascalCase`
104+
/// - `camelCase`
105+
/// - `snake_case`
106+
/// - `SCREAMING_SNAKE_CASE`
107+
/// - `kebab-case`
108+
/// - `SCREAMING-KEBAB-CASE`
109+
#[derive(Debug, Clone, Copy)]
110+
struct RenameAllAttribute(Case);
111+
112+
impl RenameAllAttribute {
113+
fn convert_case(&self, ident: &syn::Ident) -> String {
114+
ident.unraw().to_string().to_case(self.0)
115+
}
116+
}
117+
118+
impl Parse for RenameAllAttribute {
119+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
120+
let _ = input.parse::<syn::Path>().and_then(|p| {
121+
if p.is_ident("rename_all") {
122+
Ok(p)
123+
} else {
124+
Err(syn::Error::new(
125+
p.span(),
126+
"unknown attribute argument, expected `rename_all = \"...\"`",
127+
))
128+
}
129+
})?;
130+
131+
input.parse::<Token![=]>()?;
132+
133+
let value: LitStr = input.parse()?;
134+
135+
// TODO should we really do a case insensitive comparision here?
136+
Ok(Self(match value.value().replace(['-', '_'], "").to_lowercase().as_str() {
137+
"lowercase" => Case::Flat,
138+
"uppercase" => Case::UpperFlat,
139+
"pascalcase" => Case::Pascal,
140+
"camelcase" => Case::Camel,
141+
"snakecase" => Case::Snake,
142+
"screamingsnakecase" => Case::UpperSnake,
143+
"kebabcase" => Case::Kebab,
144+
"screamingkebabcase" => Case::UpperKebab,
145+
_ => 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\""))
146+
}))
147+
}
148+
}
149+
91150
/// Representation of a [`fmt`]-like attribute.
92151
///
93152
/// ```rust,ignore
@@ -516,19 +575,81 @@ struct ContainerAttributes {
516575

517576
/// Addition trait bounds.
518577
bounds: BoundsAttribute,
578+
579+
/// Rename unit enum variants following a similar behavior as [`serde`](https://serde.rs/container-attrs.html#rename_all).
580+
rename_all: Option<RenameAllAttribute>,
581+
}
582+
583+
impl Spanning<ContainerAttributes> {
584+
fn validate_for_struct(&self, attr_name: impl Display) -> syn::Result<()> {
585+
if self.rename_all.is_some() {
586+
Err(syn::Error::new(
587+
self.span,
588+
format_args!("`#[{attr_name}(rename_all=\"...\")]` can not be specified on structs or variants"),
589+
))
590+
} else {
591+
Ok(())
592+
}
593+
}
594+
}
595+
596+
mod kw {
597+
use syn::custom_keyword;
598+
599+
custom_keyword!(rename_all);
600+
custom_keyword!(bounds);
601+
custom_keyword!(bound);
519602
}
520603

521604
impl Parse for ContainerAttributes {
522605
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
523606
// We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow
524607
// any error of the `Either::Left` if the `Either::Right` succeeds.
525608
FmtAttribute::check_legacy_fmt(input)?;
526-
<Either<FmtAttribute, BoundsAttribute>>::parse(input).map(|v| match v {
527-
Either::Left(fmt) => Self {
609+
let lookahead = input.lookahead1();
610+
Ok(if lookahead.peek(LitStr) {
611+
Self {
612+
fmt: Some(input.parse()?),
528613
bounds: BoundsAttribute::default(),
529-
fmt: Some(fmt),
530-
},
531-
Either::Right(bounds) => Self { bounds, fmt: None },
614+
rename_all: None,
615+
}
616+
} else if lookahead.peek(kw::rename_all)
617+
|| lookahead.peek(kw::bounds)
618+
|| lookahead.peek(kw::bound)
619+
|| lookahead.peek(Token![where])
620+
{
621+
let mut bounds = BoundsAttribute::default();
622+
let mut rename_all = None;
623+
624+
while !input.is_empty() {
625+
let lookahead = input.lookahead1();
626+
if lookahead.peek(kw::rename_all) {
627+
if rename_all.is_some() {
628+
return Err(
629+
input.error("`rename_all` can only be specified once")
630+
);
631+
} else {
632+
rename_all = Some(input.parse()?);
633+
}
634+
} else if lookahead.peek(kw::bounds)
635+
|| lookahead.peek(kw::bound)
636+
|| lookahead.peek(Token![where])
637+
{
638+
bounds.0.extend(input.parse::<BoundsAttribute>()?.0)
639+
} else {
640+
return Err(lookahead.error());
641+
}
642+
if !input.is_empty() {
643+
input.parse::<Token![,]>()?;
644+
}
645+
}
646+
Self {
647+
fmt: None,
648+
bounds,
649+
rename_all,
650+
}
651+
} else {
652+
return Err(lookahead.error());
532653
})
533654
}
534655
}
@@ -554,6 +675,16 @@ impl attr::ParseMultiple for ContainerAttributes {
554675
format!("multiple `#[{name}(\"...\", ...)]` attributes aren't allowed"),
555676
));
556677
}
678+
if new
679+
.rename_all
680+
.and_then(|n| prev.rename_all.replace(n))
681+
.is_some()
682+
{
683+
return Err(syn::Error::new(
684+
new_span,
685+
format!("multiple `#[{name}(rename_all=\"...\")]` attributes aren't allowed"),
686+
));
687+
}
557688
prev.bounds.0.extend(new.bounds.0);
558689

559690
Ok(Spanning::new(
@@ -582,7 +713,7 @@ where
582713
}
583714
}
584715

585-
/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to travers its type parameters.
716+
/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to traverse its type parameters.
586717
trait ContainsGenericsExt {
587718
/// Checks whether this definition contains any of the provided `type_params`.
588719
fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool;

tests/compile_fail/debug/unknown_attribute.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: unknown attribute argument, expected `bound(...)`
1+
error: expected one of: string literal, `rename_all`, `bounds`, `bound`, `where`
22
--> tests/compile_fail/debug/unknown_attribute.rs:2:9
33
|
44
2 | #[debug(unknown = "unknown")]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#[derive(derive_more::Display)]
2+
#[display(rename_all = "Whatever")]
3+
enum Enum {
4+
UnitVariant,
5+
}
6+
7+
fn main() {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error: unexpected casing expected one of: "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", or "SCREAMING-KEBAB-CASE"
2+
--> tests/compile_fail/display/invalid_casing.rs:2:24
3+
|
4+
2 | #[display(rename_all = "Whatever")]
5+
| ^^^^^^^^^^
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#[derive(derive_more::Display)]
2+
enum Enum {
3+
#[display(rename_all = "lowercase")]
4+
RenameAllOnVariant,
5+
}
6+
7+
#[derive(derive_more::Display)]
8+
#[display(rename_all = "lowercase")]
9+
struct Struct;
10+
11+
fn main() {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
error: `#[display(rename_all="...")]` can not be specified on structs or variants
2+
--> tests/compile_fail/display/rename_all_on_struct.rs:3:5
3+
|
4+
3 | #[display(rename_all = "lowercase")]
5+
| ^
6+
7+
error: `#[display(rename_all="...")]` can not be specified on structs or variants
8+
--> tests/compile_fail/display/rename_all_on_struct.rs:8:1
9+
|
10+
8 | #[display(rename_all = "lowercase")]
11+
| ^

tests/compile_fail/display/unknown_attribute.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
error: unknown attribute argument, expected `bound(...)`
1+
error: expected one of: string literal, `rename_all`, `bounds`, `bound`, `where`
22
--> tests/compile_fail/display/unknown_attribute.rs:3:11
33
|
44
3 | #[display(unknown = "unknown")]

0 commit comments

Comments
 (0)