filter: flatten module
Some checks failed
Build and test / check (push) Failing after 1m2s
Build and test / test (push) Failing after 1m17s
Build and test / build (push) Failing after 1m23s
Build and test / docs (push) Failing after 1m9s

This commit is contained in:
2025-12-08 13:32:18 +09:00
parent a7a8ceedeb
commit 86edd4c5b3
3 changed files with 625 additions and 628 deletions

View File

@@ -1,3 +1,625 @@
mod filter;
use std::fmt;
pub use filter::*;
use chrono::{DateTime, Utc};
use lalrpop_util::lalrpop_mod;
use serde::{Deserialize, Serialize};
use crate::{
commands::RequestParserError,
types::{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(Vec<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, bool),
EqUri(String),
Base(String),
AddedSince(DateTime<Utc>),
ModifiedSince(DateTime<Utc>),
AudioFormatEq {
sample_rate: u32,
bits: u8,
channels: u8,
},
AudioFormatEqMask {
sample_rate: Option<u32>,
bits: Option<u8>,
channels: Option<u8>,
},
PrioCmp(ComparisonOperator, Priority),
}
fn quote_string(s: &str) -> String {
let mut result = String::new();
result.push('"');
for c in s.chars() {
match c {
'\n' => result.push_str(r"\n"),
'\t' => result.push_str(r"\t"),
'\r' => result.push_str(r"\r"),
'\\' => result.push_str(r"\\"),
'"' => result.push_str(r#"\""#),
other => result.push(other),
}
}
result.push('"');
result
}
impl fmt::Display for Filter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Filter::Not(inner) => write!(f, "(!{})", inner),
Filter::And(inner) => write!(
f,
"({})",
inner
.iter()
.map(|f| format!("{}", f))
.collect::<Vec<_>>()
.join(" AND "),
),
Filter::EqTag(tag, case_sensitivity, negated) => {
let op = match (negated, case_sensitivity) {
(false, CaseSensitivity::CommandDependent) => "==",
(true, CaseSensitivity::CommandDependent) => "!=",
(false, CaseSensitivity::CaseSensitive) => "eq_cs",
(true, CaseSensitivity::CaseSensitive) => "!eq_cs",
(false, CaseSensitivity::CaseInsensitive) => "eq_ci",
(true, CaseSensitivity::CaseInsensitive) => "!eq_ci",
};
write!(f, "({} {} {})", tag.key(), op, quote_string(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,
quote_string(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,
quote_string(tag.value())
)
}
Filter::PerlRegex(tag, negated) => {
if *negated {
write!(f, "({} !~ {})", tag.key(), quote_string(tag.value()))
} else {
write!(f, "({} =~ {})", tag.key(), quote_string(tag.value()))
}
}
Filter::EqUri(uri) => {
write!(f, "(uri == {})", quote_string(uri))
}
Filter::Base(path) => {
write!(f, "(base {})", quote_string(path))
}
Filter::ModifiedSince(timestamp) => {
write!(
f,
"(modified-since {})",
quote_string(&timestamp.timestamp().to_string())
)
}
Filter::AddedSince(timestamp) => {
write!(
f,
"(added-since {})",
quote_string(&timestamp.timestamp().to_string())
)
}
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(super) fn unescape_string(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(escaped) = chars.next() {
match escaped {
'n' => result.push('\n'),
't' => result.push('\t'),
'r' => result.push('\r'),
'\\' => result.push('\\'),
'"' => result.push('"'),
other => {
result.push('\\');
result.push(other);
}
}
} else {
result.push('\\');
}
} else {
result.push(c);
}
}
result
}
lalrpop_mod!(filter_grammar, "/filter_grammar.rs");
impl Filter {
pub fn parse(input: &str) -> Result<Filter, RequestParserError> {
let parser = filter_grammar::ExpressionParser::new();
// println!("Parsing filter: {:#?}", parser.parse(input));
parser
.parse(input)
.map_err(|e| RequestParserError::SyntaxError(0, format!("{}", e)))
}
}
// TODO: There is a significant amount of error handling to be improved and tested here.
#[derive(Debug, Clone, PartialEq)]
pub enum FilterParserError<'a> {
/// Could not parse the response due to a syntax error.
SyntaxError(usize, &'a str),
/// Audio format string is invalid.
InvalidAudioFormat,
/// Parentheses are unbalanced.
UnbalancedParentheses,
/// String literal is not closed.
UnclosedStringLiteral,
}
#[cfg(test)]
mod tests {
use chrono::DateTime;
use super::*;
#[test]
fn test_parse_filter_eq_tag() {
let filter = Filter::parse(r#"(artist == "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)),
);
let filter = Filter::parse(r#"(artist != "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
true,
)),
);
}
#[test]
fn test_parse_filter_contains() {
let filter = Filter::parse(r#"(album contains "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CommandDependent,
false,
))
);
let filter = Filter::parse(r#"(album !contains "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CommandDependent,
true,
)),
);
}
#[test]
fn test_parse_filter_starts_with() {
let filter = Filter::parse(r#"(title starts_with "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CommandDependent,
false,
)),
);
let filter = Filter::parse(r#"(title !starts_with "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CommandDependent,
true,
)),
);
}
#[test]
fn test_parse_filter_perl_regex_positive() {
let filter = Filter::parse(r#"(composer =~ "Beethoven.*")"#);
assert_eq!(
filter,
Ok(Filter::PerlRegex(
Tag::Composer("Beethoven.*".to_string()),
false,
)),
);
let filter = Filter::parse(r#"(genre !~ "Pop.*")"#);
assert_eq!(
filter,
Ok(Filter::PerlRegex(Tag::Genre("Pop.*".to_string()), true)),
);
}
#[test]
fn test_parse_filter_base() {
let filter = Filter::parse(r#"(base "Rock/Classics")"#);
assert_eq!(filter, Ok(Filter::Base("Rock/Classics".to_string())));
}
#[test]
fn test_parse_filter_modified_since() {
let filter = Filter::parse(r#"(modified-since '1622505600')"#);
assert_eq!(
filter,
Ok(Filter::ModifiedSince(
DateTime::from_timestamp(1622505600, 0).unwrap()
))
);
let date_time = DateTime::from_timestamp(1622505600, 0).unwrap();
let iso8601_str = date_time.to_rfc3339();
let filter = Filter::parse(format!(r#"(modified-since "{}")"#, iso8601_str).as_str());
assert_eq!(filter, Ok(Filter::ModifiedSince(date_time)));
}
#[test]
fn test_parse_filter_audio_added_since() {
let filter = Filter::parse(r#"(added-since '1622505600')"#);
assert_eq!(
filter,
Ok(Filter::AddedSince(
DateTime::from_timestamp(1622505600, 0).unwrap()
))
);
let date_time = DateTime::from_timestamp(1622505600, 0).unwrap();
let iso8601_str = date_time.to_rfc3339();
let filter = Filter::parse(format!(r#"(added-since "{}")"#, iso8601_str).as_str());
assert_eq!(filter, Ok(Filter::AddedSince(date_time)));
}
#[test]
fn test_parse_filter_audio_format_eq() {
let filter = Filter::parse(r#"(AudioFormat == 44100:16:2)"#);
assert_eq!(
filter,
Ok(Filter::AudioFormatEq {
sample_rate: 44100,
bits: 16,
channels: 2,
}),
);
}
#[test]
fn test_parse_filter_audio_format_eq_mask() {
let filter = Filter::parse(r#"(AudioFormat =~ 44100:*:2)"#);
assert_eq!(
filter,
Ok(Filter::AudioFormatEqMask {
sample_rate: Some(44100),
bits: None,
channels: Some(2),
}),
);
}
#[test]
fn test_parse_filter_prio_cmp() {
for (op_str, op_enum) in &[
(">", ComparisonOperator::GreaterThan),
(">=", ComparisonOperator::GreaterThanOrEqual),
("<", ComparisonOperator::LessThan),
("<=", ComparisonOperator::LessThanOrEqual),
("==", ComparisonOperator::Equal),
("!=", ComparisonOperator::NotEqual),
] {
let filter_str = format!(r#"(prio {} 42)"#, op_str);
let filter = Filter::parse(&filter_str);
assert_eq!(filter, Ok(Filter::PrioCmp(op_enum.clone(), 42)),);
}
}
#[test]
fn test_parse_filter_not() {
let filter = Filter::parse(r#"(!(artist == "The Beatles"))"#);
assert_eq!(
filter,
Ok(Filter::Not(Box::new(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)))),
);
}
#[test]
fn test_parse_filter_and() {
let filter = Filter::parse(
r#"((artist == "The Beatles") AND (album == "Abbey Road") AND (title == "Come Together"))"#,
);
assert_eq!(
filter,
Ok(Filter::And(vec![
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
),
Filter::EqTag(
Tag::Album("Abbey Road".to_string()),
CaseSensitivity::CommandDependent,
false,
),
Filter::EqTag(
Tag::Title("Come Together".to_string()),
CaseSensitivity::CommandDependent,
false,
),
])),
);
}
#[test]
fn test_parse_filter_explicitly_case_sensitive() {
let filter = Filter::parse(r#"(artist eq_cs "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseSensitive,
false,
)),
);
let filter = Filter::parse(r#"(artist !eq_cs "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseSensitive,
true,
)),
);
let filter = Filter::parse(r#"(album contains_cs "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseSensitive,
false,
)),
);
let filter = Filter::parse(r#"(album !contains_cs "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseSensitive,
true,
)),
);
let filter = Filter::parse(r#"(title starts_with_cs "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseSensitive,
false,
)),
);
let filter = Filter::parse(r#"(title !starts_with_cs "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseSensitive,
true,
)),
);
}
#[test]
fn test_parse_filter_explicitly_case_insensitive() {
let filter = Filter::parse(r#"(artist eq_ci "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseInsensitive,
false,
)),
);
let filter = Filter::parse(r#"(artist !eq_ci "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseInsensitive,
true,
)),
);
let filter = Filter::parse(r#"(album contains_ci "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseInsensitive,
false,
)),
);
let filter = Filter::parse(r#"(album !contains_ci "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseInsensitive,
true,
)),
);
let filter = Filter::parse(r#"(title starts_with_ci "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseInsensitive,
false,
)),
);
let filter = Filter::parse(r#"(title !starts_with_ci "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseInsensitive,
true,
)),
);
}
#[test]
fn test_parse_filter_string_escapes() {
let filter = Filter::parse(r#"(Artist == "\"foo\\'bar\\\"")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist(r#""foo\'bar\""#.to_string()),
CaseSensitivity::CommandDependent,
false,
)),
);
}
#[test]
fn test_parse_filter_excessive_whitespace() {
let filter = Filter::parse("(\tartist\n == \"The Beatles\" ) ");
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)),
);
}
}

