Skip to main content

convert_case/
converter.rs

1use crate::boundary;
2use crate::boundary::Boundary;
3use crate::pattern::Pattern;
4use crate::Case;
5
6use alloc::string::{String, ToString};
7use alloc::vec;
8use alloc::vec::Vec;
9
10/// The parameters for performing a case conversion.
11///
12/// A `Converter` stores three fields needed for case conversion.
13/// 1) `boundaries`: how a string is split into _words_.
14/// 2) `patterns`: how words are mutated, or how each character's case will change.
15/// 3) `delimiter`: how the mutated words are joined into the final string.
16///
17/// Then calling [`convert`](Converter::convert) on a `Converter` will apply a case conversion
18/// defined by those fields.  The `Converter` struct is what is used underneath those functions
19/// available in the `Casing` struct.
20///
21/// You can use `Converter` when you need more specificity on conversion
22/// than those provided in `Casing`, or if it is simply more convenient or explicit.
23///
24/// ```
25/// use convert_case::{Boundary, Case, Casing, Converter, Pattern};
26///
27/// let s = "DialogueBox-border-shadow";
28///
29/// // Convert using Casing trait
30/// assert_eq!(
31///     s.from_case(Case::Kebab).to_case(Case::Snake),
32///     "dialoguebox_border_shadow",
33/// );
34///
35/// // Convert using similar methods on Converter
36/// let conv = Converter::new()
37///     .from_case(Case::Kebab)
38///     .to_case(Case::Snake);
39/// assert_eq!(conv.convert(s), "dialoguebox_border_shadow");
40///
41/// // Convert by setting each field explicitly
42/// let conv = Converter::new()
43///     .set_boundaries(&[Boundary::Hyphen])
44///     .set_patterns(&[Pattern::Lowercase])
45///     .set_delimiter("_");
46/// assert_eq!(conv.convert(s), "dialoguebox_border_shadow");
47/// ```
48///
49/// Or you can use `Converter` when you are performing a transformation
50/// not provided as a variant of `Case`.
51///
52/// ```
53/// # use convert_case::{Boundary, Case, Casing, Converter, Pattern};
54/// let dot_camel = Converter::new()
55///     .set_boundaries(&[Boundary::LowerUpper, Boundary::LowerDigit])
56///     .set_patterns(&[Pattern::Camel])
57///     .set_delimiter(".");
58/// assert_eq!(dot_camel.convert("CollisionShape2D"), "collision.Shape.2d");
59/// ```
60pub struct Converter {
61    /// How a string is split into words.
62    pub boundaries: Vec<Boundary>,
63
64    /// How each word is mutated before joining.
65    pub patterns: Vec<Pattern>,
66
67    /// The string used to join mutated words together.
68    pub delimiter: String,
69}
70
71impl Default for Converter {
72    fn default() -> Self {
73        Converter {
74            boundaries: Boundary::defaults().to_vec(),
75            patterns: Vec::new(),
76            delimiter: String::new(),
77        }
78    }
79}
80
81impl Converter {
82    /// Creates a new `Converter` with default fields.  This is the same as `Default::default()`.
83    /// The `Converter` will use [`Boundary::defaults()`] for boundaries, no patterns, and an empty
84    /// string as a delimiter.
85    /// ```
86    /// # use convert_case::Converter;
87    /// let conv = Converter::new();
88    /// assert_eq!(conv.convert("Ice-cream TRUCK"), "IcecreamTRUCK")
89    /// ```
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Converts a string.
95    /// ```
96    /// # use convert_case::{Case, Converter};
97    /// let conv = Converter::new()
98    ///     .to_case(Case::Camel);
99    /// assert_eq!(conv.convert("XML_HTTP_Request"), "xmlHttpRequest")
100    /// ```
101    pub fn convert<T>(&self, s: T) -> String
102    where
103        T: AsRef<str>,
104    {
105        let words = boundary::split(&s, &self.boundaries);
106
107        let mut result: Vec<String> = words.into_iter().map(|s| s.to_string()).collect();
108        for pattern in &self.patterns {
109            result = pattern.mutate(&result);
110        }
111
112        result.join(&self.delimiter)
113    }
114
115    /// Set the pattern and delimiter to those associated with the given case.
116    /// ```
117    /// # use convert_case::{Case, Converter};
118    /// let conv = Converter::new()
119    ///     .to_case(Case::Pascal);
120    /// assert_eq!(conv.convert("variable name"), "VariableName")
121    /// ```
122    pub fn to_case(mut self, case: Case) -> Self {
123        self.patterns.push(case.pattern());
124        self.delimiter = case.delimiter().to_string();
125        self
126    }
127
128    /// Sets the boundaries to those associated with the provided case.  This is used
129    /// by the `from_case` function in the `Casing` trait.
130    /// ```
131    /// # use convert_case::{Case, Converter};
132    /// let conv = Converter::new()
133    ///     .from_case(Case::Snake)
134    ///     .to_case(Case::Title);
135    /// assert_eq!(conv.convert("dot_productValue"), "Dot Productvalue")
136    /// ```
137    pub fn from_case(mut self, case: Case) -> Self {
138        self.boundaries = case.boundaries().to_vec();
139        self
140    }
141
142    /// Sets the boundaries to those provided.
143    /// ```
144    /// # use convert_case::{Boundary, Case, Converter};
145    /// let conv = Converter::new()
146    ///     .set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
147    ///     .to_case(Case::Lower);
148    /// assert_eq!(conv.convert("firstName_lastName"), "first name last name");
149    /// ```
150    pub fn set_boundaries(mut self, bs: &[Boundary]) -> Self {
151        self.boundaries = bs.to_vec();
152        self
153    }
154
155    /// Adds a boundary to the list of boundaries.
156    /// ```
157    /// # use convert_case::{Boundary, Case, Converter};
158    /// let conv = Converter::new()
159    ///     .from_case(Case::Title)
160    ///     .add_boundary(Boundary::Hyphen)
161    ///     .to_case(Case::Snake);
162    /// assert_eq!(conv.convert("My Biography - Video 1"), "my_biography___video_1")
163    /// ```
164    pub fn add_boundary(mut self, b: Boundary) -> Self {
165        self.boundaries.push(b);
166        self
167    }
168
169    /// Adds a vector of boundaries to the list of boundaries.
170    /// ```
171    /// # use convert_case::{Boundary, Case, Converter};
172    /// let conv = Converter::new()
173    ///     .from_case(Case::Kebab)
174    ///     .to_case(Case::Title)
175    ///     .add_boundaries(&[Boundary::Underscore, Boundary::LowerUpper]);
176    /// assert_eq!(conv.convert("2020-10_firstDay"), "2020 10 First Day");
177    /// ```
178    pub fn add_boundaries(mut self, bs: &[Boundary]) -> Self {
179        self.boundaries.extend(bs);
180        self
181    }
182
183    /// Removes a boundary from the list of boundaries if it exists.
184    ///
185    /// Note: [`Boundary::Custom`] variants are never considered equal due to
186    /// function pointer comparison limitations, so they cannot be removed using this method.
187    /// Recall that the default boundaries include no custom enumerations.
188    /// ```
189    /// # use convert_case::{Boundary, Case, Converter};
190    /// let conv = Converter::new()
191    ///     .remove_boundary(Boundary::Acronym)
192    ///     .to_case(Case::Kebab);
193    /// assert_eq!(conv.convert("HTTPRequest_parser"), "httprequest-parser");
194    /// ```
195    pub fn remove_boundary(mut self, b: Boundary) -> Self {
196        self.boundaries.retain(|&x| x != b);
197        self
198    }
199
200    /// Removes all the provided boundaries from the list of boundaries if it exists.
201    ///
202    /// Note: [`Boundary::Custom`] variants are never considered equal due to
203    /// function pointer comparison limitations, so they cannot be removed using this method.
204    /// Recall that the default boundaries include no custom enumerations.
205    /// ```
206    /// # use convert_case::{Boundary, Case, Converter};
207    /// let conv = Converter::new()
208    ///     .remove_boundaries(&Boundary::digits())
209    ///     .to_case(Case::Snake);
210    /// assert_eq!(conv.convert("C04 S03 Path Finding.pdf"), "c04_s03_path_finding.pdf");
211    /// ```
212    pub fn remove_boundaries(mut self, bs: &[Boundary]) -> Self {
213        for b in bs {
214            self.boundaries.retain(|&x| x != *b);
215        }
216        self
217    }
218
219    /// Sets a single pattern, replacing any existing patterns.
220    /// ```
221    /// # use convert_case::{Converter, Pattern};
222    /// let conv = Converter::new()
223    ///     .set_delimiter("_")
224    ///     .set_pattern(Pattern::Sentence);
225    /// assert_eq!(conv.convert("BJARNE CASE"), "Bjarne_case");
226    /// ```
227    pub fn set_pattern(mut self, p: Pattern) -> Self {
228        self.patterns = vec![p];
229        self
230    }
231
232    /// Sets the patterns to those provided, replacing any existing patterns.
233    /// An empty slice means no mutation (words pass through unchanged).
234    /// ```
235    /// # use convert_case::{Converter, Pattern};
236    /// let conv = Converter::new()
237    ///     .set_delimiter("_")
238    ///     .set_patterns(&[Pattern::Sentence]);
239    /// assert_eq!(conv.convert("BJARNE CASE"), "Bjarne_case");
240    /// ```
241    pub fn set_patterns(mut self, ps: &[Pattern]) -> Self {
242        self.patterns = ps.to_vec();
243        self
244    }
245
246    /// Adds a pattern to the end of the pattern list.
247    /// Patterns are applied in order, so this pattern will be applied last.
248    /// ```
249    /// # use convert_case::{Case, Converter, Pattern};
250    /// let conv = Converter::new()
251    ///     .from_case(Case::Kebab)
252    ///     .add_pattern(Pattern::RemoveEmpty)
253    ///     .add_pattern(Pattern::Camel);
254    /// assert_eq!(conv.convert("--leading-delims"), "leadingDelims");
255    /// ```
256    pub fn add_pattern(mut self, p: Pattern) -> Self {
257        self.patterns.push(p);
258        self
259    }
260
261    /// Adds multiple patterns to the end of the pattern list.
262    /// ```
263    /// # use convert_case::{Converter, Pattern};
264    /// let conv = Converter::new()
265    ///     .add_patterns(&[Pattern::RemoveEmpty, Pattern::Lowercase]);
266    /// ```
267    pub fn add_patterns(mut self, ps: &[Pattern]) -> Self {
268        self.patterns.extend(ps);
269        self
270    }
271
272    /// Removes a pattern from the list if it exists.
273    ///
274    /// Note: [`Pattern::Custom`] variants are never considered equal due to
275    /// function pointer comparison limitations, so they cannot be removed using this method.
276    /// ```
277    /// # use convert_case::{Boundary, Case, Converter, Pattern};
278    /// let conv = Converter::new()
279    ///     .set_boundaries(&[Boundary::Space])
280    ///     .to_case(Case::Snake)
281    ///     .remove_pattern(Pattern::Lowercase);
282    /// assert_eq!(conv.convert("HeLLo WoRLD"), "HeLLo_WoRLD");
283    /// ```
284    pub fn remove_pattern(mut self, p: Pattern) -> Self {
285        self.patterns.retain(|&x| x != p);
286        self
287    }
288
289    /// Removes all specified patterns from the list.
290    ///
291    /// Note: [`Pattern::Custom`] variants are never considered equal due to
292    /// function pointer comparison limitations, so they cannot be removed using this method.
293    /// ```
294    /// # use convert_case::{Converter, Pattern};
295    /// let conv = Converter::new()
296    ///     .set_patterns(&[Pattern::RemoveEmpty, Pattern::Lowercase, Pattern::Capital])
297    ///     .remove_patterns(&[Pattern::Lowercase, Pattern::Capital]);
298    /// // Only RemoveEmpty remains
299    /// ```
300    pub fn remove_patterns(mut self, ps: &[Pattern]) -> Self {
301        for p in ps {
302            self.patterns.retain(|&x| x != *p);
303        }
304        self
305    }
306
307    /// Sets the delimiter.
308    /// ```
309    /// # use convert_case::{Case, Converter};
310    /// let conv = Converter::new()
311    ///     .to_case(Case::Snake)
312    ///     .set_delimiter(".");
313    /// assert_eq!(conv.convert("LowerWithDots"), "lower.with.dots");
314    /// ```
315    pub fn set_delimiter<T>(mut self, d: T) -> Self
316    where
317        T: ToString,
318    {
319        self.delimiter = d.to_string();
320        self
321    }
322}
323
324#[cfg(test)]
325mod test {
326    use super::*;
327    use crate::Casing;
328
329    #[test]
330    fn snake_converter_from_case() {
331        let conv = Converter::new().to_case(Case::Snake);
332        let s = String::from("my var name");
333        assert_eq!(s.to_case(Case::Snake), conv.convert(s));
334    }
335
336    #[test]
337    fn snake_converter_from_scratch() {
338        let conv = Converter::new()
339            .set_delimiter("_")
340            .set_patterns(&[Pattern::Lowercase]);
341        let s = String::from("my var name");
342        assert_eq!(s.to_case(Case::Snake), conv.convert(s));
343    }
344
345    #[test]
346    fn custom_pattern() {
347        let conv = Converter::new()
348            .to_case(Case::Snake)
349            .set_patterns(&[Pattern::Sentence]);
350        assert_eq!(conv.convert("bjarne case"), "Bjarne_case");
351    }
352
353    #[test]
354    fn custom_delim() {
355        let conv = Converter::new().set_delimiter("..");
356        assert_eq!(conv.convert("ohMy"), "oh..My");
357    }
358
359    #[test]
360    fn no_delim() {
361        let conv = Converter::new()
362            .from_case(Case::Title)
363            .to_case(Case::Kebab)
364            .set_delimiter("");
365        assert_eq!(conv.convert("Just Flat"), "justflat");
366    }
367
368    #[test]
369    fn no_digit_boundaries() {
370        let conv = Converter::new()
371            .remove_boundaries(&Boundary::digits())
372            .to_case(Case::Snake);
373        assert_eq!(conv.convert("Test 08Bound"), "test_08bound");
374        assert_eq!(conv.convert("a8aA8A"), "a8a_a8a");
375    }
376
377    #[test]
378    fn remove_boundary() {
379        let conv = Converter::new()
380            .remove_boundary(Boundary::DigitUpper)
381            .to_case(Case::Snake);
382        assert_eq!(conv.convert("Test 08Bound"), "test_08bound");
383        assert_eq!(conv.convert("a8aA8A"), "a_8_a_a_8a");
384    }
385
386    #[test]
387    fn add_boundary() {
388        let conv = Converter::new()
389            .from_case(Case::Snake)
390            .to_case(Case::Kebab)
391            .add_boundary(Boundary::LowerUpper);
392        assert_eq!(conv.convert("word_wordWord"), "word-word-word");
393    }
394
395    #[test]
396    fn add_boundaries() {
397        let conv = Converter::new()
398            .from_case(Case::Snake)
399            .to_case(Case::Kebab)
400            .add_boundaries(&[Boundary::LowerUpper, Boundary::UpperLower]);
401        assert_eq!(conv.convert("word_wordWord"), "word-word-w-ord");
402    }
403
404    #[test]
405    fn twice() {
406        let s = "myVarName".to_string();
407        let conv = Converter::new().to_case(Case::Snake);
408        let snake = conv.convert(&s);
409        let kebab = s.to_case(Case::Kebab);
410        assert_eq!(snake.to_case(Case::Camel), kebab.to_case(Case::Camel));
411    }
412
413    #[test]
414    fn reuse_after_change() {
415        let conv = Converter::new().from_case(Case::Snake).to_case(Case::Kebab);
416        assert_eq!(conv.convert("word_wordWord"), "word-wordword");
417
418        let conv = conv.add_boundary(Boundary::LowerUpper);
419        assert_eq!(conv.convert("word_wordWord"), "word-word-word");
420    }
421
422    #[test]
423    fn explicit_boundaries() {
424        let conv = Converter::new()
425            .set_boundaries(&[
426                Boundary::DigitLower,
427                Boundary::DigitUpper,
428                Boundary::Acronym,
429            ])
430            .to_case(Case::Snake);
431        assert_eq!(
432            conv.convert("section8lesson2HTTPRequests"),
433            "section8_lesson2_http_requests"
434        );
435    }
436}