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::Vec;
8
9/// The parameters for performing a case conversion.
10///
11/// A `Converter` stores three fields needed for case conversion.
12/// 1) `boundaries`: how a string is segmented into _words_.
13/// 2) `pattern`: how words are mutated, or how each character's case will change.
14/// 3) `delim` or delimeter: how the mutated words are joined into the final string.
15///
16/// Then calling [`convert`](Converter::convert) on a `Converter` will apply a case conversion
17/// defined by those fields. The `Converter` struct is what is used underneath those functions
18/// available in the `Casing` struct.
19///
20/// You can use `Converter` when you need more specificity on conversion
21/// than those provided in `Casing`, or if it is simply more convenient or explicit.
22///
23/// ```
24/// use convert_case::{Boundary, Case, Casing, Converter, Pattern};
25///
26/// let s = "DialogueBox-border-shadow";
27///
28/// // Convert using Casing trait
29/// assert_eq!(
30/// s.from_case(Case::Kebab).to_case(Case::Snake),
31/// "dialoguebox_border_shadow",
32/// );
33///
34/// // Convert using similar functions on Converter
35/// let conv = Converter::new()
36/// .from_case(Case::Kebab)
37/// .to_case(Case::Snake);
38/// assert_eq!(conv.convert(s), "dialoguebox_border_shadow");
39///
40/// // Convert by setting each field explicitly.
41/// let conv = Converter::new()
42/// .set_boundaries(&[Boundary::Hyphen])
43/// .set_pattern(Pattern::Lowercase)
44/// .set_delim("_");
45/// assert_eq!(conv.convert(s), "dialoguebox_border_shadow");
46/// ```
47///
48/// Or you can use `Converter` when you are trying to make a unique case
49/// not provided as a variant of `Case`.
50///
51/// ```
52/// # use convert_case::{Boundary, Case, Casing, Converter, Pattern};
53/// let dot_camel = Converter::new()
54/// .set_boundaries(&[Boundary::LowerUpper, Boundary::LowerDigit])
55/// .set_pattern(Pattern::Camel)
56/// .set_delim(".");
57/// assert_eq!(dot_camel.convert("CollisionShape2D"), "collision.Shape.2d");
58/// ```
59pub struct Converter {
60 /// How a string is segmented into words.
61 pub boundaries: Vec<Boundary>,
62
63 /// How each word is mutated before joining. In the case that there is no pattern, none of the
64 /// words will be mutated before joining and will maintain whatever case they were in the
65 /// original string.
66 pub pattern: Pattern,
67
68 /// The string used to join mutated words together.
69 pub delim: String,
70}
71
72impl Default for Converter {
73 fn default() -> Self {
74 Converter {
75 boundaries: Boundary::defaults().to_vec(),
76 pattern: Pattern::Noop,
77 delim: String::new(),
78 }
79 }
80}
81
82impl Converter {
83 /// Creates a new `Converter` with default fields. This is the same as `Default::default()`.
84 /// The `Converter` will use `Boundary::defaults()` for boundaries, no pattern, and an empty
85 /// string as a delimeter.
86 /// ```
87 /// # use convert_case::Converter;
88 /// let conv = Converter::new();
89 /// assert_eq!(conv.convert("Ice-cream TRUCK"), "IcecreamTRUCK")
90 /// ```
91 pub fn new() -> Self {
92 Self::default()
93 }
94
95 /// Converts a string.
96 /// ```
97 /// # use convert_case::{Case, Converter};
98 /// let conv = Converter::new()
99 /// .to_case(Case::Camel);
100 /// assert_eq!(conv.convert("XML_HTTP_Request"), "xmlHttpRequest")
101 /// ```
102 pub fn convert<T>(&self, s: T) -> String
103 where
104 T: AsRef<str>,
105 {
106 // TODO: if I change AsRef -> Borrow or ToString, fix here
107 let words = boundary::split(&s, &self.boundaries);
108 let words = words.to_vec();
109 self.pattern.mutate(&words).join(&self.delim)
110 }
111
112 /// Set the pattern and delimiter to those associated with the given case.
113 /// ```
114 /// # use convert_case::{Case, Converter};
115 /// let conv = Converter::new()
116 /// .to_case(Case::Pascal);
117 /// assert_eq!("VariableName", conv.convert("variable name"))
118 /// ```
119 pub fn to_case(mut self, case: Case) -> Self {
120 self.pattern = case.pattern();
121 self.delim = case.delim().to_string();
122 self
123 }
124
125 /// Sets the boundaries to those associated with the provided case. This is used
126 /// by the `from_case` function in the `Casing` trait.
127 /// ```
128 /// # use convert_case::{Case, Converter};
129 /// let conv = Converter::new()
130 /// .from_case(Case::Snake)
131 /// .to_case(Case::Title);
132 /// assert_eq!("Dot Productvalue", conv.convert("dot_productValue"))
133 /// ```
134 pub fn from_case(mut self, case: Case) -> Self {
135 self.boundaries = case.boundaries().to_vec();
136 self
137 }
138
139 /// Sets the boundaries to those provided.
140 /// ```
141 /// # use convert_case::{Boundary, Case, Converter};
142 /// let conv = Converter::new()
143 /// .set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
144 /// .to_case(Case::Lower);
145 /// assert_eq!("panic attack dream theater", conv.convert("panicAttack_dreamTheater"))
146 /// ```
147 pub fn set_boundaries(mut self, bs: &[Boundary]) -> Self {
148 self.boundaries = bs.to_vec();
149 self
150 }
151
152 /// Adds a boundary to the list of boundaries.
153 /// ```
154 /// # use convert_case::{Boundary, Case, Converter};
155 /// let conv = Converter::new()
156 /// .from_case(Case::Title)
157 /// .add_boundary(Boundary::Hyphen)
158 /// .to_case(Case::Snake);
159 /// assert_eq!("my_biography___video_1", conv.convert("My Biography - Video 1"))
160 /// ```
161 pub fn add_boundary(mut self, b: Boundary) -> Self {
162 self.boundaries.push(b);
163 self
164 }
165
166 /// Adds a vector of boundaries to the list of boundaries.
167 /// ```
168 /// # use convert_case::{Boundary, Case, Converter};
169 /// let conv = Converter::new()
170 /// .from_case(Case::Kebab)
171 /// .to_case(Case::Title)
172 /// .add_boundaries(&[Boundary::Underscore, Boundary::LowerUpper]);
173 /// assert_eq!("2020 10 First Day", conv.convert("2020-10_firstDay"));
174 /// ```
175 pub fn add_boundaries(mut self, bs: &[Boundary]) -> Self {
176 self.boundaries.extend(bs);
177 self
178 }
179
180 /// Removes a boundary from the list of boundaries if it exists.
181 /// ```
182 /// # use convert_case::{Boundary, Case, Converter};
183 /// let conv = Converter::new()
184 /// .remove_boundary(Boundary::Acronym)
185 /// .to_case(Case::Kebab);
186 /// assert_eq!("httprequest-parser", conv.convert("HTTPRequest_parser"));
187 /// ```
188 pub fn remove_boundary(mut self, b: Boundary) -> Self {
189 self.boundaries.retain(|&x| x != b);
190 self
191 }
192
193 /// Removes all the provided boundaries from the list of boundaries if it exists.
194 /// ```
195 /// # use convert_case::{Boundary, Case, Converter};
196 /// let conv = Converter::new()
197 /// .remove_boundaries(&Boundary::digits())
198 /// .to_case(Case::Snake);
199 /// assert_eq!("c04_s03_path_finding.pdf", conv.convert("C04 S03 Path Finding.pdf"));
200 /// ```
201 pub fn remove_boundaries(mut self, bs: &[Boundary]) -> Self {
202 for b in bs {
203 self.boundaries.retain(|&x| x != *b);
204 }
205 self
206 }
207
208 /// Sets the delimeter.
209 /// ```
210 /// # use convert_case::{Case, Converter};
211 /// let conv = Converter::new()
212 /// .to_case(Case::Snake)
213 /// .set_delim(".");
214 /// assert_eq!("lower.with.dots", conv.convert("LowerWithDots"));
215 /// ```
216 pub fn set_delim<T>(mut self, d: T) -> Self
217 where
218 T: ToString,
219 {
220 self.delim = d.to_string();
221 self
222 }
223
224 /// Sets the pattern.
225 /// ```
226 /// # use convert_case::{Case, Converter, Pattern};
227 /// let conv = Converter::new()
228 /// .set_delim("_")
229 /// .set_pattern(Pattern::Sentence);
230 /// assert_eq!("Bjarne_case", conv.convert("BJARNE CASE"));
231 /// ```
232 pub fn set_pattern(mut self, p: Pattern) -> Self {
233 self.pattern = p;
234 self
235 }
236}
237
238#[cfg(test)]
239mod test {
240 use super::*;
241 use crate::Casing;
242
243 #[test]
244 fn snake_converter_from_case() {
245 let conv = Converter::new().to_case(Case::Snake);
246 let s = String::from("my var name");
247 assert_eq!(s.to_case(Case::Snake), conv.convert(s));
248 }
249
250 #[test]
251 fn snake_converter_from_scratch() {
252 let conv = Converter::new()
253 .set_delim("_")
254 .set_pattern(Pattern::Lowercase);
255 let s = String::from("my var name");
256 assert_eq!(s.to_case(Case::Snake), conv.convert(s));
257 }
258
259 #[test]
260 fn custom_pattern() {
261 let conv = Converter::new()
262 .to_case(Case::Snake)
263 .set_pattern(Pattern::Sentence);
264 assert_eq!("Bjarne_case", conv.convert("bjarne case"));
265 }
266
267 #[test]
268 fn custom_delim() {
269 let conv = Converter::new().set_delim("..");
270 assert_eq!("oh..My", conv.convert("ohMy"));
271 }
272
273 #[test]
274 fn no_delim() {
275 let conv = Converter::new()
276 .from_case(Case::Title)
277 .to_case(Case::Kebab)
278 .set_delim("");
279 assert_eq!("justflat", conv.convert("Just Flat"));
280 }
281
282 #[test]
283 fn no_digit_boundaries() {
284 let conv = Converter::new()
285 .remove_boundaries(&Boundary::digits())
286 .to_case(Case::Snake);
287 assert_eq!("test_08bound", conv.convert("Test 08Bound"));
288 assert_eq!("a8a_a8a", conv.convert("a8aA8A"));
289 }
290
291 #[test]
292 fn remove_boundary() {
293 let conv = Converter::new()
294 .remove_boundary(Boundary::DigitUpper)
295 .to_case(Case::Snake);
296 assert_eq!("test_08bound", conv.convert("Test 08Bound"));
297 assert_eq!("a_8_a_a_8a", conv.convert("a8aA8A"));
298 }
299
300 #[test]
301 fn add_boundary() {
302 let conv = Converter::new()
303 .from_case(Case::Snake)
304 .to_case(Case::Kebab)
305 .add_boundary(Boundary::LowerUpper);
306 assert_eq!("word-word-word", conv.convert("word_wordWord"));
307 }
308
309 #[test]
310 fn add_boundaries() {
311 let conv = Converter::new()
312 .from_case(Case::Snake)
313 .to_case(Case::Kebab)
314 .add_boundaries(&[Boundary::LowerUpper, Boundary::UpperLower]);
315 assert_eq!("word-word-w-ord", conv.convert("word_wordWord"));
316 }
317
318 #[test]
319 fn twice() {
320 let s = "myVarName".to_string();
321 let conv = Converter::new().to_case(Case::Snake);
322 let snake = conv.convert(&s);
323 let kebab = s.to_case(Case::Kebab);
324 assert_eq!(snake.to_case(Case::Camel), kebab.to_case(Case::Camel));
325 }
326
327 #[test]
328 fn reuse_after_change() {
329 let conv = Converter::new().from_case(Case::Snake).to_case(Case::Kebab);
330 assert_eq!("word-wordword", conv.convert("word_wordWord"));
331
332 let conv = conv.add_boundary(Boundary::LowerUpper);
333 assert_eq!("word-word-word", conv.convert("word_wordWord"));
334 }
335
336 #[test]
337 fn explicit_boundaries() {
338 let conv = Converter::new()
339 .set_boundaries(&[
340 Boundary::DigitLower,
341 Boundary::DigitUpper,
342 Boundary::Acronym,
343 ])
344 .to_case(Case::Snake);
345 assert_eq!(
346 "section8_lesson2_http_requests",
347 conv.convert("section8lesson2HTTPRequests")
348 );
349 }
350}