• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 use heck::{
2     ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToTrainCase,
3     ToUpperCamelCase,
4 };
5 use std::str::FromStr;
6 use syn::{
7     parse::{Parse, ParseStream},
8     Ident, LitStr,
9 };
10 
11 #[allow(clippy::enum_variant_names)]
12 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13 pub 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 
27 const 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 
41 impl Parse for CaseStyle {
parse(input: ParseStream) -> syn::Result<Self>42     fn parse(input: ParseStream) -> syn::Result<Self> {
43         let text = input.parse::<LitStr>()?;
44         let val = text.value();
45 
46         val.as_str().parse().map_err(|_| {
47             syn::Error::new_spanned(
48                 &text,
49                 format!(
50                     "Unexpected case style for serialize_all: `{}`. Valid values are: `{:?}`",
51                     val, VALID_CASE_STYLES
52                 ),
53             )
54         })
55     }
56 }
57 
58 impl FromStr for CaseStyle {
59     type Err = ();
60 
from_str(text: &str) -> Result<Self, ()>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 
83 pub trait CaseStyleHelpers {
convert_case(&self, case_style: Option<CaseStyle>) -> String84     fn convert_case(&self, case_style: Option<CaseStyle>) -> String;
85 }
86 
87 impl CaseStyleHelpers for Ident {
convert_case(&self, case_style: Option<CaseStyle>) -> String88     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)]
120 mod tests {
121     use super::*;
122 
123     #[test]
test_convert_case()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]
test_impl_from_str_for_case_style_pascal_case()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`.
snakify(s: &str) -> String165 pub 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![];
168     for (pos, c) in output.iter().enumerate() {
169         if c.is_digit(10) && pos != 0 && !output[pos - 1].is_digit(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 in num_starts.into_iter().rev() {
175         output.insert(i, '_')
176     }
177     output.into_iter().collect()
178 }
179