codegenr_lib/helpers/
string_ext.rs

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