convert_case/
converter.rs

1use crate::boundary;
2use crate::boundary::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/// let dot_camel = Converter::new()
51///     .set_boundaries(&[Boundary::LOWER_UPPER, Boundary::LOWER_DIGIT])
52///     .set_pattern(Pattern::Camel)
53///     .set_delim(".");
54/// assert_eq!("collision.Shape.2d", dot_camel.convert("CollisionShape2D"));
55/// ```
56pub struct Converter {
57    /// How a string is segmented into words.
58    pub boundaries: Vec<Boundary>,
59
60    /// How each word is mutated before joining.  In the case that there is no pattern, none of the
61    /// words will be mutated before joining and will maintain whatever case they were in the
62    /// original string.
63    pub pattern: Option<Pattern>,
64
65    /// The string used to join mutated words together.
66    pub delim: String,
67}
68
69impl Default for Converter {
70    fn default() -> Self {
71        Converter {
72            boundaries: Boundary::defaults().to_vec(),
73            pattern: None,
74            delim: String::new(),
75        }
76    }
77}
78
79impl Converter {
80    /// Creates a new `Converter` with default fields.  This is the same as `Default::default()`.
81    /// The `Converter` will use `Boundary::defaults()` for boundaries, no pattern, and an empty
82    /// string as a delimeter.
83    /// ```
84    /// # use convert_case::Converter;
85    /// let conv = Converter::new();
86    /// assert_eq!("DeathPerennialQUEST", conv.convert("Death-Perennial QUEST"))
87    /// ```
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Converts a string.
93    /// ```
94    /// # use convert_case::{Case, Converter};
95    /// let conv = Converter::new()
96    ///     .to_case(Case::Camel);
97    /// assert_eq!("xmlHttpRequest", conv.convert("XML_HTTP_Request"))
98    /// ```
99    pub fn convert<T>(&self, s: T) -> String
100    where
101        T: AsRef<str>,
102    {
103        // TODO: if I change AsRef -> Borrow or ToString, fix here
104        let words = boundary::split(&s, &self.boundaries);
105        if let Some(p) = self.pattern {
106            let words = words.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
107            p.mutate(&words).join(&self.delim)
108        } else {
109            words.join(&self.delim)
110        }
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 = Some(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();
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 = Some(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 = None;
263        self
264    }
265}
266
267#[cfg(test)]
268mod test {
269    use super::*;
270    use crate::Casing;
271    use crate::Pattern;
272
273    #[test]
274    fn snake_converter_from_case() {
275        let conv = Converter::new().to_case(Case::Snake);
276        let s = String::from("my var name");
277        assert_eq!(s.to_case(Case::Snake), conv.convert(s));
278    }
279
280    #[test]
281    fn snake_converter_from_scratch() {
282        let conv = Converter::new()
283            .set_delim("_")
284            .set_pattern(Pattern::Lowercase);
285        let s = String::from("my var name");
286        assert_eq!(s.to_case(Case::Snake), conv.convert(s));
287    }
288
289    #[test]
290    fn custom_pattern() {
291        let conv = Converter::new()
292            .to_case(Case::Snake)
293            .set_pattern(Pattern::Sentence);
294        assert_eq!("Bjarne_case", conv.convert("bjarne case"));
295    }
296
297    #[test]
298    fn custom_delim() {
299        let conv = Converter::new().set_delim("..");
300        assert_eq!("oh..My", conv.convert("ohMy"));
301    }
302
303    #[test]
304    fn no_pattern() {
305        let conv = Converter::new()
306            .from_case(Case::Title)
307            .to_case(Case::Kebab)
308            .remove_pattern();
309        assert_eq!("wIErd-CASing", conv.convert("wIErd CASing"));
310    }
311
312    #[test]
313    fn no_delim() {
314        let conv = Converter::new()
315            .from_case(Case::Title)
316            .to_case(Case::Kebab)
317            .remove_delim();
318        assert_eq!("justflat", conv.convert("Just Flat"));
319    }
320
321    #[test]
322    fn no_digit_boundaries() {
323        let conv = Converter::new()
324            .remove_boundaries(&Boundary::digits())
325            .to_case(Case::Snake);
326        assert_eq!("test_08bound", conv.convert("Test 08Bound"));
327        assert_eq!("a8a_a8a", conv.convert("a8aA8A"));
328    }
329
330    #[test]
331    fn remove_boundary() {
332        let conv = Converter::new()
333            .remove_boundary(Boundary::DIGIT_UPPER)
334            .to_case(Case::Snake);
335        assert_eq!("test_08bound", conv.convert("Test 08Bound"));
336        assert_eq!("a_8_a_a_8a", conv.convert("a8aA8A"));
337    }
338
339    #[test]
340    fn add_boundary() {
341        let conv = Converter::new()
342            .from_case(Case::Snake)
343            .to_case(Case::Kebab)
344            .add_boundary(Boundary::LOWER_UPPER);
345        assert_eq!("word-word-word", conv.convert("word_wordWord"));
346    }
347
348    #[test]
349    fn add_boundaries() {
350        let conv = Converter::new()
351            .from_case(Case::Snake)
352            .to_case(Case::Kebab)
353            .add_boundaries(&[Boundary::LOWER_UPPER, Boundary::UPPER_LOWER]);
354        assert_eq!("word-word-w-ord", conv.convert("word_wordWord"));
355    }
356
357    #[test]
358    fn reuse_after_change() {
359        let conv = Converter::new().from_case(Case::Snake).to_case(Case::Kebab);
360        assert_eq!("word-wordword", conv.convert("word_wordWord"));
361
362        let conv = conv.add_boundary(Boundary::LOWER_UPPER);
363        assert_eq!("word-word-word", conv.convert("word_wordWord"));
364    }
365
366    #[test]
367    fn explicit_boundaries() {
368        let conv = Converter::new()
369            .set_boundaries(&[
370                Boundary::DIGIT_LOWER,
371                Boundary::DIGIT_UPPER,
372                Boundary::ACRONYM,
373            ])
374            .to_case(Case::Snake);
375        assert_eq!(
376            "section8_lesson2_http_requests",
377            conv.convert("section8lesson2HTTPRequests")
378        );
379    }
380}