Skip to main content

gluesql_core/data/
string_ext.rs

1use {
2    crate::result::Result,
3    regex::{Regex, RegexBuilder},
4    serde::Serialize,
5    thiserror::Error,
6};
7
8#[derive(Error, Serialize, Debug, PartialEq, Eq)]
9pub enum StringExtError {
10    #[error("unreachable literal unary operation")]
11    UnreachablePatternParsing,
12    #[error("invalid regular expression {pattern:?}: {error}")]
13    InvalidRegexPattern { pattern: String, error: String },
14}
15
16pub trait StringExt {
17    fn like(&self, pattern: &str, case_sensitive: bool) -> Result<bool>;
18    fn regex(&self, pattern: &str, case_sensitive: bool) -> Result<bool>;
19}
20
21impl StringExt for str {
22    fn like(&self, pattern: &str, case_sensitive: bool) -> Result<bool> {
23        let (match_string, match_pattern) = if case_sensitive {
24            (self.to_owned(), pattern.to_owned())
25        } else {
26            let lowercase_string = self.to_lowercase();
27            let lowercase_pattern = pattern.to_lowercase();
28
29            (lowercase_string, lowercase_pattern)
30        };
31
32        Ok(Regex::new(&format!(
33            "^{}$",
34            regex::escape(match_pattern.as_str())
35                .replace('%', ".*")
36                .replace('_', ".")
37        ))
38        .map_err(|_| StringExtError::UnreachablePatternParsing)?
39        .is_match(match_string.as_str()))
40    }
41
42    fn regex(&self, pattern: &str, case_sensitive: bool) -> Result<bool> {
43        Ok(RegexBuilder::new(pattern)
44            .case_insensitive(!case_sensitive)
45            .build()
46            .map_err(|error| StringExtError::InvalidRegexPattern {
47                pattern: pattern.to_owned(),
48                error: error.to_string(),
49            })
50            .map(|regex| regex.is_match(self))?)
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use {
57        super::{StringExt, StringExtError},
58        crate::result::Error,
59    };
60
61    #[test]
62    fn regex() {
63        assert_eq!("Hello".regex("ell", true), Ok(true));
64        assert_eq!("Hello".regex("^hello$", true), Ok(false));
65        assert_eq!("Hello".regex("^hello$", false), Ok(true));
66        assert!(matches!(
67            "Hello".regex("[", true),
68            Err(Error::StringExt(StringExtError::InvalidRegexPattern { pattern, .. })) if pattern == "["
69        ));
70        assert!(matches!(
71            "Hello".regex("(?i)[", true),
72            Err(Error::StringExt(StringExtError::InvalidRegexPattern { pattern, .. })) if pattern == "(?i)["
73        ));
74        assert!(matches!(
75            "Hello".regex("[", false),
76            Err(Error::StringExt(StringExtError::InvalidRegexPattern { pattern, .. })) if pattern == "["
77        ));
78
79        // the case-insensitive flag must not leak into the reported parse error
80        assert!(matches!(
81            "Hello".regex("[", false),
82            Err(Error::StringExt(StringExtError::InvalidRegexPattern { error, .. }))
83                if !error.contains("(?i)")
84        ));
85    }
86}