use super::string_matcher::{CompiledStringMatcher, StringMatcher};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum LogicalMatcher {
Not(Box<LogicalMatcher>),
Or(Vec<LogicalMatcher>),
And(Vec<LogicalMatcher>),
#[serde(untagged)]
Leaf(StringMatcher),
}
impl Default for LogicalMatcher {
fn default() -> Self {
LogicalMatcher::Leaf(StringMatcher::Exists(true))
}
}
#[derive(Debug, Clone)]
pub enum CompiledLogicalMatcher {
Not(Box<CompiledLogicalMatcher>),
Or(Vec<CompiledLogicalMatcher>),
And(Vec<CompiledLogicalMatcher>),
Leaf(CompiledStringMatcher),
}
impl CompiledLogicalMatcher {
pub fn compile(matcher: &LogicalMatcher) -> Result<Self, regex::Error> {
match matcher {
LogicalMatcher::Not(inner) => {
Ok(CompiledLogicalMatcher::Not(Box::new(Self::compile(inner)?)))
}
LogicalMatcher::Or(matchers) => {
let compiled: Result<Vec<_>, _> = matchers.iter().map(Self::compile).collect();
Ok(CompiledLogicalMatcher::Or(compiled?))
}
LogicalMatcher::And(matchers) => {
let compiled: Result<Vec<_>, _> = matchers.iter().map(Self::compile).collect();
Ok(CompiledLogicalMatcher::And(compiled?))
}
LogicalMatcher::Leaf(string_matcher) => Ok(CompiledLogicalMatcher::Leaf(
CompiledStringMatcher::compile(string_matcher)?,
)),
}
}
pub fn matches(&self, value: Option<&str>, case_sensitive: bool) -> bool {
match self {
CompiledLogicalMatcher::Not(inner) => !inner.matches(value, case_sensitive),
CompiledLogicalMatcher::Or(matchers) => {
matchers.iter().any(|m| m.matches(value, case_sensitive))
}
CompiledLogicalMatcher::And(matchers) => {
matchers.iter().all(|m| m.matches(value, case_sensitive))
}
CompiledLogicalMatcher::Leaf(matcher) => matcher.matches(value, case_sensitive),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logical_not() {
let matcher = CompiledLogicalMatcher::compile(&LogicalMatcher::Not(Box::new(
LogicalMatcher::Leaf(StringMatcher::Equals("test".to_string())),
)))
.unwrap();
assert!(!matcher.matches(Some("test"), true)); assert!(matcher.matches(Some("other"), true)); assert!(matcher.matches(None, true)); }
#[test]
fn test_logical_or() {
let matcher = CompiledLogicalMatcher::compile(&LogicalMatcher::Or(vec![
LogicalMatcher::Leaf(StringMatcher::Equals("foo".to_string())),
LogicalMatcher::Leaf(StringMatcher::Equals("bar".to_string())),
LogicalMatcher::Leaf(StringMatcher::Equals("baz".to_string())),
]))
.unwrap();
assert!(matcher.matches(Some("foo"), true));
assert!(matcher.matches(Some("bar"), true));
assert!(matcher.matches(Some("baz"), true));
assert!(!matcher.matches(Some("qux"), true));
assert!(!matcher.matches(None, true));
}
#[test]
fn test_logical_and() {
let matcher = CompiledLogicalMatcher::compile(&LogicalMatcher::And(vec![
LogicalMatcher::Leaf(StringMatcher::Contains("api".to_string())),
LogicalMatcher::Leaf(StringMatcher::StartsWith("/".to_string())),
]))
.unwrap();
assert!(matcher.matches(Some("/api/v1"), true));
assert!(matcher.matches(Some("/my-api"), true));
assert!(!matcher.matches(Some("api/v1"), true)); assert!(!matcher.matches(Some("/users"), true)); }
#[test]
fn test_logical_nested() {
let matcher = CompiledLogicalMatcher::compile(&LogicalMatcher::Not(Box::new(
LogicalMatcher::Or(vec![
LogicalMatcher::Leaf(StringMatcher::Equals("foo".to_string())),
LogicalMatcher::Leaf(StringMatcher::Equals("bar".to_string())),
]),
)))
.unwrap();
assert!(!matcher.matches(Some("foo"), true));
assert!(!matcher.matches(Some("bar"), true));
assert!(matcher.matches(Some("baz"), true));
assert!(matcher.matches(Some("anything"), true));
}
}