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