Files
empidee/src/filter.rs
2025-11-21 18:36:54 +09:00

519 lines
16 KiB
Rust

use std::{fmt, str::SplitWhitespace};
use serde::{Deserialize, Serialize};
use crate::{
commands::RequestParserError,
common::{Priority, Tag},
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CaseSensitivity {
CaseSensitive,
CaseInsensitive,
CommandDependent,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ComparisonOperator {
Equal,
NotEqual,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Filter {
Not(Box<Filter>),
And(Box<Filter>, Box<Filter>),
// The bool indicates whether the comparison is negated (true for !=, false for ==)
EqTag(Tag, CaseSensitivity, bool),
Contains(Tag, CaseSensitivity, bool),
StartsWith(Tag, CaseSensitivity, bool),
PerlRegex(Tag),
NegPerlRegex(Tag),
EqUri(String),
Base(String),
ModifiedSince(u64),
AudioFormatEq {
sample_rate: u32,
bits: u8,
channels: u8,
},
AudioFormatEqMask {
sample_rate: Option<u32>,
bits: Option<u8>,
channels: Option<u8>,
},
PrioCmp(ComparisonOperator, Priority),
}
impl fmt::Display for Filter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Filter::Not(inner) => write!(f, "(NOT ({}))", inner),
Filter::And(left, right) => write!(f, "({} AND {})", left, right),
Filter::EqTag(tag, case_sensitivity, negated) => {
let op = if *negated { "!=" } else { "==" };
let cs = match case_sensitivity {
CaseSensitivity::CaseSensitive => "_cs",
CaseSensitivity::CaseInsensitive => "_ci",
CaseSensitivity::CommandDependent => "",
};
write!(f, "({} {}{} '{}')", tag.key(), op, cs, tag.value())
}
Filter::Contains(tag, case_sensitivity, negated) => {
let op = if *negated { "!contains" } else { "contains" };
let cs = match case_sensitivity {
CaseSensitivity::CaseSensitive => "_cs",
CaseSensitivity::CaseInsensitive => "_ci",
CaseSensitivity::CommandDependent => "",
};
write!(f, "({} {}{} '{}')", tag.key(), op, cs, tag.value())
}
Filter::StartsWith(tag, case_sensitivity, negated) => {
let op = if *negated {
"!starts_with"
} else {
"starts_with"
};
let cs = match case_sensitivity {
CaseSensitivity::CaseSensitive => "_cs",
CaseSensitivity::CaseInsensitive => "_ci",
CaseSensitivity::CommandDependent => "",
};
write!(f, "({} {}{} '{}')", tag.key(), op, cs, tag.value())
}
Filter::PerlRegex(tag) => {
write!(f, "({} =~ '{}')", tag.key(), tag.value())
}
Filter::NegPerlRegex(tag) => {
write!(f, "({} !~ '{}')", tag.key(), tag.value())
}
Filter::EqUri(uri) => {
write!(f, "(uri == '{}')", uri)
}
Filter::Base(path) => {
write!(f, "(base '{}')", path)
}
Filter::ModifiedSince(timestamp) => {
write!(f, "(modified-since '{}')", timestamp)
}
Filter::AudioFormatEq {
sample_rate,
bits,
channels,
} => {
write!(
f,
"(AudioFormat == '{}:{}:{}')",
sample_rate, bits, channels
)
}
Filter::AudioFormatEqMask {
sample_rate,
bits,
channels,
} => {
let sr_str = match sample_rate {
Some(sr) => sr.to_string(),
None => "*".to_string(),
};
let bits_str = match bits {
Some(b) => b.to_string(),
None => "*".to_string(),
};
let ch_str = match channels {
Some(c) => c.to_string(),
None => "*".to_string(),
};
write!(f, "(AudioFormat =~ '{}:{}:{}')", sr_str, bits_str, ch_str)
}
Filter::PrioCmp(op, prio) => {
let op_str = match op {
ComparisonOperator::Equal => "==",
ComparisonOperator::NotEqual => "!=",
ComparisonOperator::GreaterThan => ">",
ComparisonOperator::GreaterThanOrEqual => ">=",
ComparisonOperator::LessThan => "<",
ComparisonOperator::LessThanOrEqual => "<=",
};
write!(f, "(prio {} {})", op_str, prio)
}
}
}
}
pub fn parse_filter(parts: &mut SplitWhitespace<'_>) -> Result<Filter, RequestParserError> {
// TODO: count parentheses and extract the full filter string
unimplemented!()
}
fn parse_filter_str(string: &str) -> Result<Filter, RequestParserError> {
unimplemented!()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_filter_eq_tag() {
let mut parts = "(artist == 'The Beatles')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)
);
let mut parts = "(artist != 'The Beatles')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
true,
)
);
}
#[test]
fn test_parse_filter_contains() {
let mut parts = "(album contains 'Greatest Hits')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CommandDependent,
false,
),
);
let mut parts = "(album !contains 'Greatest Hits')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CommandDependent,
true,
),
);
}
#[test]
fn test_parse_filter_starts_with() {
let mut parts = "(title starts_with 'Symphony No. ')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CommandDependent,
false,
),
);
let mut parts = "(title !starts_with 'Symphony No. ')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CommandDependent,
true,
),
);
}
#[test]
fn test_parse_filter_perl_regex_positive() {
let mut parts = "(composer =~ 'Beethoven.*')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::PerlRegex(Tag::Composer("Beethoven.*".to_string())),
);
}
#[test]
fn test_parse_filter_perl_regex_negative() {
let mut parts = "(genre !~ 'Pop.*')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::NegPerlRegex(Tag::Genre("Pop.*".to_string())),
);
}
#[test]
fn test_parse_filter_base() {
let mut parts = "(base 'Rock/Classics')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(filter, Filter::Base("Rock/Classics".to_string()),);
}
#[test]
fn test_parse_filter_modified_since() {
let mut parts = "(modified-since '1622505600')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(filter, Filter::ModifiedSince(1622505600),);
}
#[test]
fn test_parse_filter_audio_added_since() {
let mut parts = "(added-since '1622505600')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(filter, Filter::ModifiedSince(1622505600),);
}
#[test]
fn test_parse_filter_audio_format_eq() {
let mut parts = "(AudioFormat == '44100:16:2')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::AudioFormatEq {
sample_rate: 44100,
bits: 16,
channels: 2,
},
);
}
#[test]
fn test_parse_filter_audio_format_eq_mask() {
let mut parts = "(AudioFormat =~ '44100:*:2')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::AudioFormatEqMask {
sample_rate: Some(44100),
bits: None,
channels: Some(2),
},
);
}
#[test]
fn test_parse_filter_prio_cmp() {
let mut parts = "(prio >= 42)".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::PrioCmp(ComparisonOperator::GreaterThanOrEqual, 42),
);
}
#[test]
fn test_parse_filter_not() {
let mut parts = "(!(artist == 'The Beatles'))".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::Not(Box::new(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
))),
);
}
#[test]
fn test_parse_filter_and() {
let mut parts =
"((artist == 'The Beatles') AND (album == 'Abbey Road'))".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::And(
Box::new(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)),
Box::new(Filter::EqTag(
Tag::Album("Abbey Road".to_string()),
CaseSensitivity::CommandDependent,
false,
)),
),
);
}
#[test]
fn test_parse_filter_explicitly_case_sensitive() {
let mut parts = "(artist eq_cs 'The Beatles')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseSensitive,
false,
),
);
let mut parts = "(artist !eq_cs 'The Beatles')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseSensitive,
true,
),
);
let mut parts = "(album contains_cs 'Greatest Hits')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseSensitive,
false,
),
);
let mut parts = "(album !contains_cs 'Greatest Hits')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseSensitive,
true,
),
);
let mut parts = "(title starts_with_cs 'Symphony No. ')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseSensitive,
false,
),
);
let mut parts = "(title !starts_with_cs 'Symphony No. ')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseSensitive,
true,
),
);
}
#[test]
fn test_parse_filter_explicitly_case_insensitive() {
let mut parts = "(artist eq_ci 'The Beatles')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseInsensitive,
false,
),
);
let mut parts = "(artist !eq_ci 'The Beatles')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseInsensitive,
true,
),
);
let mut parts = "(album contains_ci 'Greatest Hits')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseInsensitive,
false,
),
);
let mut parts = "(album !contains_ci 'Greatest Hits')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseInsensitive,
true,
),
);
let mut parts = "(title starts_with_ci 'Symphony No. ')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseInsensitive,
false,
),
);
let mut parts = "(title !starts_with_ci 'Symphony No. ')".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseInsensitive,
true,
),
);
}
#[test]
fn test_parse_filter_string_escapes() {
let mut parts = "(Artist == \"foo\\'bar\\\"\")".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::EqTag(
Tag::Artist("foo'bar\"".to_string()),
CaseSensitivity::CommandDependent,
false,
),
);
}
#[test]
fn test_parse_filter_excessive_whitespace() {
let mut parts = "(\tartist\n == 'The Beatles' ) ".split_whitespace();
let filter = parse_filter(&mut parts).unwrap();
assert_eq!(
filter,
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)
);
}
}