convert_case/
converter.rs

1use crate::boundary;
2use crate::boundary::Boundary;
3use crate::pattern;
4use crate::pattern::Pattern;
5use crate::Case;
6
7use alloc::string::{String, ToString};
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 segmented into _words_.
14/// 2) `pattern`: how words are mutated, or how each character's case will change.
15/// 3) `delim` or delimeter: 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///     "dialoguebox_border_shadow",
32///     s.from_case(Case::Kebab).to_case(Case::Snake)
33/// );
34///
35/// // Convert using similar functions on Converter
36/// let conv = Converter::new()
37///     .from_case(Case::Kebab)
38///     .to_case(Case::Snake);
39/// assert_eq!("dialoguebox_border_shadow", conv.convert(s));
40///
41/// // Convert by setting each field explicitly.
42/// let conv = Converter::new()
43///     .set_boundaries(&[Boundary::HYPHEN])
44///     .set_pattern(pattern::lowercase)
45///     .set_delim("_");
46/// assert_eq!("dialoguebox_border_shadow", conv.convert(s));
47/// ```
48///
49/// Or you can use `Converter` when you are trying to make a unique case
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::LOWER_UPPER, Boundary::LOWER_DIGIT])
56///     .set_pattern(pattern::camel)
57///     .set_delim(".");
58/// assert_eq!("collision.Shape.2d", dot_camel.convert("CollisionShape2D"));
59/// ```
60pub struct Converter {
61    /// How a string is segmented into words.
62    pub boundaries: Vec<Boundary>,
63
64    /// How each word is mutated before joining.  In the case that there is no pattern, none of the
65    /// words will be mutated before joining and will maintain whatever case they were in the
66    /// original string.
67    pub pattern: Pattern,
68
69    /// The string used to join mutated words together.
70    pub delim: String,
71}
72
73impl Default for Converter {
74    fn default() -> Self {
75        Converter {
76            boundaries: Boundary::defaults().to_vec(),
77            pattern: pattern::noop,
78            delim: String::new(),
79        }
80    }
81}
82
83impl Converter {
84    /// Creates a new `Converter` with default fields.  This is the same as `Default::default()`.
85    /// The `Converter` will use `Boundary::defaults()` for boundaries, no pattern, and an empty
86    /// string as a delimeter.
87    /// ```
88    /// # use convert_case::Converter;
89    /// let conv = Converter::new();
90    /// assert_eq!("DeathPerennialQUEST", conv.convert("Death-Perennial QUEST"))
91    /// ```
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Converts a string.
97    /// ```
98    /// # use convert_case::{Case, Converter};
99    /// let conv = Converter::new()
100    ///     .to_case(Case::Camel);
101    /// assert_eq!("xmlHttpRequest", conv.convert("XML_HTTP_Request"))
102    /// ```
103    pub fn convert<T>(&self, s: T) -> String
104    where
105        T: AsRef<str>,
106    {
107        // TODO: if I change AsRef -> Borrow or ToString, fix here
108        let words = boundary::split(&s, &self.boundaries);
109        let words = words.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
110        (self.pattern)(&words).join(&self.delim)
111    }
112
113    /// Set the pattern and delimiter to those associated with the given case.
114    /// ```
115    /// # use convert_case::{Case, Converter};
116    /// let conv = Converter::new()
117    ///     .to_case(Case::Pascal);
118    /// assert_eq!("VariableName", conv.convert("variable name"))
119    /// ```
120    pub fn to_case(mut self, case: Case) -> Self {
121        self.pattern = case.pattern();
122        self.delim = case.delim().to_string();
123        self
124    }
125
126    /// Sets the boundaries to those associated with the provided case.  This is used
127    /// by the `from_case` function in the `Casing` trait.
128    /// ```
129    /// # use convert_case::{Case, Converter};
130    /// let conv = Converter::new()
131    ///     .from_case(Case::Snake)
132    ///     .to_case(Case::Title);
133    /// assert_eq!("Dot Productvalue", conv.convert("dot_productValue"))
134    /// ```
135    pub fn from_case(mut self, case: Case) -> Self {
136        self.boundaries = case.boundaries().to_vec();
137        self
138    }
139
140    /// Sets the boundaries to those provided.
141    /// ```
142    /// # use convert_case::{Boundary, Case, Converter};
143    /// let conv = Converter::new()
144    ///     .set_boundaries(&[Boundary::UNDERSCORE, Boundary::LOWER_UPPER])
145    ///     .to_case(Case::Lower);
146    /// assert_eq!("panic attack dream theater", conv.convert("panicAttack_dreamTheater"))
147    /// ```
148    pub fn set_boundaries(mut self, bs: &[Boundary]) -> Self {
149        self.boundaries = bs.to_vec();
150        self
151    }
152
153    /// Adds a boundary to the list of boundaries.
154    /// ```
155    /// # use convert_case::{Boundary, Case, Converter};
156    /// let conv = Converter::new()
157    ///     .from_case(Case::Title)
158    ///     .add_boundary(Boundary::HYPHEN)
159    ///     .to_case(Case::Snake);
160    /// assert_eq!("my_biography_video_1", conv.convert("My Biography - Video 1"))
161    /// ```
162    pub fn add_boundary(mut self, b: Boundary) -> Self {
163        self.boundaries.push(b);
164        self
165    }
166
167    /// Adds a vector of boundaries to the list of boundaries.
168    /// ```
169    /// # use convert_case::{Boundary, Case, Converter};
170    /// let conv = Converter::new()
171    ///     .from_case(Case::Kebab)
172    ///     .to_case(Case::Title)
173    ///     .add_boundaries(&[Boundary::UNDERSCORE, Boundary::LOWER_UPPER]);
174    /// assert_eq!("2020 10 First Day", conv.convert("2020-10_firstDay"));
175    /// ```
176    pub fn add_boundaries(mut self, bs: &[Boundary]) -> Self {
177        self.boundaries.extend(bs);
178        self
179    }
180
181    /// Removes a boundary from the list of boundaries if it exists.
182    /// ```
183    /// # use convert_case::{Boundary, Case, Converter};
184    /// let conv = Converter::new()
185    ///     .remove_boundary(Boundary::ACRONYM)
186    ///     .to_case(Case::Kebab);
187    /// assert_eq!("httprequest-parser", conv.convert("HTTPRequest_parser"));
188    /// ```
189    pub fn remove_boundary(mut self, b: Boundary) -> Self {
190        self.boundaries.retain(|&x| x != b);
191        self
192    }
193
194    /// Removes all the provided boundaries from the list of boundaries if it exists.
195    /// ```
196    /// # use convert_case::{Boundary, Case, Converter};
197    /// let conv = Converter::new()
198    ///     .remove_boundaries(&Boundary::digits())
199    ///     .to_case(Case::Snake);
200    /// assert_eq!("c04_s03_path_finding.pdf", conv.convert("C04 S03 Path Finding.pdf"));
201    /// ```
202    pub fn remove_boundaries(mut self, bs: &[Boundary]) -> Self {
203        for b in bs {
204            self.boundaries.retain(|&x| x != *b);
205        }
206        self
207    }
208
209    /// Sets the delimeter.
210    /// ```
211    /// # use convert_case::{Case, Converter};
212    /// let conv = Converter::new()
213    ///     .to_case(Case::Snake)
214    ///     .set_delim(".");
215    /// assert_eq!("lower.with.dots", conv.convert("LowerWithDots"));
216    /// ```
217    pub fn set_delim<T>(mut self, d: T) -> Self
218    where
219        T: ToString,
220    {
221        self.delim = d.to_string();
222        self
223    }
224
225    /// Sets the delimeter to an empty string.
226    /// ```
227    /// # use convert_case::{Case, Converter};
228    /// let conv = Converter::new()
229    ///     .to_case(Case::Snake)
230    ///     .remove_delim();
231    /// assert_eq!("nodelimshere", conv.convert("No Delims Here"));
232    /// ```
233    pub fn remove_delim(mut self) -> Self {
234        self.delim = String::new();
235        self
236    }
237
238    /// Sets the pattern.
239    /// ```
240    /// # use convert_case::{Case, Converter, pattern};
241    /// let conv = Converter::new()
242    ///     .set_delim("_")
243    ///     .set_pattern(pattern::sentence);
244    /// assert_eq!("Bjarne_case", conv.convert("BJARNE CASE"));
245    /// ```
246    pub fn set_pattern(mut self, p: Pattern) -> Self {
247        self.pattern = p;
248        self
249    }
250
251    /// Sets the pattern field to `None`.  Where there is no pattern, a character's case is never
252    /// mutated and will be maintained at the end of conversion.
253    /// ```
254    /// # use convert_case::{Case, Converter};
255    /// let conv = Converter::new()
256    ///     .from_case(Case::Title)
257    ///     .to_case(Case::Snake)
258    ///     .remove_pattern();
259    /// assert_eq!("KoRn_Alone_I_Break", conv.convert("KoRn Alone I Break"));
260    /// ```
261    pub fn remove_pattern(mut self) -> Self {
262        self.pattern = pattern::noop;
263        self
264    }
265}
266
267#[cfg(test)]
268mod test {
269    use super::*;
270    use crate::Casing;
271
272    #[test]
273    fn snake_converter_from_case() {
274        let conv = Converter::new().to_case(Case::Snake);
275        let s = String::from("my var name");
276        assert_eq!(s.to_case(Case::Snake), conv.convert(s));
277    }
278
279    #[test]
280    fn snake_converter_from_scratch() {
281        let conv = Converter::new()
282            .set_delim("_")
283            .set_pattern(pattern::lowercase);
284        let s = String::from("my var name");
285        assert_eq!(s.to_case(Case::Snake), conv.convert(s));
286    }
287
288    #[test]
289    fn custom_pattern() {
290        let conv = Converter::new()
291            .to_case(Case::Snake)
292            .set_pattern(pattern::sentence);
293        assert_eq!("Bjarne_case", conv.convert("bjarne case"));
294    }
295
296    #[test]
297    fn custom_delim() {
298        let conv = Converter::new().set_delim("..");
299        assert_eq!("oh..My", conv.convert("ohMy"));
300    }
301
302    #[test]
303    fn no_pattern() {
304        let conv = Converter::new()
305            .from_case(Case::Title)
306            .to_case(Case::Kebab)
307            .remove_pattern();
308        assert_eq!("wIErd-CASing", conv.convert("wIErd CASing"));
309    }
310
311    #[test]
312    fn no_delim() {
313        let conv = Converter::new()
314            .from_case(Case::Title)
315            .to_case(Case::Kebab)
316            .remove_delim();
317        assert_eq!("justflat", conv.convert("Just Flat"));
318    }
319
320    #[test]
321    fn no_digit_boundaries() {
322        let conv = Converter::new()
323            .remove_boundaries(&Boundary::digits())
324            .to_case(Case::Snake);
325        assert_eq!("test_08bound", conv.convert("Test 08Bound"));
326        assert_eq!("a8a_a8a", conv.convert("a8aA8A"));
327    }
328
329    #[test]
330    fn remove_boundary() {
331        let conv = Converter::new()
332            .remove_boundary(Boundary::DIGIT_UPPER)
333            .to_case(Case::Snake);
334        assert_eq!("test_08bound", conv.convert("Test 08Bound"));
335        assert_eq!("a_8_a_a_8a", conv.convert("a8aA8A"));
336    }
337
338    #[test]
339    fn add_boundary() {
340        let conv = Converter::new()
341            .from_case(Case::Snake)
342            .to_case(Case::Kebab)
343            .add_boundary(Boundary::LOWER_UPPER);
344        assert_eq!("word-word-word", conv.convert("word_wordWord"));
345    }
346
347    #[test]
348    fn add_boundaries() {
349        let conv = Converter::new()
350            .from_case(Case::Snake)
351            .to_case(Case::Kebab)
352            .add_boundaries(&[Boundary::LOWER_UPPER, Boundary::UPPER_LOWER]);
353        assert_eq!("word-word-w-ord", conv.convert("word_wordWord"));
354    }
355
356    #[test]
357    fn reuse_after_change() {
358        let conv = Converter::new().from_case(Case::Snake).to_case(Case::Kebab);
359        assert_eq!("word-wordword", conv.convert("word_wordWord"));
360
361        let conv = conv.add_boundary(Boundary::LOWER_UPPER);
362        assert_eq!("word-word-word", conv.convert("word_wordWord"));
363    }
364
365    #[test]
366    fn explicit_boundaries() {
367        let conv = Converter::new()
368            .set_boundaries(&[
369                Boundary::DIGIT_LOWER,
370                Boundary::DIGIT_UPPER,
371                Boundary::ACRONYM,
372            ])
373            .to_case(Case::Snake);
374        assert_eq!(
375            "section8_lesson2_http_requests",
376            conv.convert("section8lesson2HTTPRequests")
377        );
378    }
379}