use regex::Regex;
#[cfg(feature = "serde_derive")]
use serde::{Deserialize, Serialize};
pub trait Needle {
fn is_match(&self, haystack: &str) -> bool;
}
pub trait NeedleIter: Needle {
fn is_match_in<'a, I, S>(&self, haystacks: &mut I) -> bool
where
I: Iterator<Item = S>,
S: Into<&'a str>,
{
haystacks.any(|h| self.is_match(h.into()))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub enum StringMatchLength {
Full,
Partial,
Word,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub struct StringMatch {
text: String,
match_length: StringMatchLength,
case_sensitive: bool,
}
impl<S> From<S> for StringMatch
where
S: Into<String>,
{
fn from(text: S) -> Self {
Self {
text: text.into(),
match_length: StringMatchLength::Full,
case_sensitive: true,
}
}
}
impl StringMatch {
pub fn new<S>(text: S) -> Self
where
S: Into<String>,
{
Self::from(text)
}
pub fn is_full_match(&self) -> bool {
matches!(self.match_length, StringMatchLength::Full)
}
pub fn is_partial_match(&self) -> bool {
matches!(self.match_length, StringMatchLength::Partial)
}
pub fn is_word_match(&self) -> bool {
matches!(self.match_length, StringMatchLength::Word)
}
pub fn is_case_sensitive(&self) -> bool {
self.case_sensitive
}
pub fn partial(mut self) -> Self {
self.match_length = StringMatchLength::Partial;
self
}
pub fn full(mut self) -> Self {
self.match_length = StringMatchLength::Full;
self
}
pub fn word(mut self) -> Self {
self.match_length = StringMatchLength::Word;
self
}
pub fn case_insensitive(mut self) -> Self {
self.case_sensitive = false;
self
}
pub fn case_sensitive(mut self) -> Self {
self.case_sensitive = true;
self
}
}
fn needle_in_haystack(needle: &str, haystack: &str, match_length: &StringMatchLength) -> bool {
match match_length {
StringMatchLength::Full => haystack == needle,
StringMatchLength::Partial => haystack.contains(needle),
StringMatchLength::Word => format!(" {} ", haystack).contains(&format!(" {} ", needle)),
}
}
impl Needle for StringMatch {
fn is_match(&self, haystack: &str) -> bool {
match self.case_sensitive {
true => needle_in_haystack(&self.text, haystack, &self.match_length),
false => {
let hs = haystack.to_lowercase();
let needle = self.text.to_lowercase();
needle_in_haystack(&needle, &hs, &self.match_length)
}
}
}
}
impl Needle for Regex {
fn is_match(&self, haystack: &str) -> bool {
self.is_match(haystack)
}
}
impl Needle for &str {
fn is_match(&self, haystack: &str) -> bool {
self == &haystack
}
}
impl Needle for String {
fn is_match(&self, haystack: &str) -> bool {
self == haystack
}
}
impl<F> Needle for F
where
F: Fn(&str) -> bool,
{
fn is_match(&self, haystack: &str) -> bool {
self(haystack)
}
}
pub trait StringMatchable: Into<StringMatch> {
fn match_case_sensitive(self) -> StringMatch {
self.into().case_sensitive()
}
fn match_case_insensitive(self) -> StringMatch {
self.into().case_insensitive()
}
fn match_full(self) -> StringMatch {
self.into().full()
}
fn match_word(self) -> StringMatch {
self.into().word()
}
fn match_partial(self) -> StringMatch {
self.into().partial()
}
}
impl StringMatchable for String {}
impl StringMatchable for &str {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stringmatch() {
assert!(StringMatch::from("a").is_full_match());
assert!(!StringMatch::from("a").is_partial_match());
assert!(StringMatch::from("a").is_case_sensitive());
assert!(StringMatch::from("a").is_match("a"));
assert!(!StringMatch::from("a").is_match(""));
assert!(!StringMatch::from("a").is_match("b"));
assert!(!StringMatch::from("a").is_match("A"));
assert!(StringMatch::from("a").partial().is_partial_match());
assert!(!StringMatch::from("a").partial().is_full_match());
assert!(!StringMatch::from("a").partial().is_word_match());
assert!(StringMatch::from("a").partial().is_case_sensitive());
assert!(StringMatch::from("a").partial().is_match("a"));
assert!(StringMatch::from("a").partial().is_match("aa"));
assert!(StringMatch::from("a").partial().is_match("dad"));
assert!(StringMatch::from("a").partial().is_match("ba"));
assert!(!StringMatch::from("A").partial().is_match("a"));
assert!(!StringMatch::from("a").partial().is_match("A"));
assert!(!StringMatch::from("a").case_insensitive().is_case_sensitive());
assert!(!StringMatch::from("a").case_insensitive().is_partial_match());
assert!(StringMatch::from("a").case_insensitive().is_match("a"));
assert!(StringMatch::from("a").case_insensitive().is_match("A"));
assert!(!StringMatch::from("a").case_insensitive().is_match("aa"));
assert!(StringMatch::from("a").partial().case_insensitive().is_partial_match());
assert!(!StringMatch::from("a").partial().case_insensitive().is_case_sensitive());
assert!(StringMatch::from("a").partial().case_insensitive().is_match("a"));
assert!(StringMatch::from("a").partial().case_insensitive().is_match("aa"));
assert!(StringMatch::from("a").partial().case_insensitive().is_match("A"));
assert!(StringMatch::from("a").partial().case_insensitive().is_match("AA"));
assert!(StringMatch::from("aA").partial().case_insensitive().is_match("Aa"));
assert!(StringMatch::from("aA").partial().case_insensitive().is_match("Aaa"));
assert!(StringMatch::from("a").word().is_word_match());
assert!(!StringMatch::from("a").word().is_partial_match());
assert!(!StringMatch::from("a").word().is_full_match());
assert!(StringMatch::from("a").word().is_case_sensitive());
assert!(StringMatch::from("a").word().is_match("a"));
assert!(!StringMatch::from("a").word().is_match("aa"));
assert!(!StringMatch::from("a").word().is_match("dad"));
assert!(!StringMatch::from("a").word().is_match("ba"));
assert!(StringMatch::from("a").word().is_match("aa a aa"));
assert!(StringMatch::from("a").word().is_match("a aa"));
assert!(StringMatch::from("a").word().is_match("aa a"));
assert!(!StringMatch::from("A").word().is_match("a"));
assert!(!StringMatch::from("a").word().is_match("A"));
assert!(StringMatch::from("aaa aa").word().case_insensitive().is_match("aaa aa"));
assert!(StringMatch::from("aaa aa").word().case_insensitive().is_match("aa aaa aa aaa"));
assert!(StringMatch::from("aaa aa").word().case_insensitive().is_match("aaa aa aaa"));
assert!(StringMatch::from("aaa aa").word().case_insensitive().is_match("aa aaa aa"));
assert!(!StringMatch::from("aaa aa").word().case_insensitive().is_match("aa aaa aaa"));
assert!(StringMatch::from("a").word().case_insensitive().is_word_match());
assert!(!StringMatch::from("a").word().case_insensitive().is_case_sensitive());
assert!(StringMatch::from("a").word().case_insensitive().is_match("a"));
assert!(!StringMatch::from("a").word().case_insensitive().is_match("aa"));
assert!(StringMatch::from("a").word().case_insensitive().is_match("A"));
assert!(StringMatch::from("a").word().case_insensitive().is_match("AA A AA"));
assert!(StringMatch::from("aA").word().case_insensitive().is_match("Aa"));
assert!(StringMatch::from("aA").word().case_insensitive().is_match("aa"));
assert!(StringMatch::from("A").word().case_insensitive().is_match("aa a aa"));
assert!(StringMatch::from("A").word().case_insensitive().is_match("a aa"));
assert!(StringMatch::from("A").word().case_insensitive().is_match("aa a"));
assert!(StringMatch::from("AAA AA").word().case_insensitive().is_match("aaa aa"));
assert!(StringMatch::from("AAA AA").word().case_insensitive().is_match("aa aaa aa aaa"));
assert!(StringMatch::from("AAA AA").word().case_insensitive().is_match("aaa aa aaa"));
assert!(StringMatch::from("AAA AA").word().case_insensitive().is_match("aa aaa aa"));
assert!(!StringMatch::from("AAA AA").word().case_insensitive().is_match("aa aaa aaa"));
}
#[test]
fn test_stringmatchable() {
assert_eq!("a".match_full(), StringMatch::new("a").full());
assert_eq!("a".match_partial(), StringMatch::new("a").partial());
assert_eq!("a".match_word(), StringMatch::new("a").word());
assert_eq!("a".match_case_insensitive(), StringMatch::new("a").case_insensitive());
assert_eq!("a".match_case_sensitive(), StringMatch::new("a").case_sensitive());
assert_eq!(String::from("a").match_full(), StringMatch::new("a").full());
assert_eq!(String::from("a").match_partial(), StringMatch::new("a").partial());
assert_eq!(String::from("a").match_word(), StringMatch::new("a").word());
assert_eq!(
String::from("a").match_case_insensitive(),
StringMatch::new("a").case_insensitive()
);
assert_eq!(
String::from("a").match_case_sensitive(),
StringMatch::new("a").case_sensitive()
);
}
fn needle_is_match<N>(needle: N) -> bool
where
N: Needle,
{
needle.is_match("Test")
}
#[test]
fn test_needle() {
assert!(needle_is_match("Test"));
assert!(!needle_is_match("test")); assert!(!needle_is_match("Te")); assert!(needle_is_match(String::from("Test")));
assert!(!needle_is_match(String::from("test"))); assert!(!needle_is_match(String::from("Te"))); assert!(needle_is_match(StringMatch::from("Test")));
assert!(!needle_is_match(StringMatch::from("test")));
assert!(needle_is_match(StringMatch::from("test").case_insensitive()));
assert!(needle_is_match(StringMatch::from("Te").partial()));
assert!(!needle_is_match(StringMatch::from("te").partial()));
assert!(needle_is_match(StringMatch::from("te").partial().case_insensitive()));
assert!(needle_is_match(Regex::new("Test").unwrap()));
assert!(needle_is_match(Regex::new("Te").unwrap())); assert!(!needle_is_match(Regex::new("te").unwrap())); assert!(needle_is_match(Regex::new(r"(?i)te").unwrap())); assert!(needle_is_match(Regex::new(r"\w+").unwrap()));
assert!(needle_is_match(Regex::new(r"\w").unwrap()));
assert!(!needle_is_match(Regex::new(r"^T$").unwrap()));
assert!(!needle_is_match(Regex::new(r"^est").unwrap()));
assert!(!needle_is_match(Regex::new(r"Te$").unwrap()));
assert!(needle_is_match(Regex::new(r"^T.+t$").unwrap()));
}
fn dynamic_dispatched_needle(needle: &dyn Needle) -> bool {
needle.is_match("Test")
}
#[test]
fn test_dyn_needle() {
assert!(dynamic_dispatched_needle(&"Test"));
assert!(!dynamic_dispatched_needle(&"test")); assert!(!dynamic_dispatched_needle(&"Te")); assert!(dynamic_dispatched_needle(&String::from("Test")));
assert!(!dynamic_dispatched_needle(&String::from("test"))); assert!(!dynamic_dispatched_needle(&String::from("Te"))); assert!(dynamic_dispatched_needle(&StringMatch::from("Test")));
assert!(!dynamic_dispatched_needle(&StringMatch::from("test")));
assert!(dynamic_dispatched_needle(&StringMatch::from("test").case_insensitive()));
assert!(dynamic_dispatched_needle(&StringMatch::from("Te").partial()));
assert!(!dynamic_dispatched_needle(&StringMatch::from("te").partial()));
assert!(dynamic_dispatched_needle(&StringMatch::from("te").partial().case_insensitive()));
assert!(dynamic_dispatched_needle(&Regex::new("Test").unwrap()));
assert!(dynamic_dispatched_needle(&Regex::new("Te").unwrap())); assert!(!dynamic_dispatched_needle(&Regex::new("te").unwrap())); assert!(dynamic_dispatched_needle(&Regex::new(r"(?i)te").unwrap())); assert!(dynamic_dispatched_needle(&Regex::new(r"\w+").unwrap()));
assert!(dynamic_dispatched_needle(&Regex::new(r"\w").unwrap()));
assert!(!dynamic_dispatched_needle(&Regex::new(r"^T$").unwrap()));
assert!(!dynamic_dispatched_needle(&Regex::new(r"^est").unwrap()));
assert!(!dynamic_dispatched_needle(&Regex::new(r"Te$").unwrap()));
assert!(dynamic_dispatched_needle(&Regex::new(r"^T.+t$").unwrap()));
assert!(dynamic_dispatched_needle(&|s: &str| s == "Test"));
assert!(!dynamic_dispatched_needle(&|s: &str| s == "test"));
assert!(!dynamic_dispatched_needle(&|s: &str| s == "Te"));
}
#[cfg(feature = "serde_derive")]
#[test]
fn test_serde() {
let orig = StringMatch::new("a").partial().case_insensitive();
let serialized: String = serde_json::to_string(&orig).unwrap();
let deserialized: StringMatch = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, orig);
}
}