use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use std::ops::Not;
use crate::command::{escape_argument, Argument};
pub static ANY: &str = "any";
pub static IS_ABSENT: &str = "";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Filter(FilterType);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Operator {
Equal,
NotEqual,
Contain,
Match,
NotMatch,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FilterError {
EmptyTag,
}
impl Error for FilterError {}
impl fmt::Display for FilterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FilterError::EmptyTag => write!(f, "Attmpted to construct a filter for an empty tag"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum FilterType {
Tag {
tag: Cow<'static, str>,
operator: Operator,
value: Cow<'static, str>,
},
Not(Box<FilterType>),
And(Vec<FilterType>),
}
impl Filter {
pub fn tag(
tag: impl Into<Cow<'static, str>>,
operator: Operator,
value: impl Into<Cow<'static, str>>,
) -> Result<Self, FilterError> {
let tag = tag.into();
if tag.is_empty() {
Err(FilterError::EmptyTag)
} else {
Ok(Filter(FilterType::Tag {
tag,
operator,
value: value.into(),
}))
}
}
pub fn equal(tag: impl Into<Cow<'static, str>>, value: impl Into<Cow<'static, str>>) -> Self {
Filter::tag(tag, Operator::Equal, value).expect("Invalid filter expression")
}
pub fn negate(mut self) -> Self {
self.0 = FilterType::Not(Box::new(self.0));
self
}
pub fn and(self, other: Self) -> Self {
let mut out = match self.0 {
FilterType::And(inner) => inner,
condition => {
let mut out = Vec::with_capacity(2);
out.push(condition);
out
}
};
match other.0 {
FilterType::And(inner) => {
for c in inner {
out.push(c);
}
}
condition => out.push(condition),
}
Self(FilterType::And(out))
}
}
impl Argument for Filter {
fn render(self) -> Cow<'static, str> {
Cow::Owned(self.0.render())
}
}
impl Not for Filter {
type Output = Self;
fn not(self) -> Self::Output {
self.negate()
}
}
impl Operator {
fn to_str(self) -> &'static str {
match self {
Operator::Equal => "==",
Operator::NotEqual => "!=",
Operator::Contain => "contains",
Operator::Match => "=~",
Operator::NotMatch => "!~",
}
}
}
impl FilterType {
fn render(self) -> String {
match self {
FilterType::Tag {
tag,
operator,
value,
} => format!(
"({} {} \"{}\")",
tag,
operator.to_str(),
escape_argument(&value)
),
FilterType::Not(inner) => format!("(!{})", inner.render()),
FilterType::And(inner) => {
assert!(inner.len() >= 2);
let inner = inner.into_iter().map(|s| s.render()).collect::<Vec<_>>();
let mut capacity = 2;
capacity += inner.iter().map(|s| s.len()).sum::<usize>();
capacity += (inner.len() - 1) * 5;
let mut out = String::with_capacity(capacity);
out.push('(');
let mut first = true;
for filter in inner {
if first {
first = false;
} else {
out.push_str(" AND ");
}
out.push_str(&filter);
}
out.push(')');
out
}
}
}
}
#[cfg(test)]
mod tests {
use super::{Argument, Filter, FilterError, Operator};
#[test]
fn filter_simple_equal() {
assert_eq!(
Filter::equal("artist", "foo\'s bar\"").render(),
"(artist == \"foo\\\'s bar\\\"\")"
);
}
#[test]
fn filter_other_operator() {
assert_eq!(
Filter::tag("artist", Operator::Contain, "mep mep")
.unwrap()
.render(),
"(artist contains \"mep mep\")"
);
}
#[test]
fn filter_empty_value() {
assert_eq!(
Filter::tag("", Operator::Equal, "mep mep").unwrap_err(),
FilterError::EmptyTag,
);
}
#[test]
fn filter_not() {
assert_eq!(
Filter::equal("artist", "hello").negate().render(),
"(!(artist == \"hello\"))"
);
}
#[test]
fn filter_and() {
let first = Filter::equal("artist", "hello");
let second = Filter::equal("album", "world");
assert_eq!(
first.and(second).render(),
"((artist == \"hello\") AND (album == \"world\"))"
);
}
#[test]
fn filter_and_multiple() {
let first = Filter::equal("artist", "hello");
let second = Filter::equal("album", "world");
let third = Filter::equal("title", "foo");
assert_eq!(
first.and(second).and(third).render(),
"((artist == \"hello\") AND (album == \"world\") AND (title == \"foo\"))"
);
}
}