use std::borrow::Cow;
use std::ops::Not;
use mpd_protocol::command::{escape_argument, Argument};
use crate::Tag;
const TAG_IS_ABSENT: &str = "";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Filter(FilterType);
#[derive(Clone, Debug, PartialEq, Eq)]
enum FilterType {
Tag {
tag: Tag,
operator: Operator,
value: Cow<'static, str>,
},
Not(Box<FilterType>),
And(Vec<FilterType>),
}
impl Filter {
pub fn new(tag: Tag, operator: Operator, value: impl Into<Cow<'static, str>>) -> Self {
Self(FilterType::Tag {
tag,
operator,
value: value.into(),
})
}
pub fn tag(tag: Tag, value: impl Into<Cow<'static, str>>) -> Self {
Filter::new(tag, Operator::Equal, value)
}
pub fn tag_exists(tag: Tag) -> Self {
Filter::new(tag, Operator::NotEqual, TAG_IS_ABSENT)
}
pub fn tag_absent(tag: Tag) -> Self {
Filter::new(tag, Operator::Equal, TAG_IS_ABSENT)
}
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 FilterType {
fn render(self) -> String {
match self {
FilterType::Tag {
tag,
operator,
value,
} => format!(
"({} {} \"{}\")",
tag.as_str(),
operator.as_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
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Operator {
Equal,
NotEqual,
Contain,
Match,
NotMatch,
}
impl Operator {
fn as_str(&self) -> &'static str {
match self {
Operator::Equal => "==",
Operator::NotEqual => "!=",
Operator::Contain => "contains",
Operator::Match => "=~",
Operator::NotMatch => "!~",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_simple_equal() {
assert_eq!(
Filter::tag(Tag::Artist, "foo\'s bar\"").render(),
"(Artist == \"foo\\\'s bar\\\"\")"
);
}
#[test]
fn filter_other_operator() {
assert_eq!(
Filter::new(Tag::Artist, Operator::Contain, "mep mep").render(),
"(Artist contains \"mep mep\")"
);
}
#[test]
fn filter_not() {
assert_eq!(
Filter::tag(Tag::Artist, "hello").negate().render(),
"(!(Artist == \"hello\"))"
);
}
#[test]
fn filter_and() {
let first = Filter::tag(Tag::Artist, "hello");
let second = Filter::tag(Tag::Album, "world");
assert_eq!(
first.and(second).render(),
"((Artist == \"hello\") AND (Album == \"world\"))"
);
}
#[test]
fn filter_and_multiple() {
let first = Filter::tag(Tag::Artist, "hello");
let second = Filter::tag(Tag::Album, "world");
let third = Filter::tag(Tag::Title, "foo");
assert_eq!(
first.and(second).and(third).render(),
"((Artist == \"hello\") AND (Album == \"world\") AND (Title == \"foo\"))"
);
}
}