1use serde::Deserialize;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum CaseConvention {
22 Lower,
23 Upper,
24 Pascal,
25 Camel,
26 Snake,
27 Kebab,
28 ScreamingSnake,
29 Flat,
30}
31
32impl CaseConvention {
33 pub fn display_name(self) -> &'static str {
34 match self {
35 Self::Lower => "lowercase",
36 Self::Upper => "uppercase",
37 Self::Pascal => "PascalCase",
38 Self::Camel => "camelCase",
39 Self::Snake => "snake_case",
40 Self::Kebab => "kebab-case",
41 Self::ScreamingSnake => "SCREAMING_SNAKE_CASE",
42 Self::Flat => "flatcase",
43 }
44 }
45
46 pub fn check(self, s: &str) -> bool {
47 if s.is_empty() {
48 return false;
49 }
50 match self {
51 Self::Lower => is_lowercase(s),
52 Self::Upper => is_uppercase(s),
53 Self::Pascal => is_pascal(s),
54 Self::Camel => is_camel(s),
55 Self::Snake => is_snake(s),
56 Self::Kebab => is_kebab(s),
57 Self::ScreamingSnake => is_screaming_snake(s),
58 Self::Flat => is_flat(s),
59 }
60 }
61}
62
63impl<'de> Deserialize<'de> for CaseConvention {
67 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
68 let raw: String = String::deserialize(d)?;
69 let canon: String = raw
70 .chars()
71 .filter(char::is_ascii_alphabetic)
72 .map(|c| c.to_ascii_lowercase())
73 .collect();
74 match canon.as_str() {
75 "lower" | "lowercase" => Ok(Self::Lower),
76 "upper" | "uppercase" => Ok(Self::Upper),
77 "pascal" | "pascalcase" | "uppercamel" | "uppercamelcase" => Ok(Self::Pascal),
78 "camel" | "camelcase" | "lowercamel" | "lowercamelcase" => Ok(Self::Camel),
79 "snake" | "snakecase" => Ok(Self::Snake),
80 "kebab" | "kebabcase" | "dash" | "dashcase" => Ok(Self::Kebab),
81 "screamingsnake" | "screamingsnakecase" | "upper_snake" | "uppersnakecase" => {
82 Ok(Self::ScreamingSnake)
83 }
84 "flat" | "flatcase" => Ok(Self::Flat),
85 other => Err(serde::de::Error::custom(format!(
86 "unknown case convention {raw:?} (normalized to {other:?})",
87 ))),
88 }
89 }
90}
91
92fn is_lowercase(s: &str) -> bool {
93 s.chars().all(|c| !c.is_alphabetic() || c.is_lowercase())
94}
95
96fn is_uppercase(s: &str) -> bool {
97 s.chars().all(|c| !c.is_alphabetic() || c.is_uppercase())
98}
99
100fn is_flat(s: &str) -> bool {
101 s.chars()
102 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
103}
104
105fn is_snake(s: &str) -> bool {
106 s.chars()
107 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
108}
109
110fn is_kebab(s: &str) -> bool {
111 s.chars()
112 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
113}
114
115fn is_screaming_snake(s: &str) -> bool {
116 s.chars()
117 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
118}
119
120fn is_camel(s: &str) -> bool {
121 check_camel_like(s, false)
122}
123
124fn is_pascal(s: &str) -> bool {
125 check_camel_like(s, true)
126}
127
128fn check_camel_like(s: &str, require_upper_first: bool) -> bool {
129 let mut chars = s.chars();
130 let Some(first) = chars.next() else {
131 return false;
132 };
133 if require_upper_first {
134 if !first.is_ascii_uppercase() {
135 return false;
136 }
137 } else if !first.is_ascii_lowercase() {
138 return false;
139 }
140 chars.all(|c| c.is_ascii_alphanumeric())
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn pascal_accepts_simple() {
149 assert!(is_pascal("Button"));
150 assert!(is_pascal("FooBar"));
151 assert!(is_pascal("Foo1Bar"));
152 assert!(is_pascal("A"));
153 assert!(is_pascal("XMLParser")); }
155
156 #[test]
157 fn pascal_rejects_wrong_shapes() {
158 assert!(!is_pascal(""));
159 assert!(!is_pascal("foo"));
160 assert!(!is_pascal("Foo_Bar"));
161 assert!(!is_pascal("Foo-Bar"));
162 }
163
164 #[test]
165 fn camel_accepts_simple() {
166 assert!(is_camel("fooBar"));
167 assert!(is_camel("foo"));
168 assert!(is_camel("ssrVFor"));
169 assert!(is_camel("getXMLParser")); assert!(is_camel("foo1Bar"));
171 }
172
173 #[test]
174 fn camel_rejects_wrong_shapes() {
175 assert!(!is_camel("FooBar"));
176 assert!(!is_camel(""));
177 assert!(!is_camel("foo_bar"));
178 }
179
180 #[test]
181 fn snake_kebab() {
182 assert!(is_snake("foo_bar_baz"));
183 assert!(!is_snake("fooBar"));
184 assert!(is_kebab("foo-bar-baz"));
185 assert!(!is_kebab("foo_bar"));
186 }
187
188 #[test]
189 fn screaming_snake() {
190 assert!(is_screaming_snake("FOO_BAR"));
191 assert!(is_screaming_snake("HELLO_2_WORLD"));
192 assert!(!is_screaming_snake("Foo_Bar"));
193 }
194
195 #[test]
196 fn flat_vs_lower() {
197 assert!(is_flat("helloworld"));
198 assert!(!is_flat("hello_world"));
199 assert!(is_lowercase("hello_world")); }
201
202 #[test]
203 fn alias_deserialization() {
204 use serde_yaml_ng::from_str;
205 let cases = &[
206 ("PascalCase", CaseConvention::Pascal),
207 ("pascal", CaseConvention::Pascal),
208 ("pascal-case", CaseConvention::Pascal),
209 ("UpperCamelCase", CaseConvention::Pascal),
210 ("camelCase", CaseConvention::Camel),
211 ("camel", CaseConvention::Camel),
212 ("kebab-case", CaseConvention::Kebab),
213 ("KEBAB", CaseConvention::Kebab),
214 ("snake_case", CaseConvention::Snake),
215 ("SCREAMING_SNAKE_CASE", CaseConvention::ScreamingSnake),
216 ("flatcase", CaseConvention::Flat),
217 ];
218 for (input, expected) in cases {
219 let parsed: CaseConvention = from_str(&format!("\"{input}\"")).unwrap();
220 assert_eq!(parsed, *expected, "input = {input}");
221 }
222 }
223}