codegenr_lib/helpers/
string_ext.rs

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
25// impl<T> StringExt for T where T: AsRef<str> {}
26
27impl 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  //todo: #[test_case("test", " t", "es")]
380  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}