use darling::FromMeta;
use syn::Lit;
use self::RenameRule::{
CamelCase, KebabCase, LowerCase, None, PascalCase, ScreamingKebabCase, ScreamingSnakeCase,
SnakeCase, UpperCase,
};
use std::fmt::{self, Debug, Display};
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum RenameRule {
#[default]
None,
LowerCase,
UpperCase,
PascalCase,
CamelCase,
SnakeCase,
ScreamingSnakeCase,
KebabCase,
ScreamingKebabCase,
}
impl FromMeta for RenameRule {
fn from_value(value: &Lit) -> darling::Result<Self> {
let Lit::Str(litstr) = value else {
return Err(darling::Error::unexpected_lit_type(value));
};
Self::from_str(&litstr.value()).map_err(darling::Error::custom)
}
}
static RENAME_RULES: &[(&str, RenameRule)] = &[
("lowercase", LowerCase),
("UPPERCASE", UpperCase),
("PascalCase", PascalCase),
("camelCase", CamelCase),
("snake_case", SnakeCase),
("SCREAMING_SNAKE_CASE", ScreamingSnakeCase),
("kebab-case", KebabCase),
("SCREAMING-KEBAB-CASE", ScreamingKebabCase),
];
impl RenameRule {
pub fn from_str(rename_all_str: &str) -> Result<Self, ParseError> {
RENAME_RULES
.iter()
.find(|(name, _)| *name == rename_all_str)
.map(|(_, rule)| *rule)
.ok_or_else(|| ParseError {
unknown: rename_all_str.to_string(),
})
}
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
}
ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
ScreamingKebabCase => ScreamingSnakeCase
.apply_to_variant(variant)
.replace('_', "-"),
}
}
pub fn apply_to_field(self, field: &str) -> String {
match self {
None | LowerCase | SnakeCase => field.to_owned(),
UpperCase | ScreamingSnakeCase => 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..]
}
KebabCase => field.replace('_', "-"),
ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
}
}
pub fn or(self, rule_b: Self) -> Self {
match self {
None => rule_b,
_ => self,
}
}
}
pub struct ParseError {
unknown: String,
}
impl Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("unknown rename rule `rename_all = ")?;
Debug::fmt(&self.unknown, f)?;
f.write_str("`, expected one of ")?;
for (i, (name, _rule)) in RENAME_RULES.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
Debug::fmt(name, f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::case::RenameRule::{
CamelCase, KebabCase, LowerCase, PascalCase, ScreamingKebabCase, ScreamingSnakeCase,
SnakeCase, UpperCase,
};
#[test]
fn 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!(
crate::case::RenameRule::None.apply_to_variant(original),
original
);
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 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!(
crate::case::RenameRule::None.apply_to_field(original),
original
);
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);
}
}
}