diff --git a/serde_derive/src/internals/ast.rs b/serde_derive/src/internals/ast.rs index 3293823a7..adb6a67af 100644 --- a/serde_derive/src/internals/ast.rs +++ b/serde_derive/src/internals/ast.rs @@ -80,8 +80,14 @@ impl<'a> Container<'a> { match &mut data { Data::Enum(variants) => { for variant in variants { + for alias_all_rule in attrs.alias_all_rules() { + variant.attrs.alias_by_rule(*alias_all_rule); + } variant.attrs.rename_by_rules(attrs.rename_all_rules()); for field in &mut variant.fields { + for alias_all_rule in attrs.alias_all_rules() { + field.attrs.alias_by_rule(*alias_all_rule); + } field.attrs.rename_by_rules( variant .attrs @@ -93,6 +99,9 @@ impl<'a> Container<'a> { } Data::Struct(_, fields) => { for field in fields { + for alias_all_rule in attrs.alias_all_rules() { + field.attrs.alias_by_rule(*alias_all_rule); + } field.attrs.rename_by_rules(attrs.rename_all_rules()); } } diff --git a/serde_derive/src/internals/attr.rs b/serde_derive/src/internals/attr.rs index 6d846ed01..6f0993823 100644 --- a/serde_derive/src/internals/attr.rs +++ b/serde_derive/src/internals/attr.rs @@ -160,6 +160,7 @@ pub struct Container { default: Default, rename_all_rules: RenameAllRules, rename_all_fields_rules: RenameAllRules, + alias_all_rules: Vec, ser_bound: Option>, de_bound: Option>, tag: TagType, @@ -245,6 +246,7 @@ impl Container { let mut rename_all_de_rule = Attr::none(cx, RENAME_ALL); let mut rename_all_fields_ser_rule = Attr::none(cx, RENAME_ALL_FIELDS); let mut rename_all_fields_de_rule = Attr::none(cx, RENAME_ALL_FIELDS); + let mut alias_all_rules = VecAttr::none(cx, ALIAS_ALL); let mut ser_bound = Attr::none(cx, BOUND); let mut de_bound = Attr::none(cx, BOUND); let mut untagged = BoolAttr::none(cx, UNTAGGED); @@ -491,6 +493,14 @@ impl Container { if let Some(s) = get_lit_str(cx, EXPECTING, &meta)? { expecting.set(&meta.path, s.value()); } + } else if meta.path == ALIAS_ALL { + // #[serde(alias_all = "...")] + if let Some(s) = get_lit_str(cx, ALIAS_ALL, &meta)? { + match RenameRule::from_str(&s.value()) { + Ok(rename_rule) => alias_all_rules.insert(&meta.path, rename_rule), + Err(err) => cx.error_spanned_by(s, err), + } + } } else { let path = meta.path.to_token_stream().to_string().replace(' ', ""); return Err( @@ -530,6 +540,7 @@ impl Container { serialize: rename_all_fields_ser_rule.get().unwrap_or(RenameRule::None), deserialize: rename_all_fields_de_rule.get().unwrap_or(RenameRule::None), }, + alias_all_rules: alias_all_rules.get(), ser_bound: ser_bound.get(), de_bound: de_bound.get(), tag: decide_tag(cx, item, untagged, internal_tag, content), @@ -569,6 +580,10 @@ impl Container { &self.default } + pub fn alias_all_rules(&self) -> &[RenameRule] { + &self.alias_all_rules + } + pub fn ser_bound(&self) -> Option<&[syn::WherePredicate]> { self.ser_bound.as_ref().map(|vec| &vec[..]) } @@ -943,6 +958,15 @@ impl Variant { .insert(self.name.deserialize.clone()); } + pub fn alias_by_rule(&mut self, rule: RenameRule) { + let alias_name = rule.apply_to_variant(&self.name.deserialize.value); + let alias = Name { + value: alias_name, + span: self.name.deserialize.span, + }; + self.name.deserialize_aliases.insert(alias); + } + pub fn rename_all_rules(&self) -> RenameAllRules { self.rename_all_rules } @@ -1341,6 +1365,16 @@ impl Field { pub fn mark_transparent(&mut self) { self.transparent = true; } + + pub fn alias_by_rule(&mut self, rule: RenameRule) { + // Apply the rename rule to the original field name to create an alias + let alias_name = rule.apply_to_field(&self.name.deserialize.value); + let alias = Name { + value: alias_name, + span: self.name.deserialize.span, + }; + self.name.deserialize_aliases.insert(alias); + } } type SerAndDe = (Option, Option); diff --git a/serde_derive/src/internals/symbol.rs b/serde_derive/src/internals/symbol.rs index 59ef8de7c..7a7314ff0 100644 --- a/serde_derive/src/internals/symbol.rs +++ b/serde_derive/src/internals/symbol.rs @@ -5,6 +5,7 @@ use syn::{Ident, Path}; pub struct Symbol(&'static str); pub const ALIAS: Symbol = Symbol("alias"); +pub const ALIAS_ALL: Symbol = Symbol("alias_all"); pub const BORROW: Symbol = Symbol("borrow"); pub const BOUND: Symbol = Symbol("bound"); pub const CONTENT: Symbol = Symbol("content"); diff --git a/test_suite/tests/test_macros.rs b/test_suite/tests/test_macros.rs index 6b2fc4c5d..2769471da 100644 --- a/test_suite/tests/test_macros.rs +++ b/test_suite/tests/test_macros.rs @@ -864,3 +864,300 @@ fn test_packed_struct_can_derive_serialize() { t: f32, } } + +#[test] +fn test_alias_all_kebab_case() { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(alias_all = "kebab-case")] + struct TestStruct { + field_one: i32, + field_two: String, + very_long_field_name: bool, + } + + // Test deserializing with original snake_case names + assert_de_tokens( + &TestStruct { + field_one: 42, + field_two: "hello".to_string(), + very_long_field_name: true, + }, + &[ + Token::Struct { + name: "TestStruct", + len: 3, + }, + Token::Str("field_one"), + Token::I32(42), + Token::Str("field_two"), + Token::Str("hello"), + Token::Str("very_long_field_name"), + Token::Bool(true), + Token::StructEnd, + ], + ); + + // Test deserializing with kebab-case aliases + assert_de_tokens( + &TestStruct { + field_one: 42, + field_two: "hello".to_string(), + very_long_field_name: true, + }, + &[ + Token::Struct { + name: "TestStruct", + len: 3, + }, + Token::Str("field-one"), + Token::I32(42), + Token::Str("field-two"), + Token::Str("hello"), + Token::Str("very-long-field-name"), + Token::Bool(true), + Token::StructEnd, + ], + ); +} + +#[test] +fn test_alias_all_uppercase() { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(alias_all = "UPPERCASE")] + struct TestStructUppercase { + field_one: i32, + field_two: String, + } + + // Test deserializing with original snake_case names + assert_de_tokens( + &TestStructUppercase { + field_one: 42, + field_two: "hello".to_string(), + }, + &[ + Token::Struct { + name: "TestStructUppercase", + len: 2, + }, + Token::Str("field_one"), + Token::I32(42), + Token::Str("field_two"), + Token::Str("hello"), + Token::StructEnd, + ], + ); + + // Test deserializing with UPPERCASE aliases + assert_de_tokens( + &TestStructUppercase { + field_one: 42, + field_two: "hello".to_string(), + }, + &[ + Token::Struct { + name: "TestStructUppercase", + len: 2, + }, + Token::Str("FIELD_ONE"), + Token::I32(42), + Token::Str("FIELD_TWO"), + Token::Str("hello"), + Token::StructEnd, + ], + ); +} + +#[test] +fn test_alias_all_camel_case() { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(alias_all = "camelCase")] + struct TestStructCamelCase { + field_one: i32, + field_two: String, + } + + // Test deserializing with original snake_case names + assert_de_tokens( + &TestStructCamelCase { + field_one: 42, + field_two: "hello".to_string(), + }, + &[ + Token::Struct { + name: "TestStructCamelCase", + len: 2, + }, + Token::Str("field_one"), + Token::I32(42), + Token::Str("field_two"), + Token::Str("hello"), + Token::StructEnd, + ], + ); + + // Test deserializing with camelCase aliases + assert_de_tokens( + &TestStructCamelCase { + field_one: 42, + field_two: "hello".to_string(), + }, + &[ + Token::Struct { + name: "TestStructCamelCase", + len: 2, + }, + Token::Str("fieldOne"), + Token::I32(42), + Token::Str("fieldTwo"), + Token::Str("hello"), + Token::StructEnd, + ], + ); +} + +#[test] +fn test_alias_all_enum() { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(alias_all = "kebab-case")] + enum TestEnum { + VariantOne { field_one: i32 }, + VariantTwo { field_two: String }, + } + + // Test deserializing with original PascalCase names + assert_de_tokens( + &TestEnum::VariantOne { field_one: 42 }, + &[ + Token::Enum { name: "TestEnum" }, + Token::Str("VariantOne"), + Token::Struct { + name: "VariantOne", + len: 1, + }, + Token::Str("field_one"), + Token::I32(42), + Token::StructEnd, + ], + ); + + // Test deserializing with kebab-case aliases + assert_de_tokens( + &TestEnum::VariantOne { field_one: 42 }, + &[ + Token::Enum { name: "TestEnum" }, + Token::Str("variant-one"), + Token::Struct { + name: "VariantOne", + len: 1, + }, + Token::Str("field-one"), + Token::I32(42), + Token::StructEnd, + ], + ); + + // Test deserializing with original PascalCase names + assert_de_tokens( + &TestEnum::VariantTwo { + field_two: "hello".to_string(), + }, + &[ + Token::Enum { name: "TestEnum" }, + Token::Str("VariantTwo"), + Token::Struct { + name: "VariantTwo", + len: 1, + }, + Token::Str("field_two"), + Token::Str("hello"), + Token::StructEnd, + ], + ); + + // Test deserializing with kebab-case aliases + assert_de_tokens( + &TestEnum::VariantTwo { + field_two: "hello".to_string(), + }, + &[ + Token::Enum { name: "TestEnum" }, + Token::Str("variant-two"), + Token::Struct { + name: "VariantTwo", + len: 1, + }, + Token::Str("field-two"), + Token::Str("hello"), + Token::StructEnd, + ], + ); +} + +#[test] +fn test_multiple_alias_all() { + #[derive(Debug, PartialEq, Deserialize)] + #[serde(alias_all = "kebab-case")] + #[serde(alias_all = "UPPERCASE")] + struct TestStruct { + field_one: i32, + field_two: String, + } + + // Test deserializing with original snake_case names + assert_de_tokens( + &TestStruct { + field_one: 42, + field_two: "hello".to_string(), + }, + &[ + Token::Struct { + name: "TestStruct", + len: 2, + }, + Token::Str("field_one"), + Token::I32(42), + Token::Str("field_two"), + Token::Str("hello"), + Token::StructEnd, + ], + ); + + // Test deserializing with kebab-case aliases + assert_de_tokens( + &TestStruct { + field_one: 42, + field_two: "hello".to_string(), + }, + &[ + Token::Struct { + name: "TestStruct", + len: 2, + }, + Token::Str("field-one"), + Token::I32(42), + Token::Str("field-two"), + Token::Str("hello"), + Token::StructEnd, + ], + ); + + // Test deserializing with UPPERCASE aliases + assert_de_tokens( + &TestStruct { + field_one: 42, + field_two: "hello".to_string(), + }, + &[ + Token::Struct { + name: "TestStruct", + len: 2, + }, + Token::Str("FIELD_ONE"), + Token::I32(42), + Token::Str("FIELD_TWO"), + Token::Str("hello"), + Token::StructEnd, + ], + ); +}