diff options
Diffstat (limited to 'src/helpers/case_style.rs')
-rw-r--r-- | src/helpers/case_style.rs | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/src/helpers/case_style.rs b/src/helpers/case_style.rs new file mode 100644 index 0000000..86a8583 --- /dev/null +++ b/src/helpers/case_style.rs @@ -0,0 +1,177 @@ +use heck::{ + ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, ToTrainCase, +}; +use std::str::FromStr; +use syn::{ + parse::{Parse, ParseStream}, + Ident, LitStr, +}; + +#[allow(clippy::enum_variant_names)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CaseStyle { + CamelCase, + KebabCase, + MixedCase, + ShoutySnakeCase, + SnakeCase, + TitleCase, + UpperCase, + LowerCase, + ScreamingKebabCase, + PascalCase, + TrainCase, +} + +const VALID_CASE_STYLES: &[&str] = &[ + "camelCase", + "PascalCase", + "kebab-case", + "snake_case", + "SCREAMING_SNAKE_CASE", + "SCREAMING-KEBAB-CASE", + "lowercase", + "UPPERCASE", + "title_case", + "mixed_case", + "Train-Case", +]; + +impl Parse for CaseStyle { + fn parse(input: ParseStream) -> syn::Result<Self> { + let text = input.parse::<LitStr>()?; + let val = text.value(); + + val.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected case style for serialize_all: `{}`. Valid values are: `{:?}`", + val, VALID_CASE_STYLES + ), + ) + }) + } +} + +impl FromStr for CaseStyle { + type Err = (); + + fn from_str(text: &str) -> Result<Self, ()> { + Ok(match text { + // "camel_case" is a soft-deprecated case-style left for backward compatibility. + // <https://github.com/Peternator7/strum/pull/250#issuecomment-1374682221> + "PascalCase" | "camel_case" => CaseStyle::PascalCase, + "camelCase" => CaseStyle::CamelCase, + "snake_case" | "snek_case" => CaseStyle::SnakeCase, + "kebab-case" | "kebab_case" => CaseStyle::KebabCase, + "SCREAMING-KEBAB-CASE" => CaseStyle::ScreamingKebabCase, + "SCREAMING_SNAKE_CASE" | "shouty_snake_case" | "shouty_snek_case" => { + CaseStyle::ShoutySnakeCase + } + "title_case" => CaseStyle::TitleCase, + "mixed_case" => CaseStyle::MixedCase, + "lowercase" => CaseStyle::LowerCase, + "UPPERCASE" => CaseStyle::UpperCase, + "Train-Case" => CaseStyle::TrainCase, + _ => return Err(()), + }) + } +} + +pub trait CaseStyleHelpers { + fn convert_case(&self, case_style: Option<CaseStyle>) -> String; +} + +impl CaseStyleHelpers for Ident { + fn convert_case(&self, case_style: Option<CaseStyle>) -> String { + let ident_string = self.to_string(); + if let Some(case_style) = case_style { + match case_style { + CaseStyle::PascalCase => ident_string.to_upper_camel_case(), + CaseStyle::KebabCase => ident_string.to_kebab_case(), + CaseStyle::MixedCase => ident_string.to_lower_camel_case(), + CaseStyle::ShoutySnakeCase => ident_string.to_shouty_snake_case(), + CaseStyle::SnakeCase => ident_string.to_snake_case(), + CaseStyle::TitleCase => ident_string.to_title_case(), + CaseStyle::UpperCase => ident_string.to_uppercase(), + CaseStyle::LowerCase => ident_string.to_lowercase(), + CaseStyle::ScreamingKebabCase => ident_string.to_kebab_case().to_uppercase(), + CaseStyle::TrainCase => ident_string.to_train_case(), + CaseStyle::CamelCase => { + let camel_case = ident_string.to_upper_camel_case(); + let mut pascal = String::with_capacity(camel_case.len()); + let mut it = camel_case.chars(); + if let Some(ch) = it.next() { + pascal.extend(ch.to_lowercase()); + } + pascal.extend(it); + pascal + } + } + } else { + ident_string + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_case() { + let id = Ident::new("test_me", proc_macro2::Span::call_site()); + assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase))); + assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase))); + assert_eq!("Test-Me", id.convert_case(Some(CaseStyle::TrainCase))); + } + + #[test] + fn test_impl_from_str_for_case_style_pascal_case() { + use CaseStyle::*; + let f = CaseStyle::from_str; + + assert_eq!(PascalCase, f("PascalCase").unwrap()); + assert_eq!(PascalCase, f("camel_case").unwrap()); + + assert_eq!(CamelCase, f("camelCase").unwrap()); + + assert_eq!(SnakeCase, f("snake_case").unwrap()); + assert_eq!(SnakeCase, f("snek_case").unwrap()); + + assert_eq!(KebabCase, f("kebab-case").unwrap()); + assert_eq!(KebabCase, f("kebab_case").unwrap()); + + assert_eq!(ScreamingKebabCase, f("SCREAMING-KEBAB-CASE").unwrap()); + + assert_eq!(ShoutySnakeCase, f("SCREAMING_SNAKE_CASE").unwrap()); + assert_eq!(ShoutySnakeCase, f("shouty_snake_case").unwrap()); + assert_eq!(ShoutySnakeCase, f("shouty_snek_case").unwrap()); + + assert_eq!(LowerCase, f("lowercase").unwrap()); + + assert_eq!(UpperCase, f("UPPERCASE").unwrap()); + + assert_eq!(TitleCase, f("title_case").unwrap()); + + assert_eq!(MixedCase, f("mixed_case").unwrap()); + } +} + +/// heck doesn't treat numbers as new words, but this function does. +/// E.g. for input `Hello2You`, heck would output `hello2_you`, and snakify would output `hello_2_you`. +pub fn snakify(s: &str) -> String { + let mut output: Vec<char> = s.to_string().to_snake_case().chars().collect(); + let mut num_starts = vec![]; + for (pos, c) in output.iter().enumerate() { + if c.is_digit(10) && pos != 0 && !output[pos - 1].is_digit(10) { + num_starts.push(pos); + } + } + // need to do in reverse, because after inserting, all chars after the point of insertion are off + for i in num_starts.into_iter().rev() { + output.insert(i, '_') + } + output.into_iter().collect() +} |