1 use heck::{
2 ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, ToTrainCase,
3 };
4 use std::str::FromStr;
5 use syn::{
6 parse::{Parse, ParseStream},
7 Ident, LitStr,
8 };
9
10 #[allow(clippy::enum_variant_names)]
11 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12 pub enum CaseStyle {
13 CamelCase,
14 KebabCase,
15 MixedCase,
16 ShoutySnakeCase,
17 SnakeCase,
18 TitleCase,
19 UpperCase,
20 LowerCase,
21 ScreamingKebabCase,
22 PascalCase,
23 TrainCase,
24 }
25
26 const VALID_CASE_STYLES: &[&str] = &[
27 "camelCase",
28 "PascalCase",
29 "kebab-case",
30 "snake_case",
31 "SCREAMING_SNAKE_CASE",
32 "SCREAMING-KEBAB-CASE",
33 "lowercase",
34 "UPPERCASE",
35 "title_case",
36 "mixed_case",
37 "Train-Case",
38 ];
39
40 impl Parse for CaseStyle {
parse(input: ParseStream) -> syn::Result<Self>41 fn parse(input: ParseStream) -> syn::Result<Self> {
42 let text = input.parse::<LitStr>()?;
43 let val = text.value();
44
45 val.as_str().parse().map_err(|_| {
46 syn::Error::new_spanned(
47 &text,
48 format!(
49 "Unexpected case style for serialize_all: `{}`. Valid values are: `{:?}`",
50 val, VALID_CASE_STYLES
51 ),
52 )
53 })
54 }
55 }
56
57 impl FromStr for CaseStyle {
58 type Err = ();
59
from_str(text: &str) -> Result<Self, ()>60 fn from_str(text: &str) -> Result<Self, ()> {
61 Ok(match text {
62 // "camel_case" is a soft-deprecated case-style left for backward compatibility.
63 // <https://github.com/Peternator7/strum/pull/250#issuecomment-1374682221>
64 "PascalCase" | "camel_case" => CaseStyle::PascalCase,
65 "camelCase" => CaseStyle::CamelCase,
66 "snake_case" | "snek_case" => CaseStyle::SnakeCase,
67 "kebab-case" | "kebab_case" => CaseStyle::KebabCase,
68 "SCREAMING-KEBAB-CASE" => CaseStyle::ScreamingKebabCase,
69 "SCREAMING_SNAKE_CASE" | "shouty_snake_case" | "shouty_snek_case" => {
70 CaseStyle::ShoutySnakeCase
71 }
72 "title_case" => CaseStyle::TitleCase,
73 "mixed_case" => CaseStyle::MixedCase,
74 "lowercase" => CaseStyle::LowerCase,
75 "UPPERCASE" => CaseStyle::UpperCase,
76 "Train-Case" => CaseStyle::TrainCase,
77 _ => return Err(()),
78 })
79 }
80 }
81
82 pub trait CaseStyleHelpers {
convert_case(&self, case_style: Option<CaseStyle>) -> String83 fn convert_case(&self, case_style: Option<CaseStyle>) -> String;
84 }
85
86 impl CaseStyleHelpers for Ident {
convert_case(&self, case_style: Option<CaseStyle>) -> String87 fn convert_case(&self, case_style: Option<CaseStyle>) -> String {
88 let ident_string = self.to_string();
89 if let Some(case_style) = case_style {
90 match case_style {
91 CaseStyle::PascalCase => ident_string.to_upper_camel_case(),
92 CaseStyle::KebabCase => ident_string.to_kebab_case(),
93 CaseStyle::MixedCase => ident_string.to_lower_camel_case(),
94 CaseStyle::ShoutySnakeCase => ident_string.to_shouty_snake_case(),
95 CaseStyle::SnakeCase => ident_string.to_snake_case(),
96 CaseStyle::TitleCase => ident_string.to_title_case(),
97 CaseStyle::UpperCase => ident_string.to_uppercase(),
98 CaseStyle::LowerCase => ident_string.to_lowercase(),
99 CaseStyle::ScreamingKebabCase => ident_string.to_kebab_case().to_uppercase(),
100 CaseStyle::TrainCase => ident_string.to_train_case(),
101 CaseStyle::CamelCase => {
102 let camel_case = ident_string.to_upper_camel_case();
103 let mut pascal = String::with_capacity(camel_case.len());
104 let mut it = camel_case.chars();
105 if let Some(ch) = it.next() {
106 pascal.extend(ch.to_lowercase());
107 }
108 pascal.extend(it);
109 pascal
110 }
111 }
112 } else {
113 ident_string
114 }
115 }
116 }
117
118 #[cfg(test)]
119 mod tests {
120 use super::*;
121
122 #[test]
test_convert_case()123 fn test_convert_case() {
124 let id = Ident::new("test_me", proc_macro2::Span::call_site());
125 assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase)));
126 assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase)));
127 assert_eq!("Test-Me", id.convert_case(Some(CaseStyle::TrainCase)));
128 }
129
130 #[test]
test_impl_from_str_for_case_style_pascal_case()131 fn test_impl_from_str_for_case_style_pascal_case() {
132 use CaseStyle::*;
133 let f = CaseStyle::from_str;
134
135 assert_eq!(PascalCase, f("PascalCase").unwrap());
136 assert_eq!(PascalCase, f("camel_case").unwrap());
137
138 assert_eq!(CamelCase, f("camelCase").unwrap());
139
140 assert_eq!(SnakeCase, f("snake_case").unwrap());
141 assert_eq!(SnakeCase, f("snek_case").unwrap());
142
143 assert_eq!(KebabCase, f("kebab-case").unwrap());
144 assert_eq!(KebabCase, f("kebab_case").unwrap());
145
146 assert_eq!(ScreamingKebabCase, f("SCREAMING-KEBAB-CASE").unwrap());
147
148 assert_eq!(ShoutySnakeCase, f("SCREAMING_SNAKE_CASE").unwrap());
149 assert_eq!(ShoutySnakeCase, f("shouty_snake_case").unwrap());
150 assert_eq!(ShoutySnakeCase, f("shouty_snek_case").unwrap());
151
152 assert_eq!(LowerCase, f("lowercase").unwrap());
153
154 assert_eq!(UpperCase, f("UPPERCASE").unwrap());
155
156 assert_eq!(TitleCase, f("title_case").unwrap());
157
158 assert_eq!(MixedCase, f("mixed_case").unwrap());
159 }
160 }
161
162 /// heck doesn't treat numbers as new words, but this function does.
163 /// E.g. for input `Hello2You`, heck would output `hello2_you`, and snakify would output `hello_2_you`.
snakify(s: &str) -> String164 pub fn snakify(s: &str) -> String {
165 let mut output: Vec<char> = s.to_string().to_snake_case().chars().collect();
166 let mut num_starts = vec![];
167 for (pos, c) in output.iter().enumerate() {
168 if c.is_digit(10) && pos != 0 && !output[pos - 1].is_digit(10) {
169 num_starts.push(pos);
170 }
171 }
172 // need to do in reverse, because after inserting, all chars after the point of insertion are off
173 for i in num_starts.into_iter().rev() {
174 output.insert(i, '_')
175 }
176 output.into_iter().collect()
177 }
178