Skip to main content

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}