Skip to content

Commit

Permalink
Fix extract attribute conflict with serde (#618)
Browse files Browse the repository at this point in the history
* fix: ToParameters will fail when using rename when deserializing

* done

* cargo fmt

* done

* cargo fmt

* fix ci

* done
  • Loading branch information
chrislearn authored Jan 10, 2024
1 parent 858ad0a commit 063b292
Show file tree
Hide file tree
Showing 20 changed files with 706 additions and 437 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
max-parallel: 1
matrix:
package:
- crate: salvo-serde-util
path: crates/salvo-serde-util
- crate: salvo_macros
path: crates/macros
- crate: salvo_core
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ salvo-oapi-macros = { version = "0.63.1", path = "crates/oapi-macros", default-f
salvo-otel = { version = "0.63.1", path = "crates/otel", default-features = false }
salvo-proxy = { version = "0.63.1", path = "crates/proxy", default-features = false }
salvo-rate-limiter = { version = "0.63.1", path = "crates/rate-limiter", default-features = false }
salvo-serde-util = { version = "0.63.1", path = "crates/serde-util", default-features = true }
salvo-serve-static = { version = "0.63.1", path = "crates/serve-static", default-features = false }
salvo-session = { version = "0.63.1", path = "crates/session", default-features = false }

Expand All @@ -50,7 +51,6 @@ bcrypt = "0.15"
cookie = "0.18"
chacha20poly1305 = "0.10"
chrono = "0.4"
cruet = "0.14"
encoding_rs = "0.8"
email_address = "0.2"
enumflags2 = "0.7"
Expand Down
3 changes: 1 addition & 2 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[features]
default = ["cookie", "fix-http1-request-uri", "server", "http1", "http2"]
default = ["cookie", "fix-http1-request-uri", "server", "http1", "http2", "test"]
full = ["cookie", "fix-http1-request-uri", "server", "http1", "http2", "quinn", "rustls", "native-tls", "openssl", "unix", "test", "tower-compat", "anyhow", "eyre"]
cookie = ["dep:cookie"]
fix-http1-request-uri = ["http1"]
Expand All @@ -41,7 +41,6 @@ async-trait = { workspace = true }
base64 = { workspace = true }
bytes = { workspace = true }
cookie = { workspace = true, features = ["percent-encode", "private", "signed"], optional = true }
cruet = { workspace = true }
encoding_rs = { workspace = true, optional = true }
enumflags2 = { workspace = true }
eyre = { workspace = true, optional = true }
Expand Down
170 changes: 170 additions & 0 deletions crates/core/src/extract/case.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::str::FromStr;

use self::RenameRule::*;

/// Rename rule for a field.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[non_exhaustive]
pub enum RenameRule {
/// Rename direct children to "lowercase" style.
LowerCase,
/// Rename direct children to "UPPERCASE" style.
UpperCase,
/// Rename direct children to "PascalCase" style, as typically used for
/// enum variants.
PascalCase,
/// Rename direct children to "camelCase" style.
CamelCase,
/// Rename direct children to "snake_case" style, as commonly used for
/// fields.
SnakeCase,
/// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly
/// used for constants.
ScreamingSnakeCase,
/// Rename direct children to "kebab-case" style.
KebabCase,
/// Rename direct children to "SCREAMING-KEBAB-CASE" style.
ScreamingKebabCase,
}

impl FromStr for RenameRule {
type Err = crate::Error;

fn from_str(input: &str) -> Result<Self, Self::Err> {
for (name, rule) in RENAME_RULES {
if input == name {
return Ok(rule);
}
}
Err(crate::Error::other(format!("invalid rename rule: {input}")))
}
}

const RENAME_RULES: [(&str, RenameRule); 8] = [
("lowercase", LowerCase),
("UPPERCASE", UpperCase),
("PascalCase", PascalCase),
("camelCase", CamelCase),
("snake_case", SnakeCase),
("SCREAMING_SNAKE_CASE", ScreamingSnakeCase),
("kebab-case", KebabCase),
("SCREAMING-KEBAB-CASE", ScreamingKebabCase),
];
impl RenameRule {
/// Apply a renaming rule to an enum variant, returning the version expected in the source.
pub fn apply_to_variant(&self, variant: impl AsRef<str>) -> String {
let variant = variant.as_ref();
match self {
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
}
ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
ScreamingKebabCase => ScreamingSnakeCase.apply_to_variant(variant).replace('_', "-"),
}
}

/// Apply a renaming rule to a struct field, returning the version expected in the source.
pub fn apply_to_field(self, field: &str) -> String {
match self {
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);
}
}
pascal
}
CamelCase => {
let pascal = PascalCase.apply_to_field(field);
pascal[..1].to_ascii_lowercase() + &pascal[1..]
}
ScreamingSnakeCase => field.to_ascii_uppercase(),
KebabCase => field.replace('_', "-"),
ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_rename_variants() {
for &(original, lower, upper, camel, snake, screaming, kebab, screaming_kebab) in &[
(
"Outcome", "outcome", "OUTCOME", "outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
),
(
"VeryTasty",
"verytasty",
"VERYTASTY",
"veryTasty",
"very_tasty",
"VERY_TASTY",
"very-tasty",
"VERY-TASTY",
),
("A", "a", "A", "a", "a", "A", "a", "A"),
("Z42", "z42", "Z42", "z42", "z42", "Z42", "z42", "Z42"),
] {
assert_eq!(LowerCase.apply_to_variant(original), lower);
assert_eq!(UpperCase.apply_to_variant(original), upper);
assert_eq!(PascalCase.apply_to_variant(original), original);
assert_eq!(CamelCase.apply_to_variant(original), camel);
assert_eq!(SnakeCase.apply_to_variant(original), snake);
assert_eq!(ScreamingSnakeCase.apply_to_variant(original), screaming);
assert_eq!(KebabCase.apply_to_variant(original), kebab);
assert_eq!(ScreamingKebabCase.apply_to_variant(original), screaming_kebab);
}
}

#[test]
fn test_rename_fields() {
for &(original, upper, pascal, camel, screaming, kebab, screaming_kebab) in &[
(
"outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
),
(
"very_tasty",
"VERY_TASTY",
"VeryTasty",
"veryTasty",
"VERY_TASTY",
"very-tasty",
"VERY-TASTY",
),
("a", "A", "A", "a", "A", "a", "A"),
("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"),
] {
assert_eq!(UpperCase.apply_to_field(original), upper);
assert_eq!(PascalCase.apply_to_field(original), pascal);
assert_eq!(CamelCase.apply_to_field(original), camel);
assert_eq!(SnakeCase.apply_to_field(original), original);
assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming);
assert_eq!(KebabCase.apply_to_field(original), kebab);
assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab);
}
}
}
Loading

0 comments on commit 063b292

Please sign in to comment.