motorx_core/config/
match_type.rs

1use std::fmt::Display;
2use std::hash::Hash;
3use std::{cmp::Ordering, str::FromStr};
4
5use once_cell::sync::Lazy;
6
7use regex::{Captures, Regex};
8
9#[cfg_attr(feature = "serde-config", derive(serde::Deserialize))]
10#[cfg_attr(feature = "serde-config", serde(rename_all = "snake_case"))]
11#[derive(Debug, Clone)]
12pub enum MatchType {
13    /// Matches from start of subject string, default behavior
14    Start(String),
15    /// Matches if this enum's value is contained in the subject string
16    Contains(String),
17    /// Uses regex pattern to match on subject string, ex. `regex(this_is_my_regex_pattern)`
18    #[cfg_attr(feature = "serde-config", serde(with = "de_regex"))]
19    Regex(Regex),
20}
21
22pub enum MatchResult<'t> {
23    Start(bool),
24    Contains(bool),
25    Regex(Option<Captures<'t>>),
26}
27
28impl<'t> MatchResult<'t> {
29    #[inline]
30    pub(crate) fn is_match(&self) -> bool {
31        match self {
32            MatchResult::Start(matched) => *matched,
33            MatchResult::Contains(matched) => *matched,
34            MatchResult::Regex(captures) => captures.is_some(),
35        }
36    }
37}
38
39impl MatchType {
40    #[inline]
41    pub(crate) fn matches<'a>(&self, string: &'a str) -> MatchResult<'a> {
42        match self {
43            MatchType::Start(pattern) => MatchResult::Start(string.starts_with(pattern)),
44            MatchType::Contains(pattern) => MatchResult::Contains(string.contains(pattern)),
45            MatchType::Regex(regex) => MatchResult::Regex(regex.captures(string)),
46        }
47    }
48
49    #[inline]
50    pub(crate) fn priority(&self) -> usize {
51        match self {
52            // highest priority
53            MatchType::Start(_) => usize::MIN,
54            MatchType::Contains(_) => 1,
55            // lowest priority
56            MatchType::Regex(_) => usize::MAX,
57        }
58    }
59
60    #[inline]
61    fn length(&self) -> usize {
62        match self {
63            MatchType::Start(pat) => pat.len(),
64            MatchType::Contains(pat) => pat.len(),
65            MatchType::Regex(re) => re.as_str().len(),
66        }
67    }
68}
69
70impl PartialEq for MatchType {
71    fn eq(&self, other: &Self) -> bool {
72        match self {
73            MatchType::Start(pat) => match other {
74                MatchType::Start(other_pat) => pat == other_pat,
75                _ => false,
76            },
77            MatchType::Contains(pat) => match other {
78                MatchType::Contains(other_pat) => pat == other_pat,
79                _ => false,
80            },
81            MatchType::Regex(re) => match other {
82                MatchType::Regex(other_re) => re.as_str() == other_re.as_str(),
83                _ => false,
84            },
85        }
86    }
87}
88
89impl Eq for MatchType {}
90
91impl Ord for MatchType {
92    fn cmp(&self, other: &Self) -> Ordering {
93        match self.priority().cmp(&other.priority()) {
94            Ordering::Greater => Ordering::Greater,
95            Ordering::Less => Ordering::Less,
96            Ordering::Equal => {
97                // priority was same, use length to break tie
98                // longer / more specific should be less (go first)
99                other.length().cmp(&self.length())
100            }
101        }
102    }
103}
104
105impl PartialOrd for MatchType {
106    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
107        Some(self.cmp(other))
108    }
109}
110
111impl Display for MatchType {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            MatchType::Start(path) => write!(f, "start({})", path),
115            MatchType::Contains(pat) => write!(f, "contains({})", pat),
116            MatchType::Regex(re) => write!(f, "regex({})", re.as_str()),
117        }
118    }
119}
120
121impl Hash for MatchType {
122    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
123        self.to_string().hash(state)
124    }
125}
126
127static MATCH_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^regex\((.*)\)$").unwrap());
128static MATCH_CONTAINS: Lazy<Regex> = Lazy::new(|| Regex::new(r"^contains\((.*)\)$").unwrap());
129
130#[derive(Debug)]
131pub struct MatchTypeFromStrError(String);
132
133impl Display for MatchTypeFromStrError {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        self.0.fmt(f)
136    }
137}
138impl std::error::Error for MatchTypeFromStrError {}
139
140impl FromStr for MatchType {
141    type Err = MatchTypeFromStrError;
142
143    fn from_str(s: &str) -> Result<Self, Self::Err> {
144        if let Some(captures) = MATCH_RE.captures(s) {
145            // regex matcher
146            captures
147                .get(1)
148                .map(|re| MatchType::Regex(Regex::new(re.into()).unwrap()))
149                .ok_or(MatchTypeFromStrError("".into()))
150        } else if let Some(captures) = MATCH_CONTAINS.captures(s) {
151            // contains matcher
152            captures
153                .get(1)
154                .map(|pat| MatchType::Contains(pat.as_str().into()))
155                .ok_or(MatchTypeFromStrError("".into()))
156        } else {
157            // path matcher
158            Ok(MatchType::Start(s.into()))
159        }
160    }
161}
162
163#[cfg(feature = "serde-config")]
164mod de_regex {
165    use std::str::FromStr;
166
167    use regex::Regex;
168    use serde::{de::Visitor, Deserializer};
169
170    struct RegexVisitor;
171
172    impl<'de> Visitor<'de> for RegexVisitor {
173        type Value = Regex;
174
175        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
176            write!(formatter, "A valid string of regex.")
177        }
178
179        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
180        where
181            E: serde::de::Error,
182        {
183            Regex::from_str(v).map_err(|e| E::custom(e.to_string()))
184        }
185    }
186
187    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
188        deserializer: D,
189    ) -> Result<Regex, D::Error> {
190        deserializer.deserialize_str(RegexVisitor)
191    }
192}