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}
394