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