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}