View File

@@ -1,625 +0,0 @@
use std::fmt;
use chrono::{DateTime, Utc};
use lalrpop_util::lalrpop_mod;
use serde::{Deserialize, Serialize};
use crate::{
commands::RequestParserError,
types::{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(Vec<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, bool),
EqUri(String),
Base(String),
AddedSince(DateTime<Utc>),
ModifiedSince(DateTime<Utc>),
AudioFormatEq {
sample_rate: u32,
bits: u8,
channels: u8,
},
AudioFormatEqMask {
sample_rate: Option<u32>,
bits: Option<u8>,
channels: Option<u8>,
},
PrioCmp(ComparisonOperator, Priority),
}
fn quote_string(s: &str) -> String {
let mut result = String::new();
result.push('"');
for c in s.chars() {
match c {
'\n' => result.push_str(r"\n"),
'\t' => result.push_str(r"\t"),
'\r' => result.push_str(r"\r"),
'\\' => result.push_str(r"\\"),
'"' => result.push_str(r#"\""#),
other => result.push(other),
}
}
result.push('"');
result
}
impl fmt::Display for Filter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Filter::Not(inner) => write!(f, "(!{})", inner),
Filter::And(inner) => write!(
f,
"({})",
inner
.iter()
.map(|f| format!("{}", f))
.collect::<Vec<_>>()
.join(" AND "),
),
Filter::EqTag(tag, case_sensitivity, negated) => {
let op = match (negated, case_sensitivity) {
(false, CaseSensitivity::CommandDependent) => "==",
(true, CaseSensitivity::CommandDependent) => "!=",
(false, CaseSensitivity::CaseSensitive) => "eq_cs",
(true, CaseSensitivity::CaseSensitive) => "!eq_cs",
(false, CaseSensitivity::CaseInsensitive) => "eq_ci",
(true, CaseSensitivity::CaseInsensitive) => "!eq_ci",
};
write!(f, "({} {} {})", tag.key(), op, quote_string(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,
quote_string(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,
quote_string(tag.value())
)
}
Filter::PerlRegex(tag, negated) => {
if *negated {
write!(f, "({} !~ {})", tag.key(), quote_string(tag.value()))
} else {
write!(f, "({} =~ {})", tag.key(), quote_string(tag.value()))
}
}
Filter::EqUri(uri) => {
write!(f, "(uri == {})", quote_string(uri))
}
Filter::Base(path) => {
write!(f, "(base {})", quote_string(path))
}
Filter::ModifiedSince(timestamp) => {
write!(
f,
"(modified-since {})",
quote_string(&timestamp.timestamp().to_string())
)
}
Filter::AddedSince(timestamp) => {
write!(
f,
"(added-since {})",
quote_string(&timestamp.timestamp().to_string())
)
}
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(super) fn unescape_string(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(escaped) = chars.next() {
match escaped {
'n' => result.push('\n'),
't' => result.push('\t'),
'r' => result.push('\r'),
'\\' => result.push('\\'),
'"' => result.push('"'),
other => {
result.push('\\');
result.push(other);
}
}
} else {
result.push('\\');
}
} else {
result.push(c);
}
}
result
}
lalrpop_mod!(filter_grammar, "/filter/filter_grammar.rs");
impl Filter {
pub fn parse(input: &str) -> Result<Filter, RequestParserError> {
let parser = filter_grammar::ExpressionParser::new();
// println!("Parsing filter: {:#?}", parser.parse(input));
parser
.parse(input)
.map_err(|e| RequestParserError::SyntaxError(0, format!("{}", e)))
}
}
// TODO: There is a significant amount of error handling to be improved and tested here.
#[derive(Debug, Clone, PartialEq)]
pub enum FilterParserError<'a> {
/// Could not parse the response due to a syntax error.
SyntaxError(usize, &'a str),
/// Audio format string is invalid.
InvalidAudioFormat,
/// Parentheses are unbalanced.
UnbalancedParentheses,
/// String literal is not closed.
UnclosedStringLiteral,
}
#[cfg(test)]
mod tests {
use chrono::DateTime;
use super::*;
#[test]
fn test_parse_filter_eq_tag() {
let filter = Filter::parse(r#"(artist == "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)),
);
let filter = Filter::parse(r#"(artist != "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
true,
)),
);
}
#[test]
fn test_parse_filter_contains() {
let filter = Filter::parse(r#"(album contains "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CommandDependent,
false,
))
);
let filter = Filter::parse(r#"(album !contains "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CommandDependent,
true,
)),
);
}
#[test]
fn test_parse_filter_starts_with() {
let filter = Filter::parse(r#"(title starts_with "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CommandDependent,
false,
)),
);
let filter = Filter::parse(r#"(title !starts_with "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CommandDependent,
true,
)),
);
}
#[test]
fn test_parse_filter_perl_regex_positive() {
let filter = Filter::parse(r#"(composer =~ "Beethoven.*")"#);
assert_eq!(
filter,
Ok(Filter::PerlRegex(
Tag::Composer("Beethoven.*".to_string()),
false,
)),
);
let filter = Filter::parse(r#"(genre !~ "Pop.*")"#);
assert_eq!(
filter,
Ok(Filter::PerlRegex(Tag::Genre("Pop.*".to_string()), true)),
);
}
#[test]
fn test_parse_filter_base() {
let filter = Filter::parse(r#"(base "Rock/Classics")"#);
assert_eq!(filter, Ok(Filter::Base("Rock/Classics".to_string())));
}
#[test]
fn test_parse_filter_modified_since() {
let filter = Filter::parse(r#"(modified-since '1622505600')"#);
assert_eq!(
filter,
Ok(Filter::ModifiedSince(
DateTime::from_timestamp(1622505600, 0).unwrap()
))
);
let date_time = DateTime::from_timestamp(1622505600, 0).unwrap();
let iso8601_str = date_time.to_rfc3339();
let filter = Filter::parse(format!(r#"(modified-since "{}")"#, iso8601_str).as_str());
assert_eq!(filter, Ok(Filter::ModifiedSince(date_time)));
}
#[test]
fn test_parse_filter_audio_added_since() {
let filter = Filter::parse(r#"(added-since '1622505600')"#);
assert_eq!(
filter,
Ok(Filter::AddedSince(
DateTime::from_timestamp(1622505600, 0).unwrap()
))
);
let date_time = DateTime::from_timestamp(1622505600, 0).unwrap();
let iso8601_str = date_time.to_rfc3339();
let filter = Filter::parse(format!(r#"(added-since "{}")"#, iso8601_str).as_str());
assert_eq!(filter, Ok(Filter::AddedSince(date_time)));
}
#[test]
fn test_parse_filter_audio_format_eq() {
let filter = Filter::parse(r#"(AudioFormat == 44100:16:2)"#);
assert_eq!(
filter,
Ok(Filter::AudioFormatEq {
sample_rate: 44100,
bits: 16,
channels: 2,
}),
);
}
#[test]
fn test_parse_filter_audio_format_eq_mask() {
let filter = Filter::parse(r#"(AudioFormat =~ 44100:*:2)"#);
assert_eq!(
filter,
Ok(Filter::AudioFormatEqMask {
sample_rate: Some(44100),
bits: None,
channels: Some(2),
}),
);
}
#[test]
fn test_parse_filter_prio_cmp() {
for (op_str, op_enum) in &[
(">", ComparisonOperator::GreaterThan),
(">=", ComparisonOperator::GreaterThanOrEqual),
("<", ComparisonOperator::LessThan),
("<=", ComparisonOperator::LessThanOrEqual),
("==", ComparisonOperator::Equal),
("!=", ComparisonOperator::NotEqual),
] {
let filter_str = format!(r#"(prio {} 42)"#, op_str);
let filter = Filter::parse(&filter_str);
assert_eq!(filter, Ok(Filter::PrioCmp(op_enum.clone(), 42)),);
}
}
#[test]
fn test_parse_filter_not() {
let filter = Filter::parse(r#"(!(artist == "The Beatles"))"#);
assert_eq!(
filter,
Ok(Filter::Not(Box::new(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)))),
);
}
#[test]
fn test_parse_filter_and() {
let filter = Filter::parse(
r#"((artist == "The Beatles") AND (album == "Abbey Road") AND (title == "Come Together"))"#,
);
assert_eq!(
filter,
Ok(Filter::And(vec![
Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
),
Filter::EqTag(
Tag::Album("Abbey Road".to_string()),
CaseSensitivity::CommandDependent,
false,
),
Filter::EqTag(
Tag::Title("Come Together".to_string()),
CaseSensitivity::CommandDependent,
false,
),
])),
);
}
#[test]
fn test_parse_filter_explicitly_case_sensitive() {
let filter = Filter::parse(r#"(artist eq_cs "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseSensitive,
false,
)),
);
let filter = Filter::parse(r#"(artist !eq_cs "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseSensitive,
true,
)),
);
let filter = Filter::parse(r#"(album contains_cs "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseSensitive,
false,
)),
);
let filter = Filter::parse(r#"(album !contains_cs "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseSensitive,
true,
)),
);
let filter = Filter::parse(r#"(title starts_with_cs "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseSensitive,
false,
)),
);
let filter = Filter::parse(r#"(title !starts_with_cs "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseSensitive,
true,
)),
);
}
#[test]
fn test_parse_filter_explicitly_case_insensitive() {
let filter = Filter::parse(r#"(artist eq_ci "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseInsensitive,
false,
)),
);
let filter = Filter::parse(r#"(artist !eq_ci "The Beatles")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CaseInsensitive,
true,
)),
);
let filter = Filter::parse(r#"(album contains_ci "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseInsensitive,
false,
)),
);
let filter = Filter::parse(r#"(album !contains_ci "Greatest Hits")"#);
assert_eq!(
filter,
Ok(Filter::Contains(
Tag::Album("Greatest Hits".to_string()),
CaseSensitivity::CaseInsensitive,
true,
)),
);
let filter = Filter::parse(r#"(title starts_with_ci "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseInsensitive,
false,
)),
);
let filter = Filter::parse(r#"(title !starts_with_ci "Symphony No. ")"#);
assert_eq!(
filter,
Ok(Filter::StartsWith(
Tag::Title("Symphony No. ".to_string()),
CaseSensitivity::CaseInsensitive,
true,
)),
);
}
#[test]
fn test_parse_filter_string_escapes() {
let filter = Filter::parse(r#"(Artist == "\"foo\\'bar\\\"")"#);
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist(r#""foo\'bar\""#.to_string()),
CaseSensitivity::CommandDependent,
false,
)),
);
}
#[test]
fn test_parse_filter_excessive_whitespace() {
let filter = Filter::parse("(\tartist\n == \"The Beatles\" ) ");
assert_eq!(
filter,
Ok(Filter::EqTag(
Tag::Artist("The Beatles".to_string()),
CaseSensitivity::CommandDependent,
false,
)),
);
}
}

View File

@@ -1,6 +1,6 @@
use std::str::FromStr;
use chrono::{DateTime, Utc};
use crate::filter::filter::{CaseSensitivity, ComparisonOperator, Filter, unescape_string};
use crate::filter::{CaseSensitivity, ComparisonOperator, Filter, unescape_string};
use crate::types::Tag;
grammar;