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}