Skip to content

Commit 9636818

Browse files
committed
Support Unicode in field/variant renaming
1 parent d179020 commit 9636818

File tree

2 files changed

+70
-47
lines changed

2 files changed

+70
-47
lines changed

serde_derive/src/internals/case.rs

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -57,54 +57,47 @@ impl RenameRule {
5757
pub fn apply_to_variant(self, variant: &str) -> String {
5858
match self {
5959
None | PascalCase => variant.to_owned(),
60-
LowerCase => variant.to_ascii_lowercase(),
61-
UpperCase => variant.to_ascii_uppercase(),
62-
CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..],
63-
SnakeCase => {
64-
let mut snake = String::new();
65-
for (i, ch) in variant.char_indices() {
66-
if i > 0 && ch.is_uppercase() {
67-
snake.push('_');
68-
}
69-
snake.push(ch.to_ascii_lowercase());
70-
}
71-
snake
60+
LowerCase => variant.to_lowercase(),
61+
UpperCase => variant.to_uppercase(),
62+
CamelCase => {
63+
let mut chars = variant.chars();
64+
let Some(first) = chars.next() else {
65+
return String::new();
66+
};
67+
68+
let mut camel = String::with_capacity(variant.len());
69+
camel.extend(first.to_lowercase());
70+
camel.push_str(chars.as_str());
71+
camel
7272
}
73-
ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
74-
KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
75-
ScreamingKebabCase => ScreamingSnakeCase
76-
.apply_to_variant(variant)
77-
.replace('_', "-"),
73+
SnakeCase => separate_pascal_case(variant, false, '_'),
74+
ScreamingSnakeCase => separate_pascal_case(variant, true, '_'),
75+
KebabCase => separate_pascal_case(variant, false, '-'),
76+
ScreamingKebabCase => separate_pascal_case(variant, true, '-'),
7877
}
7978
}
8079

8180
/// Apply a renaming rule to a struct field, returning the version expected in the source.
8281
pub fn apply_to_field(self, field: &str) -> String {
8382
match self {
8483
None | LowerCase | SnakeCase => field.to_owned(),
85-
UpperCase => field.to_ascii_uppercase(),
86-
PascalCase => {
87-
let mut pascal = String::new();
88-
let mut capitalize = true;
89-
for ch in field.chars() {
90-
if ch == '_' {
91-
capitalize = true;
92-
} else if capitalize {
93-
pascal.push(ch.to_ascii_uppercase());
94-
capitalize = false;
95-
} else {
96-
pascal.push(ch);
84+
UpperCase => field.to_uppercase(),
85+
PascalCase => snake_case_to_camel_case(field, true),
86+
CamelCase => snake_case_to_camel_case(field, false),
87+
ScreamingSnakeCase => field.to_uppercase(),
88+
KebabCase => field.replace('_', "-"),
89+
ScreamingKebabCase => {
90+
let kebab = field.to_uppercase();
91+
92+
let mut kebab_vec = Vec::from(kebab);
93+
for b in &mut kebab_vec {
94+
if *b == b'_' {
95+
*b = b'-';
9796
}
9897
}
99-
pascal
100-
}
101-
CamelCase => {
102-
let pascal = PascalCase.apply_to_field(field);
103-
pascal[..1].to_ascii_lowercase() + &pascal[1..]
98+
// we only replaced ASCII in place, it's still valid UTF-8
99+
String::from_utf8(kebab_vec).unwrap()
104100
}
105-
ScreamingSnakeCase => field.to_ascii_uppercase(),
106-
KebabCase => field.replace('_', "-"),
107-
ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
108101
}
109102
}
110103

@@ -117,6 +110,37 @@ impl RenameRule {
117110
}
118111
}
119112

113+
fn separate_pascal_case(pascal: &str, screaming: bool, line: char) -> String {
114+
let mut separated = String::with_capacity(pascal.len());
115+
for (i, ch) in pascal.char_indices() {
116+
if i > 0 && ch.is_uppercase() {
117+
separated.push(line);
118+
}
119+
if screaming {
120+
separated.extend(ch.to_uppercase());
121+
} else {
122+
separated.extend(ch.to_lowercase());
123+
}
124+
}
125+
separated
126+
}
127+
128+
fn snake_case_to_camel_case(snake: &str, pascal: bool) -> String {
129+
let mut camel = String::with_capacity(snake.len());
130+
let mut capitalize = pascal;
131+
for ch in snake.chars() {
132+
if ch == '_' {
133+
capitalize = true;
134+
} else if capitalize {
135+
camel.extend(ch.to_uppercase());
136+
capitalize = false;
137+
} else {
138+
camel.push(ch);
139+
}
140+
}
141+
camel
142+
}
143+
120144
pub struct ParseError<'a> {
121145
unknown: &'a str,
122146
}

test_suite/tests/test_macros.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -658,8 +658,7 @@ fn test_rename_all() {
658658
enum E {
659659
#[serde(rename_all = "camelCase")]
660660
Serialize {
661-
serialize: bool,
662-
serialize_seq: bool,
661+
serialize: bool, etwas_ändern: bool
663662
},
664663
#[serde(rename_all = "kebab-case")]
665664
SerializeSeq {
@@ -676,21 +675,21 @@ fn test_rename_all() {
676675
#[derive(Serialize, Deserialize, Debug, PartialEq)]
677676
#[serde(rename_all = "PascalCase")]
678677
struct S {
679-
serialize: bool,
678+
ändern: bool,
680679
serialize_seq: bool,
681680
}
682681

683682
#[derive(Serialize, Deserialize, Debug, PartialEq)]
684683
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
685684
struct ScreamingKebab {
686-
serialize: bool,
685+
grüße: bool,
687686
serialize_seq: bool,
688687
}
689688

690689
assert_tokens(
691690
&E::Serialize {
692691
serialize: true,
693-
serialize_seq: true,
692+
etwas_ändern: true,
694693
},
695694
&[
696695
Token::StructVariant {
@@ -700,7 +699,7 @@ fn test_rename_all() {
700699
},
701700
Token::Str("serialize"),
702701
Token::Bool(true),
703-
Token::Str("serializeSeq"),
702+
Token::Str("etwasÄndern"),
704703
Token::Bool(true),
705704
Token::StructVariantEnd,
706705
],
@@ -746,12 +745,12 @@ fn test_rename_all() {
746745

747746
assert_tokens(
748747
&S {
749-
serialize: true,
748+
ändern: true,
750749
serialize_seq: true,
751750
},
752751
&[
753752
Token::Struct { name: "S", len: 2 },
754-
Token::Str("Serialize"),
753+
Token::Str("Ändern"),
755754
Token::Bool(true),
756755
Token::Str("SerializeSeq"),
757756
Token::Bool(true),
@@ -761,15 +760,15 @@ fn test_rename_all() {
761760

762761
assert_tokens(
763762
&ScreamingKebab {
764-
serialize: true,
763+
grüße: true,
765764
serialize_seq: true,
766765
},
767766
&[
768767
Token::Struct {
769768
name: "ScreamingKebab",
770769
len: 2,
771770
},
772-
Token::Str("SERIALIZE"),
771+
Token::Str("GRÜSSE"),
773772
Token::Bool(true),
774773
Token::Str("SERIALIZE-SEQ"),
775774
Token::Bool(true),

0 commit comments

Comments
 (0)