diff --git a/serde_derive/src/internals/case.rs b/serde_derive/src/internals/case.rs index 8c8c02e75..d2bcdbc5c 100644 --- a/serde_derive/src/internals/case.rs +++ b/serde_derive/src/internals/case.rs @@ -57,24 +57,23 @@ impl RenameRule { pub fn apply_to_variant(self, variant: &str) -> String { match self { None | PascalCase => variant.to_owned(), - LowerCase => variant.to_ascii_lowercase(), - UpperCase => variant.to_ascii_uppercase(), - CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..], - SnakeCase => { - let mut snake = String::new(); - for (i, ch) in variant.char_indices() { - if i > 0 && ch.is_uppercase() { - snake.push('_'); - } - snake.push(ch.to_ascii_lowercase()); - } - snake + LowerCase => variant.to_lowercase(), + UpperCase => variant.to_uppercase(), + CamelCase => { + let mut chars = variant.chars(); + let Some(first) = chars.next() else { + return String::new(); + }; + + let mut camel = String::with_capacity(variant.len()); + camel.extend(first.to_lowercase()); + camel.push_str(chars.as_str()); + camel } - ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(), - KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"), - ScreamingKebabCase => ScreamingSnakeCase - .apply_to_variant(variant) - .replace('_', "-"), + SnakeCase => separate_pascal_case(variant, false, '_'), + ScreamingSnakeCase => separate_pascal_case(variant, true, '_'), + KebabCase => separate_pascal_case(variant, false, '-'), + ScreamingKebabCase => separate_pascal_case(variant, true, '-'), } } @@ -82,29 +81,23 @@ impl RenameRule { pub fn apply_to_field(self, field: &str) -> String { match self { None | LowerCase | SnakeCase => field.to_owned(), - UpperCase => field.to_ascii_uppercase(), - PascalCase => { - let mut pascal = String::new(); - let mut capitalize = true; - for ch in field.chars() { - if ch == '_' { - capitalize = true; - } else if capitalize { - pascal.push(ch.to_ascii_uppercase()); - capitalize = false; - } else { - pascal.push(ch); + UpperCase => field.to_uppercase(), + PascalCase => snake_case_to_camel_case(field, true), + CamelCase => snake_case_to_camel_case(field, false), + ScreamingSnakeCase => field.to_uppercase(), + KebabCase => field.replace('_', "-"), + ScreamingKebabCase => { + let kebab = field.to_uppercase(); + + let mut kebab_vec = Vec::from(kebab); + for b in &mut kebab_vec { + if *b == b'_' { + *b = b'-'; } } - pascal - } - CamelCase => { - let pascal = PascalCase.apply_to_field(field); - pascal[..1].to_ascii_lowercase() + &pascal[1..] + // we only replaced ASCII in place, it's still valid UTF-8 + String::from_utf8(kebab_vec).unwrap() } - ScreamingSnakeCase => field.to_ascii_uppercase(), - KebabCase => field.replace('_', "-"), - ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"), } } @@ -117,6 +110,37 @@ impl RenameRule { } } +fn separate_pascal_case(pascal: &str, screaming: bool, line: char) -> String { + let mut separated = String::with_capacity(pascal.len()); + for (i, ch) in pascal.char_indices() { + if (i > 0 && ch.is_uppercase()) || ch == '_' { + separated.push(line); + } + if screaming { + separated.extend(ch.to_uppercase()); + } else { + separated.extend(ch.to_lowercase()); + } + } + separated +} + +fn snake_case_to_camel_case(snake: &str, pascal: bool) -> String { + let mut camel = String::with_capacity(snake.len()); + let mut capitalize = pascal; + for ch in snake.chars() { + if ch == '_' { + capitalize = true; + } else if capitalize { + camel.extend(ch.to_uppercase()); + capitalize = false; + } else { + camel.push(ch); + } + } + camel +} + pub struct ParseError<'a> { unknown: &'a str, } diff --git a/test_suite/tests/test_macros.rs b/test_suite/tests/test_macros.rs index 6b2fc4c5d..21f8db7d8 100644 --- a/test_suite/tests/test_macros.rs +++ b/test_suite/tests/test_macros.rs @@ -658,8 +658,7 @@ fn test_rename_all() { enum E { #[serde(rename_all = "camelCase")] Serialize { - serialize: bool, - serialize_seq: bool, + serialize: bool, etwas_ändern: bool }, #[serde(rename_all = "kebab-case")] SerializeSeq { @@ -676,21 +675,21 @@ fn test_rename_all() { #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "PascalCase")] struct S { - serialize: bool, + ändern: bool, serialize_seq: bool, } #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "SCREAMING-KEBAB-CASE")] struct ScreamingKebab { - serialize: bool, + grüße: bool, serialize_seq: bool, } assert_tokens( &E::Serialize { serialize: true, - serialize_seq: true, + etwas_ändern: true, }, &[ Token::StructVariant { @@ -700,7 +699,7 @@ fn test_rename_all() { }, Token::Str("serialize"), Token::Bool(true), - Token::Str("serializeSeq"), + Token::Str("etwasÄndern"), Token::Bool(true), Token::StructVariantEnd, ], @@ -746,12 +745,12 @@ fn test_rename_all() { assert_tokens( &S { - serialize: true, + ändern: true, serialize_seq: true, }, &[ Token::Struct { name: "S", len: 2 }, - Token::Str("Serialize"), + Token::Str("Ändern"), Token::Bool(true), Token::Str("SerializeSeq"), Token::Bool(true), @@ -761,7 +760,7 @@ fn test_rename_all() { assert_tokens( &ScreamingKebab { - serialize: true, + grüße: true, serialize_seq: true, }, &[ @@ -769,7 +768,7 @@ fn test_rename_all() { name: "ScreamingKebab", len: 2, }, - Token::Str("SERIALIZE"), + Token::Str("GRÜSSE"), Token::Bool(true), Token::Str("SERIALIZE-SEQ"), Token::Bool(true),