diff --git a/impl/Cargo.toml b/impl/Cargo.toml index b836827b..330dd8a8 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -56,7 +56,7 @@ constructor = [] debug = ["syn/extra-traits", "dep:unicode-xid"] deref = [] deref_mut = [] -display = ["syn/extra-traits", "dep:unicode-xid"] +display = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"] error = ["syn/extra-traits"] from = ["syn/extra-traits"] from_str = [] diff --git a/impl/src/fmt/debug.rs b/impl/src/fmt/debug.rs index e2c8bc83..95cd0d55 100644 --- a/impl/src/fmt/debug.rs +++ b/impl/src/fmt/debug.rs @@ -2,6 +2,8 @@ //! //! [`fmt::Debug`]: std::fmt::Debug +use std::convert::Infallible; + use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; @@ -77,7 +79,7 @@ pub fn expand(input: &syn::DeriveInput, _: &str) -> syn::Result { /// /// [`fmt::Debug`]: std::fmt::Debug fn expand_struct( - attrs: ContainerAttributes, + attrs: ContainerAttributes, ident: &syn::Ident, s: &syn::DataStruct, type_params: &[&syn::Ident], @@ -115,7 +117,7 @@ fn expand_struct( /// /// [`fmt::Debug`]: std::fmt::Debug fn expand_enum( - mut attrs: ContainerAttributes, + mut attrs: ContainerAttributes, e: &syn::DataEnum, type_params: &[&syn::Ident], attr_name: &syn::Ident, @@ -207,7 +209,7 @@ type FieldAttribute = Either; /// [`Debug::fmt()`]: std::fmt::Debug::fmt() #[derive(Debug)] struct Expansion<'a> { - attr: &'a ContainerAttributes, + attr: &'a ContainerAttributes, /// Struct or enum [`Ident`](struct@syn::Ident). ident: &'a syn::Ident, diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs index 758d8586..d6d023fd 100644 --- a/impl/src/fmt/display.rs +++ b/impl/src/fmt/display.rs @@ -11,7 +11,7 @@ use crate::utils::{attr::ParseMultiple as _, Spanning}; use super::{ trait_name_to_attribute_name, ContainerAttributes, ContainsGenericsExt as _, - FmtAttribute, + FmtAttribute, RenameAllAttribute, }; /// Expands a [`fmt::Display`]-like derive macro. @@ -29,9 +29,13 @@ pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result syn::Result = ( - &'a ContainerAttributes, + &'a ContainerAttributes, &'a [&'a syn::Ident], &'a syn::Ident, &'a syn::Ident, @@ -97,6 +101,7 @@ fn expand_struct( ) -> syn::Result<(Vec, TokenStream)> { let s = Expansion { shared_attr: None, + rename_all: None, attrs, fields: &s.fields, type_params, @@ -148,14 +153,18 @@ fn expand_enum( let (bounds, match_arms) = e.variants.iter().try_fold( (Vec::new(), TokenStream::new()), |(mut bounds, mut arms), variant| { - let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)? - .map(Spanning::into_inner) - .unwrap_or_default(); + let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)?; + if let Some(attrs) = &attrs { + attrs.validate_for_struct(attr_name)?; + }; + let attrs = attrs.map(Spanning::into_inner).unwrap_or_default(); + let ident = &variant.ident; if attrs.fmt.is_none() && variant.fields.is_empty() && attr_name != "display" + && container_attrs.rename_all.is_none() { return Err(syn::Error::new( e.variants.span(), @@ -168,6 +177,7 @@ fn expand_enum( let v = Expansion { shared_attr: container_attrs.fmt.as_ref(), + rename_all: container_attrs.rename_all, attrs: &attrs, fields: &variant.fields, type_params, @@ -234,8 +244,13 @@ struct Expansion<'a> { /// [`None`] for a struct. shared_attr: Option<&'a FmtAttribute>, + /// [`RenameAllAttribute`] placed on enum. + /// + /// [`None`] for a struct. + rename_all: Option, + /// Derive macro [`ContainerAttributes`]. - attrs: &'a ContainerAttributes, + attrs: &'a ContainerAttributes, /// Struct or enum [`syn::Ident`]. /// @@ -305,7 +320,11 @@ impl Expansion<'_> { None => { if shared_attr_is_wrapping || !has_shared_attr { body = if self.fields.is_empty() { - let ident_str = self.ident.unraw().to_string(); + let ident_str = if let Some(rename_all) = &self.rename_all { + rename_all.convert_case(self.ident) + } else { + self.ident.unraw().to_string() + }; if shared_attr_is_wrapping { quote! { #ident_str } diff --git a/impl/src/fmt/mod.rs b/impl/src/fmt/mod.rs index 63041a06..596e2026 100644 --- a/impl/src/fmt/mod.rs +++ b/impl/src/fmt/mod.rs @@ -8,6 +8,8 @@ pub(crate) mod debug; pub(crate) mod display; mod parsing; +#[cfg(feature = "display")] +use convert_case::{Case, Casing as _}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use syn::{ @@ -88,6 +90,65 @@ impl BoundsAttribute { } } +/// Representation of a `rename_all` macro attribute. +/// +/// ```rust,ignore +/// #[(rename_all = "...")] +/// ``` +/// +/// Possible Cases: +/// - `lowercase` +/// - `UPPERCASE` +/// - `PascalCase` +/// - `camelCase` +/// - `snake_case` +/// - `SCREAMING_SNAKE_CASE` +/// - `kebab-case` +/// - `SCREAMING-KEBAB-CASE` +#[cfg(feature = "display")] +#[derive(Debug, Clone, Copy)] +struct RenameAllAttribute(#[allow(unused)] Case); + +#[cfg(feature = "display")] +impl RenameAllAttribute { + fn convert_case(&self, ident: &syn::Ident) -> String { + ident.unraw().to_string().to_case(self.0) + } +} + +#[cfg(feature = "display")] +impl Parse for RenameAllAttribute { + fn parse(input: ParseStream<'_>) -> syn::Result { + let _ = input.parse::().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::()?; + + let value: syn::LitStr = input.parse()?; + + // TODO should we really do a case insensitive comparision here? + Ok(Self(match value.value().replace(['-', '_'], "").to_lowercase().as_str() { + "lowercase" => Case::Flat, + "uppercase" => Case::UpperFlat, + "pascalcase" => Case::Pascal, + "camelcase" => Case::Camel, + "snakecase" => Case::Snake, + "screamingsnakecase" => Case::UpperSnake, + "kebabcase" => Case::Kebab, + "screamingkebabcase" => Case::UpperKebab, + _ => 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\"")) + })) + } +} + /// Representation of a [`fmt`]-like attribute. /// /// ```rust,ignore @@ -509,16 +570,47 @@ impl Placeholder { /// are allowed. /// /// [`fmt::Display`]: std::fmt::Display -#[derive(Debug, Default)] -struct ContainerAttributes { +#[derive(Debug)] +struct ContainerAttributes { /// Interpolation [`FmtAttribute`]. fmt: Option, /// Addition trait bounds. bounds: BoundsAttribute, + + /// Rename unit enum variants following a similar behavior as [`serde`](https://serde.rs/container-attrs.html#rename_all). + rename_all: Option, +} + +impl std::default::Default for ContainerAttributes { + fn default() -> Self { + Self { + fmt: None, + bounds: BoundsAttribute::default(), + rename_all: None, + } + } +} + +#[cfg(feature = "display")] +impl Spanning> { + fn validate_for_struct( + &self, + attr_name: impl std::fmt::Display, + ) -> syn::Result<()> { + if self.rename_all.is_some() { + Err(syn::Error::new( + self.span, + format_args!("`#[{attr_name}(rename_all=\"...\")]` can not be specified on structs or variants"), + )) + } else { + Ok(()) + } + } } -impl Parse for ContainerAttributes { +#[cfg(feature = "debug")] +impl Parse for ContainerAttributes { fn parse(input: ParseStream<'_>) -> syn::Result { // We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow // any error of the `Either::Left` if the `Either::Right` succeeds. @@ -527,13 +619,83 @@ impl Parse for ContainerAttributes { Either::Left(fmt) => Self { bounds: BoundsAttribute::default(), fmt: Some(fmt), + rename_all: None, }, - Either::Right(bounds) => Self { bounds, fmt: None }, + Either::Right(bounds) => Self { + bounds, + fmt: None, + rename_all: None, + }, + }) + } +} + +#[cfg(feature = "display")] +impl Parse for ContainerAttributes { + fn parse(input: ParseStream<'_>) -> syn::Result { + mod kw { + use syn::custom_keyword; + + custom_keyword!(rename_all); + custom_keyword!(bounds); + custom_keyword!(bound); + } + + // We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow + // any error of the `Either::Left` if the `Either::Right` succeeds. + FmtAttribute::check_legacy_fmt(input)?; + let lookahead = input.lookahead1(); + Ok(if lookahead.peek(syn::LitStr) { + Self { + fmt: Some(input.parse()?), + bounds: BoundsAttribute::default(), + rename_all: None, + } + } else if lookahead.peek(kw::rename_all) + || lookahead.peek(kw::bounds) + || lookahead.peek(kw::bound) + || lookahead.peek(syn::Token![where]) + { + let mut bounds = BoundsAttribute::default(); + let mut rename_all = None; + + while !input.is_empty() { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::rename_all) { + if rename_all.is_some() { + return Err( + input.error("`rename_all` can only be specified once") + ); + } else { + rename_all = Some(input.parse()?); + } + } else if lookahead.peek(kw::bounds) + || lookahead.peek(kw::bound) + || lookahead.peek(syn::Token![where]) + { + bounds.0.extend(input.parse::()?.0) + } else { + return Err(lookahead.error()); + } + if !input.is_empty() { + input.parse::()?; + } + } + Self { + fmt: None, + bounds, + rename_all, + } + } else { + return Err(lookahead.error()); }) } } -impl attr::ParseMultiple for ContainerAttributes { +impl attr::ParseMultiple for ContainerAttributes +where + ContainerAttributes: Parse, +{ fn merge_attrs( prev: Spanning, new: Spanning, @@ -554,6 +716,16 @@ impl attr::ParseMultiple for ContainerAttributes { format!("multiple `#[{name}(\"...\", ...)]` attributes aren't allowed"), )); } + if new + .rename_all + .and_then(|n| prev.rename_all.replace(n)) + .is_some() + { + return Err(syn::Error::new( + new_span, + format!("multiple `#[{name}(rename_all=\"...\")]` attributes aren't allowed"), + )); + } prev.bounds.0.extend(new.bounds.0); Ok(Spanning::new( @@ -582,7 +754,7 @@ where } } -/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to travers its type parameters. +/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to traverse its type parameters. trait ContainsGenericsExt { /// Checks whether this definition contains any of the provided `type_params`. fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool; diff --git a/tests/compile_fail/display/invalid_casing.rs b/tests/compile_fail/display/invalid_casing.rs new file mode 100644 index 00000000..461cc50d --- /dev/null +++ b/tests/compile_fail/display/invalid_casing.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Display)] +#[display(rename_all = "Whatever")] +enum Enum { + UnitVariant, +} + +fn main() {} diff --git a/tests/compile_fail/display/invalid_casing.stderr b/tests/compile_fail/display/invalid_casing.stderr new file mode 100644 index 00000000..60f687a6 --- /dev/null +++ b/tests/compile_fail/display/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/display/invalid_casing.rs:2:24 + | +2 | #[display(rename_all = "Whatever")] + | ^^^^^^^^^^ diff --git a/tests/compile_fail/display/rename_all_on_struct.rs b/tests/compile_fail/display/rename_all_on_struct.rs new file mode 100644 index 00000000..9e2d9d1c --- /dev/null +++ b/tests/compile_fail/display/rename_all_on_struct.rs @@ -0,0 +1,11 @@ +#[derive(derive_more::Display)] +enum Enum { + #[display(rename_all = "lowercase")] + RenameAllOnVariant, +} + +#[derive(derive_more::Display)] +#[display(rename_all = "lowercase")] +struct Struct; + +fn main() {} diff --git a/tests/compile_fail/display/rename_all_on_struct.stderr b/tests/compile_fail/display/rename_all_on_struct.stderr new file mode 100644 index 00000000..d80364e9 --- /dev/null +++ b/tests/compile_fail/display/rename_all_on_struct.stderr @@ -0,0 +1,11 @@ +error: `#[display(rename_all="...")]` can not be specified on structs or variants + --> tests/compile_fail/display/rename_all_on_struct.rs:3:5 + | +3 | #[display(rename_all = "lowercase")] + | ^ + +error: `#[display(rename_all="...")]` can not be specified on structs or variants + --> tests/compile_fail/display/rename_all_on_struct.rs:8:1 + | +8 | #[display(rename_all = "lowercase")] + | ^ diff --git a/tests/compile_fail/display/unknown_attribute.stderr b/tests/compile_fail/display/unknown_attribute.stderr index be551fc0..d18706cb 100644 --- a/tests/compile_fail/display/unknown_attribute.stderr +++ b/tests/compile_fail/display/unknown_attribute.stderr @@ -1,4 +1,4 @@ -error: unknown attribute argument, expected `bound(...)` +error: expected one of: string literal, `rename_all`, `bounds`, `bound`, `where` --> tests/compile_fail/display/unknown_attribute.rs:3:11 | 3 | #[display(unknown = "unknown")] diff --git a/tests/display.rs b/tests/display.rs index 478e1d94..00c38b84 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -1797,6 +1797,63 @@ mod enums { } } } + + mod rename_all { + use super::*; + + macro_rules! casing_test { + ($name:ident, $casing:literal, $VariantOne:literal, $Two:literal) => { + #[test] + fn $name() { + #[derive( + Binary, Display, LowerExp, LowerHex, Octal, Pointer, UpperExp, + UpperHex + )] + #[binary(rename_all = $casing)] + #[display(rename_all = $casing)] + #[lower_exp(rename_all = $casing)] + #[lower_hex(rename_all = $casing)] + #[octal(rename_all = $casing)] + #[pointer(rename_all = $casing)] + #[upper_exp(rename_all = $casing)] + #[upper_hex(rename_all = $casing)] + enum Enum { + VariantOne, + Two, + } + + assert_eq!(Enum::VariantOne.to_string(), $VariantOne); + assert_eq!(Enum::Two.to_string(), $Two); + assert_eq!(format!("{:b}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:e}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:x}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:o}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:p}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:E}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:X}", Enum::VariantOne), $VariantOne); + } + }; + } + + 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" + ); + } } mod generic {