convert_case/pattern.rs
1use alloc::string::{String, ToString};
2use alloc::vec::Vec;
3
4fn lowercase_word(word: &str) -> String {
5 word.to_lowercase()
6}
7
8fn uppercase_word(word: &str) -> String {
9 word.to_uppercase()
10}
11
12/// Applies capital pattern to a single word using graphemes.
13fn capital_word(word: &str) -> String {
14 let mut chars = word.chars();
15
16 if let Some(c) = chars.next() {
17 [c.to_uppercase().collect(), chars.as_str().to_lowercase()].concat()
18 } else {
19 String::new()
20 }
21}
22
23/// Transformations on a list of words.
24///
25/// A pattern is a function that maps a list of words into another list
26/// after changing the casing of each letter. How a patterns mutates
27/// each letter can be dependent on the word the letters are present in.
28///
29/// ## Custom Pattern
30///
31/// A pattern is a function that maps from a borrowed list of words `&[&str]` to
32/// an owned list of owned words `Vec<String>` by applying a transformation.
33/// One example of custom behavior might be a pattern
34/// that detects a fixed list of two-letter acronyms, and capitalizes them
35/// appropriately on output.
36/// ```
37/// use convert_case::{Converter, Pattern};
38///
39/// fn pascal_upper_acronyms(words: &[&str]) -> Vec<String> {
40/// Pattern::Capital.mutate(words).into_iter()
41/// .map(|word| match word.as_ref() {
42/// "Io" | "Xml" => word.to_uppercase(),
43/// _ => word,
44/// })
45/// .collect()
46/// }
47///
48/// let acronym_converter = Converter::new()
49/// .set_patterns(&[Pattern::Custom(pascal_upper_acronyms)]);
50///
51/// assert_eq!(acronym_converter.convert("io_stream"), "IOStream");
52/// assert_eq!(acronym_converter.convert("xml request"), "XMLRequest");
53/// ```
54///
55/// Another example might be a one that explicitly adds leading
56/// and trailing double underscores. We do this by modifying the words directly,
57/// which will get passed as-is to the join function.
58/// ```
59/// use convert_case::{Converter, Pattern};
60///
61/// fn snake_dunder(mut words: &[&str]) -> Vec<String> {
62/// words
63/// .into_iter()
64/// .map(|word| word.to_lowercase())
65/// .enumerate()
66/// .map(|(i, word)| {
67/// if words.len() == 1 {
68/// format!("__{}__", word)
69/// } else if i == 0 {
70/// format!("__{}", word)
71/// } else if i == words.len() - 1 {
72/// format!("{}__", word)
73/// } else {
74/// word
75/// }
76/// })
77/// .collect()
78/// }
79///
80/// let dunder_converter = Converter::new()
81/// .set_patterns(&[Pattern::Custom(snake_dunder)])
82/// .set_delimiter("_");
83///
84/// assert_eq!(dunder_converter.convert("getAttr"), "__get_attr__");
85/// assert_eq!(dunder_converter.convert("ITER"), "__iter__");
86/// ```
87#[derive(Debug, Copy, Clone)]
88pub enum Pattern {
89 /// Makes all words lowercase.
90 /// ```
91 /// # use convert_case::Pattern;
92 /// assert_eq!(
93 /// Pattern::Lowercase.mutate(&["Case", "CONVERSION", "library"]),
94 /// vec!["case", "conversion", "library"],
95 /// );
96 /// ```
97 Lowercase,
98
99 /// Makes all words uppercase.
100 /// ```
101 /// # use convert_case::Pattern;
102 /// assert_eq!(
103 /// Pattern::Uppercase.mutate(&["Case", "CONVERSION", "library"]),
104 /// vec!["CASE", "CONVERSION", "LIBRARY"],
105 /// );
106 /// ```
107 Uppercase,
108
109 /// Makes the first letter of each word uppercase
110 /// and the remaining letters of each word lowercase.
111 /// ```
112 /// # use convert_case::Pattern;
113 /// assert_eq!(
114 /// Pattern::Capital.mutate(&["Case", "CONVERSION", "library"]),
115 /// vec!["Case", "Conversion", "Library"],
116 /// );
117 /// ```
118 Capital,
119
120 /// Makes the first non-empty word lowercase and the
121 /// remaining capitalized.
122 /// ```
123 /// # use convert_case::Pattern;
124 /// assert_eq!(
125 /// Pattern::Camel.mutate(&["Case", "CONVERSION", "library"]),
126 /// vec!["case", "Conversion", "Library"],
127 /// );
128 /// ```
129 Camel,
130
131 /// Makes the first non-empty word capitalized and the
132 /// remaining lowercase.
133 /// ```
134 /// # use convert_case::Pattern;
135 /// assert_eq!(
136 /// Pattern::Sentence.mutate(&["Case", "CONVERSION", "library"]),
137 /// vec!["Case", "conversion", "library"],
138 /// );
139 /// ```
140 Sentence,
141
142 /// Filters out empty words from the list.
143 /// Useful when splitting produces empty words from leading/trailing/duplicate delimiters.
144 /// ```
145 /// # use convert_case::Pattern;
146 /// assert_eq!(
147 /// Pattern::RemoveEmpty.mutate(&["", "first", "", "second", ""]),
148 /// vec!["first", "second"],
149 /// );
150 /// ```
151 RemoveEmpty,
152
153 /// Define custom behavior to transform a set of words.
154 ///
155 /// See the [`Pattern`] documentation for examples.
156 Custom(fn(&[&str]) -> Vec<String>),
157}
158
159impl Pattern {
160 /// Applies the pattern transformation to a list of words.
161 pub fn mutate<S: AsRef<str>>(&self, words: &[S]) -> Vec<String> {
162 use Pattern::*;
163 match self {
164 Custom(transformation) => {
165 let borrowed: Vec<&str> = words.iter().map(|s| s.as_ref()).collect();
166 (transformation)(&borrowed)
167 }
168 Lowercase => words
169 .iter()
170 .map(|word| lowercase_word(word.as_ref()))
171 .collect(),
172 Uppercase => words
173 .iter()
174 .map(|word| uppercase_word(word.as_ref()))
175 .collect(),
176 Capital => words
177 .iter()
178 .map(|word| capital_word(word.as_ref()))
179 .collect(),
180 Camel => words
181 .iter()
182 .enumerate()
183 .map(|(i, word)| {
184 if i == 0 {
185 lowercase_word(word.as_ref())
186 } else {
187 capital_word(word.as_ref())
188 }
189 })
190 .collect(),
191 Sentence => words
192 .iter()
193 .enumerate()
194 .map(|(i, word)| {
195 if i == 0 {
196 capital_word(word.as_ref())
197 } else {
198 lowercase_word(word.as_ref())
199 }
200 })
201 .collect(),
202 RemoveEmpty => words
203 .iter()
204 .filter(|word| !word.as_ref().is_empty())
205 .map(|word| word.as_ref().to_string())
206 .collect(),
207 }
208 }
209}
210
211impl PartialEq for Pattern {
212 fn eq(&self, other: &Self) -> bool {
213 match (self, other) {
214 (Self::Lowercase, Self::Lowercase) => true,
215 (Self::Uppercase, Self::Uppercase) => true,
216 (Self::Capital, Self::Capital) => true,
217 (Self::Camel, Self::Camel) => true,
218 (Self::Sentence, Self::Sentence) => true,
219 (Self::RemoveEmpty, Self::RemoveEmpty) => true,
220 // Custom patterns are never equal because they contain function pointers,
221 // which cannot be reliably compared.
222 (Self::Custom(_), Self::Custom(_)) => false,
223 _ => false,
224 }
225 }
226}
227
228impl Eq for Pattern {}
229
230impl core::hash::Hash for Pattern {
231 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
232 // Hash the discriminant for all variants
233 core::mem::discriminant(self).hash(state);
234 // Custom variants only hash the discriminant since they can't be meaningfully compared
235 }
236}
237
238#[cfg(test)]
239mod test {
240 use crate::Case;
241 use crate::Converter;
242
243 use super::*;
244
245 #[test]
246 fn mutate_empty_strings() {
247 for word_pattern in [lowercase_word, uppercase_word, capital_word] {
248 assert_eq!(String::new(), word_pattern(&String::new()))
249 }
250 }
251
252 #[test]
253 fn filtering_with_remove_empty() {
254 let conv = Converter::new()
255 .from_case(Case::Kebab)
256 .set_patterns(&[Pattern::RemoveEmpty, Pattern::Camel]);
257
258 assert_eq!(conv.convert("--leading-delims"), "leadingDelims");
259 }
260
261 #[test]
262 fn remove_empty_pattern() {
263 assert_eq!(
264 Pattern::RemoveEmpty.mutate(&["", "first", "", "second", ""]),
265 vec!["first", "second"]
266 );
267 assert_eq!(Pattern::RemoveEmpty.mutate(&["only"]), vec!["only"]);
268 assert_eq!(
269 Pattern::RemoveEmpty.mutate(&["", "", ""]),
270 Vec::<String>::new()
271 );
272 }
273}