use super::matcher::CachedValue;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum StringMatcher {
#[serde(rename = "equals")]
Equals(String),
#[serde(rename = "contains")]
Contains(String),
#[serde(rename = "startsWith")]
StartsWith(String),
#[serde(rename = "endsWith")]
EndsWith(String),
#[serde(rename = "matches")]
Matches(String),
#[serde(rename = "exists")]
Exists(bool),
}
impl Default for StringMatcher {
fn default() -> Self {
StringMatcher::Exists(true)
}
}
#[derive(Debug, Clone)]
pub enum CompiledStringMatcher {
Equals(CachedValue),
Contains(CachedValue),
StartsWith(CachedValue),
EndsWith(CachedValue),
Matches(Arc<Regex>),
Exists(bool),
}
impl CompiledStringMatcher {
pub fn compile(matcher: &StringMatcher) -> Result<Self, regex::Error> {
match matcher {
StringMatcher::Equals(v) => Ok(CompiledStringMatcher::Equals(CachedValue::new(v))),
StringMatcher::Contains(v) => Ok(CompiledStringMatcher::Contains(CachedValue::new(v))),
StringMatcher::StartsWith(v) => {
Ok(CompiledStringMatcher::StartsWith(CachedValue::new(v)))
}
StringMatcher::EndsWith(v) => Ok(CompiledStringMatcher::EndsWith(CachedValue::new(v))),
StringMatcher::Matches(pattern) => {
let regex = Regex::new(pattern)?;
Ok(CompiledStringMatcher::Matches(Arc::new(regex)))
}
StringMatcher::Exists(exists) => Ok(CompiledStringMatcher::Exists(*exists)),
}
}
pub fn matches(&self, value: Option<&str>, case_sensitive: bool) -> bool {
match (self, value) {
(CompiledStringMatcher::Exists(should_exist), v) => {
let does_exist = v.is_some();
*should_exist == does_exist
}
(_, None) => false,
(CompiledStringMatcher::Equals(cached), Some(v)) => cached.equals(v, case_sensitive),
(CompiledStringMatcher::Contains(cached), Some(v)) => {
cached.contained_in(v, case_sensitive)
}
(CompiledStringMatcher::StartsWith(cached), Some(v)) => {
cached.starts(v, case_sensitive)
}
(CompiledStringMatcher::EndsWith(cached), Some(v)) => cached.ends(v, case_sensitive),
(CompiledStringMatcher::Matches(regex), Some(v)) => {
regex.is_match(v)
}
}
}
pub fn matches_with_except(
&self,
value: Option<&str>,
case_sensitive: bool,
except: Option<&CompiledExcept>,
) -> bool {
let processed_value = match (value, except) {
(Some(v), Some(exc)) => Some(exc.apply(v)),
(Some(v), None) => Some(v.to_string()),
(None, _) => None,
};
self.matches(processed_value.as_deref(), case_sensitive)
}
}
#[derive(Debug, Clone)]
pub struct CompiledExcept {
pub regex: Arc<Regex>,
}
impl CompiledExcept {
pub fn compile(pattern: &str) -> Result<Self, regex::Error> {
Ok(CompiledExcept {
regex: Arc::new(Regex::new(pattern)?),
})
}
pub fn apply(&self, value: &str) -> String {
self.regex.replace_all(value, "").to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_matcher_equals() {
let matcher =
CompiledStringMatcher::compile(&StringMatcher::Equals("test".to_string())).unwrap();
assert!(matcher.matches(Some("test"), true));
assert!(!matcher.matches(Some("TEST"), true));
assert!(matcher.matches(Some("TEST"), false));
assert!(!matcher.matches(Some("other"), true));
assert!(!matcher.matches(None, true));
}
#[test]
fn test_string_matcher_contains() {
let matcher =
CompiledStringMatcher::compile(&StringMatcher::Contains("api".to_string())).unwrap();
assert!(matcher.matches(Some("/api/v1"), true));
assert!(matcher.matches(Some("my-api-service"), true));
assert!(!matcher.matches(Some("/API/v1"), true));
assert!(matcher.matches(Some("/API/v1"), false));
assert!(!matcher.matches(Some("other"), true));
assert!(!matcher.matches(None, true));
}
#[test]
fn test_string_matcher_starts_with() {
let matcher =
CompiledStringMatcher::compile(&StringMatcher::StartsWith("/api".to_string())).unwrap();
assert!(matcher.matches(Some("/api/v1"), true));
assert!(matcher.matches(Some("/api"), true));
assert!(!matcher.matches(Some("/API/v1"), true));
assert!(matcher.matches(Some("/API/v1"), false));
assert!(!matcher.matches(Some("other/api"), true));
assert!(!matcher.matches(None, true));
}
#[test]
fn test_string_matcher_ends_with() {
let matcher =
CompiledStringMatcher::compile(&StringMatcher::EndsWith(".json".to_string())).unwrap();
assert!(matcher.matches(Some("/data.json"), true));
assert!(matcher.matches(Some(".json"), true));
assert!(!matcher.matches(Some("/data.JSON"), true));
assert!(matcher.matches(Some("/data.JSON"), false));
assert!(!matcher.matches(Some("/data.xml"), true));
assert!(!matcher.matches(None, true));
}
#[test]
fn test_string_matcher_regex() {
let matcher =
CompiledStringMatcher::compile(&StringMatcher::Matches(r"^/api/v\d+/".to_string()))
.unwrap();
assert!(matcher.matches(Some("/api/v1/users"), true));
assert!(matcher.matches(Some("/api/v99/items"), true));
assert!(!matcher.matches(Some("/api/users"), true));
assert!(!matcher.matches(None, true));
}
#[test]
fn test_string_matcher_exists() {
let exists_true = CompiledStringMatcher::compile(&StringMatcher::Exists(true)).unwrap();
let exists_false = CompiledStringMatcher::compile(&StringMatcher::Exists(false)).unwrap();
assert!(exists_true.matches(Some("any value"), true));
assert!(exists_true.matches(Some(""), true));
assert!(!exists_true.matches(None, true));
assert!(!exists_false.matches(Some("any value"), true));
assert!(exists_false.matches(None, true));
}
#[test]
fn test_string_matcher_serde() {
let json = r#"{"equals": "test"}"#;
let matcher: StringMatcher = serde_json::from_str(json).unwrap();
assert_eq!(matcher, StringMatcher::Equals("test".to_string()));
let json = r#"{"contains": "api"}"#;
let matcher: StringMatcher = serde_json::from_str(json).unwrap();
assert_eq!(matcher, StringMatcher::Contains("api".to_string()));
let json = r#"{"startsWith": "/api"}"#;
let matcher: StringMatcher = serde_json::from_str(json).unwrap();
assert_eq!(matcher, StringMatcher::StartsWith("/api".to_string()));
let json = r#"{"endsWith": ".json"}"#;
let matcher: StringMatcher = serde_json::from_str(json).unwrap();
assert_eq!(matcher, StringMatcher::EndsWith(".json".to_string()));
let json = r#"{"matches": "^/api/v\\d+"}"#;
let matcher: StringMatcher = serde_json::from_str(json).unwrap();
assert_eq!(matcher, StringMatcher::Matches(r"^/api/v\d+".to_string()));
let json = r#"{"exists": true}"#;
let matcher: StringMatcher = serde_json::from_str(json).unwrap();
assert_eq!(matcher, StringMatcher::Exists(true));
}
#[test]
fn test_except_parameter() {
let except = CompiledExcept::compile(r"\d+").unwrap();
assert_eq!(except.apply("abc123def456"), "abcdef");
assert_eq!(except.apply("12345"), "");
assert_eq!(except.apply("no-digits-here"), "no-digits-here");
}
#[test]
fn test_string_matcher_with_except() {
let matcher =
CompiledStringMatcher::compile(&StringMatcher::Equals("Hello World".to_string()))
.unwrap();
let except = CompiledExcept::compile(r"\d+").unwrap();
assert!(!matcher.matches(Some("Hello123 World456"), true));
assert!(matcher.matches_with_except(Some("Hello123 World456"), true, Some(&except)));
}
}