1 | use crate::segmentation; |
2 | use crate::Boundary; |
3 | use crate::Case; |
4 | use 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 | /// ``` |
57 | pub 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 | |
70 | impl Default for Converter { |
71 | fn default() -> Self { |
72 | Converter { |
73 | boundaries: Boundary::defaults(), |
74 | pattern: None, |
75 | delim: String::new(), |
76 | } |
77 | } |
78 | } |
79 | |
80 | impl 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)] |
281 | mod 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 | |