#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::Matching;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Pattern {
pub matching: Option<Matching>,
pub pattern: String,
pub negated: bool,
pub needle: String,
}
impl From<&str> for Pattern {
fn from(needle: &str) -> Self {
Pattern::new(needle, None, false)
}
}
impl From<String> for Pattern {
fn from(needle: String) -> Self {
needle.as_str().into()
}
}
impl From<&String> for Pattern {
fn from(needle: &String) -> Self {
needle.as_str().into()
}
}
impl Pattern {
pub fn new(needle: &str, matching: Option<Matching>, negated: bool) -> Self {
Self {
matching,
pattern: needle.to_string(),
negated,
needle: needle.to_string(),
}
}
pub fn parse(atom: &str) -> Self {
let mut needle = atom;
let mut negated = false;
let mut prefix = false;
let mut suffix = false;
let mut substring = false;
if let Some(rest) = needle.strip_prefix('!') {
needle = rest;
negated = true;
}
if needle.starts_with("\\!") {
needle = &needle[1..]; }
if let Some(rest) = needle.strip_prefix('^') {
needle = rest;
prefix = true;
} else if let Some(rest) = needle.strip_prefix('\'') {
needle = rest;
substring = true;
} else if needle.starts_with("\\^") || needle.starts_with("\\'") {
needle = &needle[1..]; }
let needle = if let Some(head) = needle.strip_suffix("\\$") {
format!("{head}$") } else if let Some(head) = needle.strip_suffix('$') {
suffix = true;
head.to_string()
} else {
needle.to_string()
};
let needle = needle.replace("\\ ", " ");
let matching = match (prefix, suffix, substring) {
(true, true, _) => Some(Matching::Exact),
(true, false, _) => Some(Matching::Prefix),
(false, true, _) => Some(Matching::Suffix),
(false, false, true) => Some(Matching::Substring),
_ if negated => Some(Matching::Substring),
_ => None,
};
Self {
matching,
pattern: atom.to_string(),
negated,
needle,
}
}
pub fn parse_query(query: &str) -> Vec<Pattern> {
let mut patterns = vec![];
let mut start: Option<usize> = None;
let mut prev = '\0';
let mut push = |atom: &str| {
let pattern = Self::parse(atom);
if !pattern.needle.is_empty() {
patterns.push(pattern);
}
};
for (i, c) in query.char_indices() {
if c.is_whitespace() && prev != '\\' {
if let Some(s) = start.take() {
push(&query[s..i]);
}
} else if start.is_none() {
start = Some(i);
}
prev = c;
}
if let Some(s) = start {
push(&query[s..]);
}
patterns
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_parse(atom: &str, needle: &str, matching: Option<Matching>, negated: bool) {
let pattern = Pattern::parse(atom);
assert_eq!(pattern.pattern, atom);
assert_eq!(pattern.needle, needle, "atom: {atom:?}");
assert_eq!(pattern.matching, matching, "atom: {atom:?}");
assert_eq!(pattern.negated, negated, "atom: {atom:?}");
}
#[test]
fn parse_selects_matching_mode() {
assert_parse("foo", "foo", None, false);
assert_parse("^foo", "foo", Some(Matching::Prefix), false);
assert_parse("foo$", "foo", Some(Matching::Suffix), false);
assert_parse("'foo", "foo", Some(Matching::Substring), false);
assert_parse("^foo$", "foo", Some(Matching::Exact), false);
}
#[test]
fn parse_negation() {
assert_parse("!foo", "foo", Some(Matching::Substring), true);
assert_parse("!^foo", "foo", Some(Matching::Prefix), true);
assert_parse("!foo$", "foo", Some(Matching::Suffix), true);
assert_parse("!'foo", "foo", Some(Matching::Substring), true);
assert_parse("!^foo$", "foo", Some(Matching::Exact), true);
}
#[test]
fn parse_escapes_special_syntax() {
assert_parse("\\^foo", "^foo", None, false);
assert_parse("foo\\$", "foo$", None, false);
assert_parse("\\'foo", "'foo", None, false);
assert_parse("\\!foo", "!foo", None, false);
assert_parse("foo\\ bar", "foo bar", None, false);
assert_parse("!\\^foo", "^foo", Some(Matching::Substring), true);
assert_parse("!\\!foo", "!foo", Some(Matching::Substring), true);
}
#[test]
fn parse_query_splits_atoms() {
let patterns = Pattern::parse_query("foo !^bar");
assert_eq!(patterns.len(), 2);
assert_eq!(patterns[0], Pattern::parse("foo"));
assert_eq!(patterns[1], Pattern::parse("!^bar"));
let patterns = Pattern::parse_query(" foo \t bar ");
assert_eq!(patterns.len(), 2);
assert_eq!(patterns[0].needle, "foo");
assert_eq!(patterns[1].needle, "bar");
}
#[test]
fn parse_query_escaped_space() {
let patterns = Pattern::parse_query("foo\\ bar baz");
assert_eq!(patterns.len(), 2);
assert_eq!(patterns[0].needle, "foo bar");
assert_eq!(patterns[1].needle, "baz");
}
#[test]
fn parse_query_drops_empty_atoms() {
assert!(Pattern::parse_query("").is_empty());
assert!(Pattern::parse_query(" ").is_empty());
assert!(Pattern::parse_query("! ^$ '").is_empty());
}
}