use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub enum Query {
Exact(String, bool),
StartsWith(String, bool),
Contains(String, bool),
EndsWith(String, bool),
}
impl Query {
pub fn matches(&self, text: &str) -> bool {
match self {
Query::Exact(q, case_sensitive) => {
if *case_sensitive {
text == q
} else {
text.eq_ignore_ascii_case(q)
}
}
Query::StartsWith(q, case_sensitive) => {
if *case_sensitive {
text.starts_with(q)
} else {
text.to_ascii_lowercase().starts_with(&q.to_ascii_lowercase())
}
}
Query::Contains(q, case_sensitive) => {
if *case_sensitive {
text.contains(q)
} else {
text.to_ascii_lowercase().contains(&q.to_ascii_lowercase())
}
}
Query::EndsWith(q, case_sensitive) => {
if *case_sensitive {
text.ends_with(q)
} else {
text.to_ascii_lowercase().ends_with(&q.to_ascii_lowercase())
}
}
}
}
}
impl FromStr for Query {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(());
}
let (remaining, case_sensitive) = if let Some(rest) = trimmed.strip_prefix("c:") {
(rest, true)
} else if let Some(rest) = trimmed.strip_prefix("i:") {
(rest, false)
} else {
(trimmed, false) };
let lower = remaining.to_ascii_lowercase();
if lower.is_empty() {
return Err(());
}
if let Some(rest) = remaining.strip_prefix('=') {
Ok(Query::Exact(rest.to_string(), case_sensitive))
}
else if let Some(rest) = remaining.strip_prefix('^') {
Ok(Query::StartsWith(rest.to_string(), case_sensitive))
}
else if lower.ends_with('$') {
let query_str = &remaining[..remaining.len() - 1];
Ok(Query::EndsWith(query_str.to_string(), case_sensitive))
}
else {
Ok(Query::Contains(remaining.to_string(), case_sensitive))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_case_sensitive_exact() {
let query = Query::from_str("c:=Hello").unwrap();
match &query {
Query::Exact(s, true) => assert_eq!(s, "Hello"),
_ => panic!("Expected case-sensitive Exact variant"),
}
assert!(query.matches("Hello"));
assert!(!query.matches("hello"));
}
#[test]
fn test_case_insensitive_default() {
let query = Query::from_str("=Hello").unwrap();
match &query {
Query::Exact(s, false) => assert_eq!(s, "Hello"),
_ => panic!("Expected case-insensitive Exact variant"),
}
assert!(query.matches("hello"));
assert!(query.matches("HELLO"));
}
#[test]
fn test_starts_with_case_sensitive() {
let query = Query::from_str("c:^Hell").unwrap();
match &query {
Query::StartsWith(s, true) => assert_eq!(s, "Hell"),
_ => panic!("Expected case-sensitive StartsWith variant"),
}
assert!(query.matches("Hellworld"));
assert!(!query.matches("helloworld")); }
#[test]
fn test_contains_case_insensitive() {
let query = Query::from_str("CoNtEnT").unwrap();
match &query {
Query::Contains(s, false) => assert_eq!(s, "CoNtEnT"),
_ => panic!("Expected case-insensitive Contains variant"),
}
assert!(query.matches("this content is here"));
assert!(query.matches("CONTENT!"));
}
#[test]
fn test_ends_with() {
let query = Query::from_str("something$").unwrap();
match &query {
Query::EndsWith(s, false) => assert_eq!(s, "something"),
_ => panic!("Expected ends-with, case-insensitive"),
}
assert!(query.matches("ANYTHING someThing"));
}
#[test]
fn test_empty_string_error() {
assert!(Query::from_str(" ").is_err());
}
}