1 | use crate::boundary; |
2 | use crate::boundary::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 | /// 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 | /// ``` |
56 | pub 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 | |
69 | impl 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 | |
79 | impl 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)] |
268 | mod 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 | } |
381 | |