Skip to main content

strum_macros/helpers/
case_style.rs

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 = 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
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/// heck doesn't treat numbers as new words, but this function does.
120/// E.g. for input `Hello2You`, heck would output `hello2_you`, and snakify would output `hello_2_you`.
121pub fn snakify(s: &str) -> String {
122    let mut output: Vec<char> = s.to_string().to_snake_case().chars().collect();
123    let mut num_starts = vec![];
124    for (pos, c) in output.iter().enumerate() {
125        if c.is_ascii_digit() && pos != 0 && !output[pos - 1].is_ascii_digit() {
126            num_starts.push(pos);
127        }
128    }
129    // need to do in reverse, because after inserting, all chars after the point of insertion are off
130    for i in num_starts.into_iter().rev() {
131        output.insert(i, '_')
132    }
133    output.into_iter().collect()
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_convert_case() {
142        let id = Ident::new("test_me", proc_macro2::Span::call_site());
143        assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase)));
144        assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase)));
145        assert_eq!("Test-Me", id.convert_case(Some(CaseStyle::TrainCase)));
146    }
147
148    #[test]
149    fn test_impl_from_str_for_case_style_pascal_case() {
150        use CaseStyle::*;
151        let f = CaseStyle::from_str;
152
153        assert_eq!(PascalCase, f("PascalCase").unwrap());
154        assert_eq!(PascalCase, f("camel_case").unwrap());
155
156        assert_eq!(CamelCase, f("camelCase").unwrap());
157
158        assert_eq!(SnakeCase, f("snake_case").unwrap());
159        assert_eq!(SnakeCase, f("snek_case").unwrap());
160
161        assert_eq!(KebabCase, f("kebab-case").unwrap());
162        assert_eq!(KebabCase, f("kebab_case").unwrap());
163
164        assert_eq!(ScreamingKebabCase, f("SCREAMING-KEBAB-CASE").unwrap());
165
166        assert_eq!(ShoutySnakeCase, f("SCREAMING_SNAKE_CASE").unwrap());
167        assert_eq!(ShoutySnakeCase, f("shouty_snake_case").unwrap());
168        assert_eq!(ShoutySnakeCase, f("shouty_snek_case").unwrap());
169
170        assert_eq!(LowerCase, f("lowercase").unwrap());
171
172        assert_eq!(UpperCase, f("UPPERCASE").unwrap());
173
174        assert_eq!(TitleCase, f("title_case").unwrap());
175
176        assert_eq!(MixedCase, f("mixed_case").unwrap());
177    }
178}