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