use regex::Regex;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub struct CachedValue {
pub value: String,
pub lower: String,
}
impl CachedValue {
pub fn new(value: impl Into<String>) -> Self {
let value = value.into();
let lower = value.to_lowercase();
Self { value, lower }
}
#[inline]
pub fn pattern(&self, case_sensitive: bool) -> &str {
if case_sensitive {
&self.value
} else {
&self.lower
}
}
#[inline]
pub fn equals(&self, value: &str, case_sensitive: bool) -> bool {
if case_sensitive {
value == self.value
} else {
value.to_lowercase() == self.lower
}
}
#[inline]
pub fn contained_in(&self, value: &str, case_sensitive: bool) -> bool {
if case_sensitive {
value.contains(&self.value)
} else {
value.to_lowercase().contains(&self.lower)
}
}
#[inline]
pub fn starts(&self, value: &str, case_sensitive: bool) -> bool {
if case_sensitive {
value.starts_with(&self.value)
} else {
value.to_lowercase().starts_with(&self.lower)
}
}
#[inline]
pub fn ends(&self, value: &str, case_sensitive: bool) -> bool {
if case_sensitive {
value.ends_with(&self.value)
} else {
value.to_lowercase().ends_with(&self.lower)
}
}
}
impl From<String> for CachedValue {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for CachedValue {
fn from(value: &str) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone)]
pub enum StringMatchCore {
Equals(CachedValue),
Contains(CachedValue),
StartsWith(CachedValue),
EndsWith(CachedValue),
Regex(Arc<Regex>),
}
impl StringMatchCore {
pub fn equals(value: impl Into<String>) -> Self {
Self::Equals(CachedValue::new(value))
}
pub fn contains(value: impl Into<String>) -> Self {
Self::Contains(CachedValue::new(value))
}
pub fn starts_with(value: impl Into<String>) -> Self {
Self::StartsWith(CachedValue::new(value))
}
pub fn ends_with(value: impl Into<String>) -> Self {
Self::EndsWith(CachedValue::new(value))
}
pub fn regex(pattern: &str) -> Result<Self, regex::Error> {
Ok(Self::Regex(Arc::new(Regex::new(pattern)?)))
}
pub fn matches(&self, value: &str, case_sensitive: bool) -> bool {
match self {
Self::Equals(cached) => cached.equals(value, case_sensitive),
Self::Contains(cached) => cached.contained_in(value, case_sensitive),
Self::StartsWith(cached) => cached.starts(value, case_sensitive),
Self::EndsWith(cached) => cached.ends(value, case_sensitive),
Self::Regex(regex) => regex.is_match(value),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cached_value_new() {
let cv = CachedValue::new("Hello World");
assert_eq!(cv.value, "Hello World");
assert_eq!(cv.lower, "hello world");
}
#[test]
fn test_cached_value_equals() {
let cv = CachedValue::new("Test");
assert!(cv.equals("Test", true));
assert!(!cv.equals("test", true));
assert!(!cv.equals("TEST", true));
assert!(cv.equals("Test", false));
assert!(cv.equals("test", false));
assert!(cv.equals("TEST", false));
assert!(cv.equals("tEsT", false));
}
#[test]
fn test_cached_value_contained_in() {
let cv = CachedValue::new("api");
assert!(cv.contained_in("/api/users", true));
assert!(!cv.contained_in("/API/users", true));
assert!(cv.contained_in("/api/users", false));
assert!(cv.contained_in("/API/users", false));
}
#[test]
fn test_cached_value_starts() {
let cv = CachedValue::new("/api");
assert!(cv.starts("/api/users", true));
assert!(!cv.starts("/API/users", true));
assert!(cv.starts("/api/users", false));
assert!(cv.starts("/API/users", false));
}
#[test]
fn test_cached_value_ends() {
let cv = CachedValue::new(".json");
assert!(cv.ends("data.json", true));
assert!(!cv.ends("data.JSON", true));
assert!(cv.ends("data.json", false));
assert!(cv.ends("data.JSON", false));
}
#[test]
fn test_string_match_core_equals() {
let matcher = StringMatchCore::equals("test");
assert!(matcher.matches("test", true));
assert!(!matcher.matches("TEST", true));
assert!(matcher.matches("TEST", false));
}
#[test]
fn test_string_match_core_contains() {
let matcher = StringMatchCore::contains("api");
assert!(matcher.matches("/api/v1", true));
assert!(!matcher.matches("/API/v1", true));
assert!(matcher.matches("/API/v1", false));
}
#[test]
fn test_string_match_core_starts_with() {
let matcher = StringMatchCore::starts_with("/api");
assert!(matcher.matches("/api/users", true));
assert!(!matcher.matches("/API/users", true));
assert!(matcher.matches("/API/users", false));
}
#[test]
fn test_string_match_core_ends_with() {
let matcher = StringMatchCore::ends_with(".json");
assert!(matcher.matches("data.json", true));
assert!(!matcher.matches("data.JSON", true));
assert!(matcher.matches("data.JSON", false));
}
#[test]
fn test_string_match_core_regex() {
let matcher = StringMatchCore::regex(r"^/api/v\d+/").unwrap();
assert!(matcher.matches("/api/v1/users", true));
assert!(matcher.matches("/api/v99/items", true));
assert!(!matcher.matches("/api/users", true));
}
#[test]
fn test_cached_value_from_impls() {
let cv1: CachedValue = "test".into();
let cv2: CachedValue = String::from("test").into();
assert_eq!(cv1.value, "test");
assert_eq!(cv2.value, "test");
}
}