1use heck::{
2 ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase,
3};
4use std::str::FromStr;
5use syn::{
6 parse::{Parse, ParseStream},
7 Ident, LitStr,
8};
9
10#[allow(clippy::enum_variant_names)]
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum CaseStyle {
13 CamelCase,
14 KebabCase,
15 MixedCase,
16 ShoutySnakeCase,
17 SnakeCase,
18 TitleCase,
19 UpperCase,
20 LowerCase,
21 ScreamingKebabCase,
22 PascalCase,
23}
24
25const VALID_CASE_STYLES: &[&str] = &[
26 "camelCase",
27 "PascalCase",
28 "kebab-case",
29 "snake_case",
30 "SCREAMING_SNAKE_CASE",
31 "SCREAMING-KEBAB-CASE",
32 "lowercase",
33 "UPPERCASE",
34 "title_case",
35 "mixed_case",
36];
37
38impl Parse for CaseStyle {
39 fn parse(input: ParseStream) -> syn::Result<Self> {
40 let text: LitStr = input.parse::<LitStr>()?;
41 let val: String = text.value();
42
43 val.as_str().parse().map_err(|_| {
44 syn::Error::new_spanned(
45 &text,
46 message:format!(
47 "Unexpected case style for serialize_all: `{}`. Valid values are: `{:?}`",
48 val, VALID_CASE_STYLES
49 ),
50 )
51 })
52 }
53}
54
55impl FromStr for CaseStyle {
56 type Err = ();
57
58 fn from_str(text: &str) -> Result<Self, ()> {
59 Ok(match text {
60 "camel_case" | "PascalCase" => CaseStyle::PascalCase,
61 "camelCase" => CaseStyle::CamelCase,
62 "snake_case" | "snek_case" => CaseStyle::SnakeCase,
63 "kebab_case" | "kebab-case" => CaseStyle::KebabCase,
64 "SCREAMING-KEBAB-CASE" => CaseStyle::ScreamingKebabCase,
65 "shouty_snake_case" | "shouty_snek_case" | "SCREAMING_SNAKE_CASE" => {
66 CaseStyle::ShoutySnakeCase
67 }
68 "title_case" => CaseStyle::TitleCase,
69 "mixed_case" => CaseStyle::MixedCase,
70 "lowercase" => CaseStyle::LowerCase,
71 "UPPERCASE" => CaseStyle::UpperCase,
72 _ => return Err(()),
73 })
74 }
75}
76
77pub trait CaseStyleHelpers {
78 fn convert_case(&self, case_style: Option<CaseStyle>) -> String;
79}
80
81impl CaseStyleHelpers for Ident {
82 fn convert_case(&self, case_style: Option<CaseStyle>) -> String {
83 let ident_string = self.to_string();
84 if let Some(case_style) = case_style {
85 match case_style {
86 CaseStyle::PascalCase => ident_string.to_upper_camel_case(),
87 CaseStyle::KebabCase => ident_string.to_kebab_case(),
88 CaseStyle::MixedCase => ident_string.to_lower_camel_case(),
89 CaseStyle::ShoutySnakeCase => ident_string.to_shouty_snake_case(),
90 CaseStyle::SnakeCase => ident_string.to_snake_case(),
91 CaseStyle::TitleCase => ident_string.to_title_case(),
92 CaseStyle::UpperCase => ident_string.to_uppercase(),
93 CaseStyle::LowerCase => ident_string.to_lowercase(),
94 CaseStyle::ScreamingKebabCase => ident_string.to_kebab_case().to_uppercase(),
95 CaseStyle::CamelCase => {
96 let camel_case = ident_string.to_upper_camel_case();
97 let mut pascal = String::with_capacity(camel_case.len());
98 let mut it = camel_case.chars();
99 if let Some(ch) = it.next() {
100 pascal.extend(ch.to_lowercase());
101 }
102 pascal.extend(it);
103 pascal
104 }
105 }
106 } else {
107 ident_string
108 }
109 }
110}
111
112#[test]
113fn test_convert_case() {
114 let id: Ident = Ident::new(string:"test_me", proc_macro2::Span::call_site());
115 assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase)));
116 assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase)));
117}
118