1use regex::Regex;
2
3pub trait StringExt {
4 fn is_empty_or_whitespaces(&self) -> bool;
5 fn split_get_first(&self, splitter: Option<String>) -> String;
6 fn split_get_last(&self, splitter: Option<String>) -> String;
7 fn get_first_char(&self) -> Option<char>;
8
9 fn trim_char(&self, trimmer: Option<String>) -> String;
10 fn trim_start_char(&self, trimmer: Option<String>) -> String;
11 fn trim_end_char(&self, trimmer: Option<String>) -> String;
12
13 fn uppercase_first_letter(&self) -> String;
14 fn lowercase_first_letter(&self) -> String;
15 fn pascal_case(&self) -> String;
16 fn camel_case(&self) -> String;
17 fn snake_case(&self) -> String;
18
19 fn on_one_line(&self, indent: Option<u64>, line_break: Option<bool>) -> String;
20
21 fn regex_extract(&self, regex_extractor: &str, regex_replacer: Option<&str>, separator: Option<&str>) -> Result<String, anyhow::Error>;
22 fn regex_transform(&self, regex_pattern: &str, regex_replacer: &str) -> Result<String, anyhow::Error>;
23}
24
25impl StringExt for Option<String> {
28 fn is_empty_or_whitespaces(&self) -> bool {
29 self.as_ref().map_or(true, |s| s.is_empty_or_whitespaces())
30 }
31
32 fn split_get_first(&self, splitter: Option<String>) -> String {
33 self.as_ref().map_or(Default::default(), |s| s.split_get_first(splitter))
34 }
35
36 fn split_get_last(&self, splitter: Option<String>) -> String {
37 self.as_ref().map_or(Default::default(), |s| s.split_get_last(splitter))
38 }
39
40 fn get_first_char(&self) -> Option<char> {
41 self.as_ref().and_then(|s| s.get_first_char())
42 }
43
44 fn trim_char(&self, trimmer: Option<String>) -> String {
45 self.as_ref().map_or(Default::default(), |s| s.trim_char(trimmer))
46 }
47
48 fn trim_start_char(&self, trimmer: Option<String>) -> String {
49 self.as_ref().map_or(Default::default(), |s| s.trim_start_char(trimmer))
50 }
51
52 fn trim_end_char(&self, trimmer: Option<String>) -> String {
53 self.as_ref().map_or(Default::default(), |s| s.trim_end_char(trimmer))
54 }
55
56 fn uppercase_first_letter(&self) -> String {
57 self.as_ref().map_or(Default::default(), |s| s.uppercase_first_letter())
58 }
59
60 fn lowercase_first_letter(&self) -> String {
61 self.as_ref().map_or(Default::default(), |s| s.lowercase_first_letter())
62 }
63
64 fn pascal_case(&self) -> String {
65 self.as_ref().map_or(Default::default(), |s| s.pascal_case())
66 }
67
68 fn camel_case(&self) -> String {
69 self.as_ref().map_or(Default::default(), |s| s.camel_case())
70 }
71
72 fn snake_case(&self) -> String {
73 self.as_ref().map_or(Default::default(), |s| s.snake_case())
74 }
75
76 fn on_one_line(&self, indent: Option<u64>, line_break: Option<bool>) -> String {
77 self.as_ref().map_or(Default::default(), |s| s.on_one_line(indent, line_break))
78 }
79
80 fn regex_extract(&self, regex_extractor: &str, regex_replacer: Option<&str>, separator: Option<&str>) -> Result<String, anyhow::Error> {
81 self
82 .as_ref()
83 .map(|s| s.regex_extract(regex_extractor, regex_replacer, separator))
84 .transpose()
85 .map(|s| s.unwrap_or_default())
86 }
87 fn regex_transform(&self, regex_pattern: &str, regex_replacer: &str) -> Result<String, anyhow::Error> {
88 self
89 .as_ref()
90 .map(|s| s.regex_transform(regex_pattern, regex_replacer))
91 .transpose()
92 .map(|s| s.unwrap_or_default())
93 }
94}
95
96impl StringExt for String {
97 fn is_empty_or_whitespaces(&self) -> bool {
98 self.as_str().is_empty_or_whitespaces()
99 }
100
101 fn split_get_first(&self, splitter: Option<String>) -> String {
102 self.as_str().split_get_first(splitter)
103 }
104
105 fn split_get_last(&self, splitter: Option<String>) -> String {
106 self.as_str().split_get_last(splitter)
107 }
108
109 fn get_first_char(&self) -> Option<char> {
110 self.as_str().get_first_char()
111 }
112
113 fn trim_char(&self, trimmer: Option<String>) -> String {
114 self.as_str().trim_char(trimmer)
115 }
116
117 fn trim_start_char(&self, trimmer: Option<String>) -> String {
118 self.as_str().trim_start_char(trimmer)
119 }
120
121 fn trim_end_char(&self, trimmer: Option<String>) -> String {
122 self.as_str().trim_end_char(trimmer)
123 }
124
125 fn uppercase_first_letter(&self) -> String {
126 self.as_str().uppercase_first_letter()
127 }
128
129 fn lowercase_first_letter(&self) -> String {
130 self.as_str().lowercase_first_letter()
131 }
132
133 fn pascal_case(&self) -> String {
134 self.as_str().pascal_case()
135 }
136
137 fn camel_case(&self) -> String {
138 self.as_str().camel_case()
139 }
140
141 fn snake_case(&self) -> String {
142 self.as_str().snake_case()
143 }
144
145 fn on_one_line(&self, indent: Option<u64>, line_break: Option<bool>) -> String {
146 self.as_str().on_one_line(indent, line_break)
147 }
148
149 fn regex_extract(&self, regex_extractor: &str, regex_replacer: Option<&str>, separator: Option<&str>) -> Result<String, anyhow::Error> {
150 self.as_str().regex_extract(regex_extractor, regex_replacer, separator)
151 }
152 fn regex_transform(&self, regex_pattern: &str, regex_replacer: &str) -> Result<String, anyhow::Error> {
153 self.as_str().regex_transform(regex_pattern, regex_replacer)
154 }
155}
156
157impl StringExt for &str {
158 fn is_empty_or_whitespaces(&self) -> bool {
159 self.is_empty() || self.trim().is_empty()
160 }
161
162 fn split_get_first(&self, splitter: Option<String>) -> String {
163 let c = splitter.get_first_char().unwrap_or('/');
164 self.split(c).find(|s| !s.is_empty_or_whitespaces()).unwrap_or_default().to_string()
165 }
166
167 fn split_get_last(&self, splitter: Option<String>) -> String {
168 let c = splitter.get_first_char().unwrap_or('/');
169 self
170 .split(c)
171 .filter(|s| !s.is_empty_or_whitespaces())
172 .last()
173 .unwrap_or_default()
174 .to_string()
175 }
176
177 fn get_first_char(&self) -> Option<char> {
178 self.chars().next()
179 }
180
181 fn trim_char(&self, trimmer: Option<String>) -> String {
182 let trimmer = trimmer.unwrap_or_else(|| " ".to_string()).chars().next().unwrap_or(' ');
183 self.trim_matches(trimmer).to_string()
184 }
185
186 fn trim_start_char(&self, trimmer: Option<String>) -> String {
187 let trimmer = trimmer.unwrap_or_else(|| " ".to_string()).chars().next().unwrap_or(' ');
188 self.trim_start_matches(trimmer).to_string()
189 }
190
191 fn trim_end_char(&self, trimmer: Option<String>) -> String {
192 let trimmer = trimmer.unwrap_or_else(|| " ".to_string()).chars().next().unwrap_or(' ');
193 self.trim_end_matches(trimmer).to_string()
194 }
195
196 fn uppercase_first_letter(&self) -> String {
197 if self.is_empty_or_whitespaces() {
198 return String::default();
199 }
200 let mut ve: Vec<char> = self.chars().collect();
201 ve[0] = ve[0].to_uppercase().next().unwrap();
202 let result: String = ve.into_iter().collect();
203 result
204 }
205
206 fn lowercase_first_letter(&self) -> String {
207 if self.is_empty_or_whitespaces() {
208 return String::default();
209 }
210 let mut ve: Vec<char> = self.chars().collect();
211 ve[0] = ve[0].to_lowercase().next().unwrap();
212 let result: String = ve.into_iter().collect();
213 result
214 }
215
216 fn pascal_case(&self) -> String {
217 let (_, pascal) = self.trim().chars().fold((Some(true), String::with_capacity(self.len())), |acc, x| {
218 let (upper_next, mut s) = acc;
219 if is_out_of_case(x) {
220 return (Some(true), s);
221 }
222 match upper_next {
223 Some(true) => s.push(x.to_uppercase().next().unwrap_or(x)),
224 _ => s.push(x),
225 }
226 (Some(false), s)
227 });
228 pascal
229 }
230
231 fn camel_case(&self) -> String {
232 let (_, camel) = self
233 .trim()
234 .chars()
235 .fold((Some(false), String::with_capacity(self.len())), |acc, x| {
236 let (upper_next, mut s) = acc;
237 if is_out_of_case(x) {
238 return (Some(true), s);
239 }
240
241 match upper_next {
242 Some(up) => {
243 let c = if up {
244 x.to_uppercase().next().unwrap_or(x)
245 } else {
246 x.to_lowercase().next().unwrap_or(x)
247 };
248 s.push(c);
249 }
250 None => s.push(x),
251 }
252 (None, s)
253 });
254 camel
255 }
256
257 fn snake_case(&self) -> String {
258 let (_, _, snake) = self
259 .trim()
260 .chars()
261 .fold((Some(true), Some(true), String::with_capacity(self.len())), |acc, x| {
262 let (previous_underscore, previous_upper, mut s) = acc;
263 if is_out_of_case(x) {
264 if !previous_underscore.unwrap_or(true) {
265 s.push('_');
266 }
267 return (Some(true), Some(false), s);
268 }
269
270 let is_upper = x.is_uppercase();
271 if is_upper && !previous_underscore.unwrap_or(false) && !previous_upper.unwrap_or(false) {
272 s.push('_');
273 }
274
275 s.push(x.to_lowercase().next().unwrap_or(x));
276
277 (Some(false), Some(is_upper), s)
278 });
279 snake
280 }
281
282 fn on_one_line(&self, indent: Option<u64>, line_break: Option<bool>) -> String {
283 let r = ONE_LINER_REGEX.replace_all(self, "");
284
285 let eol = if line_break.unwrap_or(true) { "\n" } else { "" };
286 let indent = indent.unwrap_or_default();
287
288 let result = format!("{:indent$}{}{}", "", r.trim(), eol, indent = indent as usize);
289 result
290 }
291
292 fn regex_extract(&self, regex_extractor: &str, replacer: Option<&str>, separator: Option<&str>) -> Result<String, anyhow::Error> {
293 let regex_extr = Regex::new(regex_extractor)?;
294 let replacer = replacer.unwrap_or("$1");
295 let separator = separator.unwrap_or(", ");
296
297 let mut values = Vec::new();
298 for m in regex_extr.find_iter(self) {
299 let matched = m.as_str();
300 let replaced = regex_extr.replace(matched, replacer);
301 values.push(replaced.to_string());
302 }
303
304 Ok(values.join(separator))
305 }
306
307 fn regex_transform(&self, regex_pattern: &str, regex_replacer: &str) -> Result<String, anyhow::Error> {
308 let regex_extr = Regex::new(regex_pattern)?;
309 let transformed = regex_extr.replace_all(self, regex_replacer);
310 Ok(transformed.into())
311 }
312}
313
314static ONE_LINER_REGEX: once_cell::sync::Lazy<regex::Regex> =
315 once_cell::sync::Lazy::new(|| regex::Regex::new(r#" *[\r\n]+ *"#).expect("The ONE_LINER_REGEX regex did not compile."));
316
317fn is_out_of_case(c: char) -> bool {
318 !(c.is_alphabetic() || c.is_numeric())
319}
320
321#[cfg(test)]
322mod test {
323 use super::*;
324 use test_case::test_case;
325
326 #[test_case("/user/{username}", "\\{([^}]*)}", Some("$1"), None, "username")]
327 #[test_case("/user/{username}/{id}", "\\{([^}]*)}", Some("$1"), None, "username, id")]
328 #[test_case("/user/{username}/{id}", "\\{([^}]*)}", Some("<$1>"), Some("|"), "<username>|<id>")]
329 fn regex_extract_tests(arg: &str, regex_extractor: &str, regex_replacer: Option<&str>, separator: Option<&str>, expected: &str) {
330 let result = arg.regex_extract(regex_extractor, regex_replacer, separator).unwrap();
331 assert_eq!(result, expected);
332 }
333
334 #[test_case("/user/{username}", "\\{([^}]*)}", "$1", "/user/username")]
335 #[test_case("/user/{user}/{id}", "\\{([^}]*)}", "$1", "/user/user/id")]
336 #[test_case("/user/{username}/{id}", "\\{([^}]*)}", "<$1>", "/user/<username>/<id>")]
337 fn regex_transform_tests(arg: &str, regex_extractor: &str, regex_replacer: &str, expected: &str) {
338 let result = arg.regex_transform(regex_extractor, regex_replacer).unwrap();
339 assert_eq!(result, expected);
340 }
341
342 #[test_case(" ", true)]
343 #[test_case(" \t\n ", true)]
344 #[test_case(" \n", true)]
345 #[test_case("hello", false)]
346 fn is_empty_or_whitespaces_tests(v: &str, expected: bool) {
347 assert_eq!(v.is_empty_or_whitespaces(), expected);
348 assert_eq!(v.to_string().is_empty_or_whitespaces(), expected);
349 assert_eq!(Some(v.to_string()).is_empty_or_whitespaces(), expected);
350 }
351
352 #[test_case("leave/me/alone", "", "leave")]
353 #[test_case("/leave/me", "", "leave")]
354 #[test_case("/leave/me", "e", "/l")]
355 #[test_case("", "e", "")]
356 fn split_get_first_tests(v: &str, splitter: &str, expected: &str) {
357 let splitter = if splitter.is_empty() { None } else { Some(splitter.to_string()) };
358 assert_eq!(v.split_get_first(splitter.clone()), expected);
359 assert_eq!(v.to_string().split_get_first(splitter.clone()), expected);
360 assert_eq!(Some(v.to_string()).split_get_first(splitter), expected);
361 }
362
363 #[test_case("leave/me/alone", "", "alone")]
364 #[test_case("/leave/me/", "", "me")]
365 #[test_case("/leave/me", "e", "/m")]
366 #[test_case("", "e", "")]
367 fn split_get_last_tests(v: &str, splitter: &str, expected: &str) {
368 let splitter = if splitter.is_empty() { None } else { Some(splitter.to_string()) };
369 assert_eq!(v.split_get_last(splitter.clone()), expected);
370 assert_eq!(v.to_string().split_get_last(splitter.clone()), expected);
371 assert_eq!(Some(v.to_string()).split_get_last(splitter), expected);
372 }
373
374 #[test_case("", "e", "")]
375 #[test_case(" leave ", "", "leave")]
376 #[test_case("elle", "e", "ll")]
377 #[test_case("-test_", "-", "test_")]
378 #[test_case("leel", "e", "leel")]
379 fn del_char_tests(v: &str, trimmer: &str, expected: &str) {
381 let trimmer = if trimmer.is_empty_or_whitespaces() {
382 None
383 } else {
384 Some(trimmer.to_string())
385 };
386 assert_eq!(v.trim_char(trimmer), expected);
387 }
388
389 #[test_case("", "e", "")]
390 #[test_case(" leave ", "", "leave ")]
391 #[test_case("elle", "e", "lle")]
392 #[test_case("-test_", "_", "-test_")]
393 #[test_case("leel", "e", "leel")]
394 fn del_char_start_tests(v: &str, trimmer: &str, expected: &str) {
395 let trimmer = if trimmer.is_empty_or_whitespaces() {
396 None
397 } else {
398 Some(trimmer.to_string())
399 };
400 assert_eq!(v.trim_start_char(trimmer), expected);
401 }
402
403 #[test_case("", "e", "")]
404 #[test_case(" leave ", "", " leave")]
405 #[test_case("elle", "e", "ell")]
406 #[test_case("-test_", "-", "-test_")]
407 #[test_case("leel", "e", "leel")]
408 fn del_char_end_tests(v: &str, trimmer: &str, expected: &str) {
409 let trimmer = if trimmer.is_empty_or_whitespaces() {
410 None
411 } else {
412 Some(trimmer.to_string())
413 };
414 assert_eq!(v.trim_end_char(trimmer), expected);
415 }
416
417 #[test_case("42", "42")]
418 #[test_case("HELLO", "HELLO")]
419 #[test_case("HelloWorld", "HelloWorld")]
420 #[test_case("helloo", "Helloo")]
421 #[test_case("heLlo wOrld", "HeLloWOrld")]
422 #[test_case("hello_wwrld", "HelloWwrld")]
423 #[test_case("hello-worldd", "HelloWorldd")]
424 #[test_case("helo-WORLD", "HeloWORLD")]
425 #[test_case("hello/WOOLD", "HelloWOOLD")]
426 #[test_case("hello\\\\WORD", "HelloWORD")]
427 fn pascal_case_tests(v: &str, expected: &str) {
428 assert_eq!(v.pascal_case(), expected);
429 }
430
431 #[test_case("42", "42")]
432 #[test_case("HELLO", "hELLO")]
433 #[test_case("hey", "hey")]
434 #[test_case("heLlo wOrld", "heLloWOrld")]
435 #[test_case("hey_world", "heyWorld")]
436 #[test_case("helo-world", "heloWorld")]
437 #[test_case("helloo-WORLD", "hellooWORLD")]
438 #[test_case("HelooWorld", "helooWorld")]
439 fn camel_case_tests(v: &str, expected: &str) {
440 assert_eq!(v.camel_case(), expected);
441 }
442
443 #[test_case("42", "42")]
444 #[test_case("hello", "hello")]
445 #[test_case("helo world", "helo_world")]
446 #[test_case("helloo_world", "helloo_world")]
447 #[test_case("heloo-world", "heloo_world")]
448 #[test_case("hallo--world", "hallo_world")]
449 #[test_case("halo__World", "halo_world")]
450 #[test_case("haloo-World", "haloo_world")]
451 #[test_case("halloo _ world", "halloo_world")]
452 #[test_case("heello - world", "heello_world")]
453 #[test_case("HelloWoorld", "hello_woorld")]
454 #[test_case("heello _WOORLD", "heello_woorld")]
455 #[test_case(" HEELLO", "heello")]
456 #[test_case("Helo ", "helo")]
457 #[test_case("2Hello2 ", "2_hello2")]
458 #[test_case("HelloWorld_42LongName ", "hello_world_42_long_name")]
459 fn snake_case_tests(v: &str, expected: &str) {
460 assert_eq!(v.snake_case(), expected);
461 }
462
463 #[test_case("42", "42")]
464 #[test_case("hello", "hello")]
465 #[test_case("Test", "test")]
466 fn lowercase_first_letter_tests(v: &str, expected: &str) {
467 assert_eq!(v.lowercase_first_letter(), expected)
468 }
469
470 #[test_case("42", "42")]
471 #[test_case("HELLO", "HELLO")]
472 #[test_case("test", "Test")]
473 fn uppercase_first_letter_tests(v: &str, expected: &str) {
474 assert_eq!(v.uppercase_first_letter(), expected)
475 }
476}