convert_case/
converter.rs

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