filter: implement basic parser
This commit is contained in:
@@ -11,8 +11,13 @@ edition = "2024"
|
||||
rust-version = "1.85.0"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
lalrpop-util = { version = "0.22.2", features = ["lexer"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = "2.0.7"
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
[build-dependencies]
|
||||
lalrpop = "0.22.2"
|
||||
|
||||
8
build.rs
Normal file
8
build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
lalrpop::process_root().unwrap();
|
||||
// let debug_mode = std::env::var("PROFILE").unwrap() == "debug";
|
||||
// lalrpop::Configuration::new()
|
||||
// .emit_comments(debug_mode)
|
||||
// .process()
|
||||
// .unwrap();
|
||||
}
|
||||
485
src/filter.rs
485
src/filter.rs
@@ -1,484 +1,3 @@
|
||||
use std::fmt;
|
||||
mod filter;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub fn parse(input: &str) -> Result<Filter, RequestParserError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_filter_eq_tag() {
|
||||
let filter = Filter::parse("(artist == 'The Beatles')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::EqTag(
|
||||
Tag::Artist("The Beatles".to_string()),
|
||||
CaseSensitivity::CommandDependent,
|
||||
false,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(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("(album contains 'Greatest Hits')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::Contains(
|
||||
Tag::Album("Greatest Hits".to_string()),
|
||||
CaseSensitivity::CommandDependent,
|
||||
false,
|
||||
))
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(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("(title starts_with 'Symphony No. ')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::StartsWith(
|
||||
Tag::Title("Symphony No. ".to_string()),
|
||||
CaseSensitivity::CommandDependent,
|
||||
false,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(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("(composer =~ 'Beethoven.*')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::PerlRegex(Tag::Composer("Beethoven.*".to_string()))),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filter_perl_regex_negative() {
|
||||
let filter = Filter::parse("(genre !~ 'Pop.*')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::NegPerlRegex(Tag::Genre("Pop.*".to_string()))),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filter_base() {
|
||||
let filter = Filter::parse("(base 'Rock/Classics')");
|
||||
assert_eq!(filter, Ok(Filter::Base("Rock/Classics".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filter_modified_since() {
|
||||
let filter = Filter::parse("(modified-since '1622505600')");
|
||||
assert_eq!(filter, Ok(Filter::ModifiedSince(1622505600)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filter_audio_added_since() {
|
||||
let filter = Filter::parse("(added-since '1622505600')");
|
||||
assert_eq!(filter, Ok(Filter::ModifiedSince(1622505600)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filter_audio_format_eq() {
|
||||
let filter = Filter::parse("(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("(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() {
|
||||
let filter = Filter::parse("(prio >= 42)");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::PrioCmp(ComparisonOperator::GreaterThanOrEqual, 42)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_filter_not() {
|
||||
let filter = Filter::parse("(!(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("((artist == 'The Beatles') AND (album == 'Abbey Road'))");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(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 filter = Filter::parse("(artist eq_cs 'The Beatles')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::EqTag(
|
||||
Tag::Artist("The Beatles".to_string()),
|
||||
CaseSensitivity::CaseSensitive,
|
||||
false,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(artist !eq_cs 'The Beatles')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::EqTag(
|
||||
Tag::Artist("The Beatles".to_string()),
|
||||
CaseSensitivity::CaseSensitive,
|
||||
true,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(album contains_cs 'Greatest Hits')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::Contains(
|
||||
Tag::Album("Greatest Hits".to_string()),
|
||||
CaseSensitivity::CaseSensitive,
|
||||
false,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(album !contains_cs 'Greatest Hits')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::Contains(
|
||||
Tag::Album("Greatest Hits".to_string()),
|
||||
CaseSensitivity::CaseSensitive,
|
||||
true,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(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("(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("(artist eq_ci 'The Beatles')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::EqTag(
|
||||
Tag::Artist("The Beatles".to_string()),
|
||||
CaseSensitivity::CaseInsensitive,
|
||||
false,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(artist !eq_ci 'The Beatles')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::EqTag(
|
||||
Tag::Artist("The Beatles".to_string()),
|
||||
CaseSensitivity::CaseInsensitive,
|
||||
true,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(album contains_ci 'Greatest Hits')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::Contains(
|
||||
Tag::Album("Greatest Hits".to_string()),
|
||||
CaseSensitivity::CaseInsensitive,
|
||||
false,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(album !contains_ci 'Greatest Hits')");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::Contains(
|
||||
Tag::Album("Greatest Hits".to_string()),
|
||||
CaseSensitivity::CaseInsensitive,
|
||||
true,
|
||||
)),
|
||||
);
|
||||
|
||||
let filter = Filter::parse("(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("(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("(Artist == \"foo\\'bar\\\"\")");
|
||||
assert_eq!(
|
||||
filter,
|
||||
Ok(Filter::EqTag(
|
||||
Tag::Artist("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,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
pub use filter::Filter;
|
||||
|
||||
625
src/filter/filter.rs
Normal file
625
src/filter/filter.rs
Normal file
@@ -0,0 +1,625 @@
|
||||
use std::fmt;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use lalrpop_util::lalrpop_mod;
|
||||
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(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(×tamp.timestamp().to_string())
|
||||
)
|
||||
}
|
||||
Filter::AddedSince(timestamp) => {
|
||||
write!(
|
||||
f,
|
||||
"(added-since {})",
|
||||
quote_string(×tamp.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,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
149
src/filter/filter_grammar.lalrpop
Normal file
149
src/filter/filter_grammar.lalrpop
Normal file
@@ -0,0 +1,149 @@
|
||||
use std::str::FromStr;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::filter::filter::{CaseSensitivity, ComparisonOperator, Filter, unescape_string};
|
||||
use crate::common::Tag;
|
||||
|
||||
grammar;
|
||||
|
||||
// match {
|
||||
// r#"[ \t\n\r]+"# => {},
|
||||
// r"[0-9]+"
|
||||
// } else {
|
||||
// r#"\w+"#,
|
||||
// _
|
||||
// }
|
||||
|
||||
i64: i64 = <s:r"[0-9]+"> => i64::from_str(s).unwrap();
|
||||
u32: u32 = <s:r"[0-9]+"> => u32::from_str(s).unwrap();
|
||||
u8: u8 = <s:r"[0-9]+"> => u8::from_str(s).unwrap();
|
||||
|
||||
pub Expression: Filter = {
|
||||
"(" "base" <p:Uri> ")" => Filter::Base(p),
|
||||
"(" "file" "==" <p:Uri> ")" => Filter::EqUri(p),
|
||||
"(" "prio" <op:PrioCmp> <n:u8> ")" => Filter::PrioCmp(op, n),
|
||||
|
||||
"(" "modified-since" <d:DateTime_> ")" => Filter::ModifiedSince(d),
|
||||
"(" "added-since" <d:DateTime_> ")" => Filter::AddedSince(d),
|
||||
"(" "AudioFormat" "==" <af:AudioFormat> ")" => {
|
||||
let (sr, b, c) = af;
|
||||
match (sr, b, c) {
|
||||
(Some(sr), Some(b), Some(c)) => Filter::AudioFormatEq {
|
||||
sample_rate: sr,
|
||||
bits: b,
|
||||
channels: c,
|
||||
},
|
||||
_ => panic!("AudioFormat equality requires all fields to be specified"),
|
||||
}
|
||||
},
|
||||
"(" "AudioFormat" "=~" <af:AudioFormat> ")" => {
|
||||
let (sr, b, c) = af;
|
||||
Filter::AudioFormatEqMask {
|
||||
sample_rate: sr,
|
||||
bits: b,
|
||||
channels: c,
|
||||
}
|
||||
},
|
||||
|
||||
"(" <t:TagName> "==" <v:TagValue> ")" => Filter::EqTag(Tag::new(t, v), CaseSensitivity::CommandDependent, false),
|
||||
"(" <t:TagName> "eq_cs" <v:TagValue> ")" => Filter::EqTag(Tag::new(t, v), CaseSensitivity::CaseSensitive, false),
|
||||
"(" <t:TagName> "eq_ci" <v:TagValue> ")" => Filter::EqTag(Tag::new(t, v), CaseSensitivity::CaseInsensitive, false),
|
||||
"(" <t:TagName> "!=" <v:TagValue> ")" => Filter::EqTag(Tag::new(t, v), CaseSensitivity::CommandDependent, true),
|
||||
"(" <t:TagName> "!eq_cs" <v:TagValue> ")" => Filter::EqTag(Tag::new(t, v), CaseSensitivity::CaseSensitive, true),
|
||||
"(" <t:TagName> "!eq_ci" <v:TagValue> ")" => Filter::EqTag(Tag::new(t, v), CaseSensitivity::CaseInsensitive, true),
|
||||
|
||||
"(" <t:TagName> "contains" <v:TagValue> ")" => Filter::Contains(Tag::new(t, v), CaseSensitivity::CommandDependent, false),
|
||||
"(" <t:TagName> "contains_cs" <v:TagValue> ")" => Filter::Contains(Tag::new(t, v), CaseSensitivity::CaseSensitive, false),
|
||||
"(" <t:TagName> "contains_ci" <v:TagValue> ")" => Filter::Contains(Tag::new(t, v), CaseSensitivity::CaseInsensitive, false),
|
||||
"(" <t:TagName> "!contains" <v:TagValue> ")" => Filter::Contains(Tag::new(t, v), CaseSensitivity::CommandDependent, true),
|
||||
"(" <t:TagName> "!contains_cs" <v:TagValue> ")" => Filter::Contains(Tag::new(t, v), CaseSensitivity::CaseSensitive, true),
|
||||
"(" <t:TagName> "!contains_ci" <v:TagValue> ")" => Filter::Contains(Tag::new(t, v), CaseSensitivity::CaseInsensitive, true),
|
||||
|
||||
"(" <t:TagName> "starts_with" <v:TagValue> ")" => Filter::StartsWith(Tag::new(t, v), CaseSensitivity::CommandDependent, false),
|
||||
"(" <t:TagName> "starts_with_cs" <v:TagValue> ")" => Filter::StartsWith(Tag::new(t, v), CaseSensitivity::CaseSensitive, false),
|
||||
"(" <t:TagName> "starts_with_ci" <v:TagValue> ")" => Filter::StartsWith(Tag::new(t, v), CaseSensitivity::CaseInsensitive, false),
|
||||
"(" <t:TagName> "!starts_with" <v:TagValue> ")" => Filter::StartsWith(Tag::new(t, v), CaseSensitivity::CommandDependent, true),
|
||||
"(" <t:TagName> "!starts_with_cs" <v:TagValue> ")" => Filter::StartsWith(Tag::new(t, v), CaseSensitivity::CaseSensitive, true),
|
||||
"(" <t:TagName> "!starts_with_ci" <v:TagValue> ")" => Filter::StartsWith(Tag::new(t, v), CaseSensitivity::CaseInsensitive, true),
|
||||
|
||||
"(" <t:TagName> "=~" <p:Pattern> ")" => Filter::PerlRegex(Tag::new(t, p), false),
|
||||
"(" <t:TagName> "!~" <p:Pattern> ")" => Filter::PerlRegex(Tag::new(t, p), true),
|
||||
"(" "!" <e:Expression> ")" => Filter::Not(Box::new(e)),
|
||||
"(" <e:AndExpression> ")" => e,
|
||||
};
|
||||
|
||||
AndExpression: Filter = {
|
||||
<e:Expression> => e,
|
||||
<e1:AndExpression> "AND" <e2:Expression> => {
|
||||
let mut filters = match e1 {
|
||||
Filter::And(fs) => fs,
|
||||
_ => vec![e1],
|
||||
};
|
||||
filters.push(e2);
|
||||
Filter::And(filters)
|
||||
}
|
||||
}
|
||||
|
||||
DateTime_: DateTime<Utc> = {
|
||||
<n:i64> => {
|
||||
DateTime::<Utc>::from_timestamp(n, 0).unwrap()
|
||||
},
|
||||
<s:EscapedString> => {
|
||||
// Try ISO 8601 without timezone first
|
||||
if let Some(dt) = DateTime::parse_from_rfc3339(&s).ok().map(|dt| dt.with_timezone(&Utc)) {
|
||||
return dt;
|
||||
} else if let Some(n) = i64::from_str(&s).ok() && let Some(d) = DateTime::<Utc>::from_timestamp(n, 0) {
|
||||
return d;
|
||||
}
|
||||
panic!("Invalid date time format: {}", s);
|
||||
}
|
||||
};
|
||||
|
||||
PrioCmp : ComparisonOperator = {
|
||||
">" => ComparisonOperator::GreaterThan,
|
||||
">=" => ComparisonOperator::GreaterThanOrEqual,
|
||||
"<" => ComparisonOperator::LessThan,
|
||||
"<=" => ComparisonOperator::LessThanOrEqual,
|
||||
"==" => ComparisonOperator::Equal,
|
||||
"!=" => ComparisonOperator::NotEqual,
|
||||
};
|
||||
|
||||
Uri: String = {
|
||||
<e:EscapedString> => e,
|
||||
};
|
||||
|
||||
TagName: String = <s:r"[a-zA-Z_][a-zA-Z0-9_]*"> => {
|
||||
unescape_string(s)
|
||||
};
|
||||
|
||||
TagValue: String = {
|
||||
<e:EscapedString> => e,
|
||||
// <s:r#"[^\s\(\)"]+"#> => {
|
||||
// s.to_string()
|
||||
// },
|
||||
};
|
||||
|
||||
Pattern: String = {
|
||||
<e:EscapedString> => e,
|
||||
};
|
||||
|
||||
pub EscapedString: String = {
|
||||
<l:r#""(\\\\|\\"|[^"\\])*""#> => {
|
||||
let unquoted = &l[1..l.len()-1];
|
||||
unescape_string(unquoted)
|
||||
},
|
||||
<l:r#"'(\\\\|\\'|[^'\\])*'"#> => {
|
||||
let unquoted = &l[1..l.len()-1];
|
||||
unescape_string(unquoted)
|
||||
},
|
||||
};
|
||||
|
||||
AudioFormat: (Option<u32>, Option<u8>, Option<u8>) = {
|
||||
<s:u32> ":" <b:u8> ":" <c:u8> => (Some(s), Some(b), Some(c)),
|
||||
<s:u32> ":" <b:u8> ":" "*" => (Some(s), Some(b), None),
|
||||
<s:u32> ":" "*" ":" <c:u8> => (Some(s), None, Some(c)),
|
||||
<s:u32> ":" "*" ":" "*" => (Some(s), None, None),
|
||||
"*" ":" <b:u8> ":" <c:u8> => (None, Some(b), Some(c)),
|
||||
"*" ":" <b:u8> ":" "*" => (None, Some(b), None),
|
||||
"*" ":" "*" ":" <c:u8> => (None, None, Some(c)),
|
||||
"*" ":" "*" ":" "*" => (None, None, None),
|
||||
};
|
||||
Reference in New Issue
Block a user