use crate::clients::Client;
use backtrace::Backtrace;
use boolinator::Boolinator;
use chrono::prelude::*;
use tracing::debug;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum OpCode {
Equality,
Inequality,
Contains,
RegexMatch,
RegexExclude,
GreaterThan,
LessThan,
GreaterThanEqual,
LessThanEqual,
}
impl std::fmt::Display for OpCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
OpCode::Equality => "==",
OpCode::Inequality => "!=",
OpCode::Contains => "contains",
OpCode::RegexMatch => "=~",
OpCode::RegexExclude => "!~",
OpCode::GreaterThan => ">",
OpCode::LessThan => "<",
OpCode::GreaterThanEqual => ">=",
OpCode::LessThanEqual => "<=",
}
)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Selector {
Artist,
Album,
AlbumArtist,
Title,
Track,
Name,
Genre,
Date,
OriginalDate,
Composer,
Performer,
Conductor,
Work,
Grouping,
Comment,
Disc,
Label,
MusicbrainzAristID,
MusicbrainzAlbumID,
MusicbrainzAlbumArtistID,
MusicbrainzTrackID,
MusicbrainzReleaseTrackID,
MusicbrainzWorkID,
File,
Base,
ModifiedSince,
AudioFormat,
Rating,
PlayCount,
LastPlayed,
}
impl std::fmt::Display for Selector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Selector::Artist => "artist",
Selector::Album => "album",
Selector::AlbumArtist => "albumartist",
Selector::Title => "title",
Selector::Track => "track",
Selector::Name => "name",
Selector::Genre => "genre",
Selector::Date => "date",
Selector::OriginalDate => "originaldate",
Selector::Composer => "composer",
Selector::Performer => "performer",
Selector::Conductor => "conductor",
Selector::Work => "work",
Selector::Grouping => "grouping",
Selector::Comment => "comment",
Selector::Disc => "disc",
Selector::Label => "label",
Selector::MusicbrainzAristID => "musicbrainz_aristid",
Selector::MusicbrainzAlbumID => "musicbrainz_albumid",
Selector::MusicbrainzAlbumArtistID => "musicbrainz_albumartistid",
Selector::MusicbrainzTrackID => "musicbrainz_trackid",
Selector::MusicbrainzReleaseTrackID => "musicbrainz_releasetrackid",
Selector::MusicbrainzWorkID => "musicbrainz_workid",
Selector::File => "file",
Selector::Base => "base",
Selector::ModifiedSince => "modified-since",
Selector::AudioFormat => "AudioFormat",
Selector::Rating => "rating",
Selector::PlayCount => "playcount",
Selector::LastPlayed => "lastplayed",
}
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Text(String),
UnixEpoch(i64),
Uint(usize),
}
fn quote_value(x: &Value) -> String {
match x {
Value::Text(s) => {
let mut ret = String::new();
ret.push('"');
for c in s.chars() {
if c == '"' || c == '\\' {
ret.push('\\');
}
ret.push(c);
}
ret.push('"');
ret
}
Value::UnixEpoch(n) => {
format!("'{}'", n)
}
Value::Uint(n) => {
format!("'{}'", n)
}
}
}
#[derive(Clone, Debug)]
pub enum Term {
UnaryCondition(Selector, Value),
BinaryCondition(Selector, OpCode, Value),
}
#[derive(Clone, Debug)]
pub enum Conjunction {
Simple(Box<Expression>, Box<Expression>),
Compound(Box<Conjunction>, Box<Expression>),
}
#[derive(Clone, Debug)]
pub enum Disjunction {
Simple(Box<Expression>, Box<Expression>),
Compound(Box<Disjunction>, Box<Expression>),
}
#[derive(Clone, Debug)]
pub enum Expression {
Simple(Box<Term>),
Negation(Box<Expression>),
Conjunction(Box<Conjunction>),
Disjunction(Box<Disjunction>),
}
#[cfg(test)]
mod smoke_tests {
use super::*;
use crate::filters::*;
#[test]
fn test_opcodes() {
assert!(ExprOpParser::new().parse("==").unwrap() == OpCode::Equality);
assert!(ExprOpParser::new().parse("!=").unwrap() == OpCode::Inequality);
assert!(ExprOpParser::new().parse("contains").unwrap() == OpCode::Contains);
assert!(ExprOpParser::new().parse("=~").unwrap() == OpCode::RegexMatch);
assert!(ExprOpParser::new().parse("!~").unwrap() == OpCode::RegexExclude);
assert!(ExprOpParser::new().parse(">").unwrap() == OpCode::GreaterThan);
assert!(ExprOpParser::new().parse("<").unwrap() == OpCode::LessThan);
assert!(ExprOpParser::new().parse(">=").unwrap() == OpCode::GreaterThanEqual);
assert!(ExprOpParser::new().parse("<=").unwrap() == OpCode::LessThanEqual);
}
#[test]
fn test_conditions() {
assert!(TermParser::new().parse("base 'foo'").is_ok());
assert!(TermParser::new().parse("artist == 'foo'").is_ok());
assert!(
TermParser::new()
.parse(r#"artist =~ "foo bar \"splat\"!""#)
.is_ok()
);
assert!(TermParser::new().parse("artist =~ 'Pogues'").is_ok());
match *TermParser::new()
.parse(r#"base "/Users/me/My Music""#)
.unwrap()
{
Term::UnaryCondition(a, b) => {
assert!(a == Selector::Base);
assert!(b == Value::Text(String::from(r#"/Users/me/My Music"#)));
}
_ => {
assert!(false);
}
}
match *TermParser::new()
.parse(r#"artist =~ "foo bar \"splat\"!""#)
.unwrap()
{
Term::BinaryCondition(t, op, s) => {
assert!(t == Selector::Artist);
assert!(op == OpCode::RegexMatch);
assert!(s == Value::Text(String::from(r#"foo bar "splat"!"#)));
}
_ => {
assert!(false);
}
}
}
#[test]
fn test_expressions() {
assert!(ExpressionParser::new().parse("( base 'foo' )").is_ok());
assert!(ExpressionParser::new().parse("(base \"foo\")").is_ok());
assert!(
ExpressionParser::new()
.parse("(!(artist == 'value'))")
.is_ok()
);
assert!(
ExpressionParser::new()
.parse(r#"((!(artist == "foo bar")) AND (base "/My Music"))"#)
.is_ok()
);
}
#[test]
fn test_quoted_expr() {
eprintln!("test_quoted_expr");
assert!(
ExpressionParser::new()
.parse(r#"(artist =~ "foo\\bar\"")"#)
.is_ok()
);
}
#[test]
fn test_real_expression() {
let result = ExpressionParser::new()
.parse(r#"(((Artist =~ 'Flogging Molly') OR (artist =~ 'Dropkick Murphys') OR (artist =~ 'Pogues')) AND ((rating > 128) OR (rating == 0)))"#);
eprintln!("{:#?}", result);
assert!(result.is_ok());
}
#[test]
fn test_conjunction() {
assert!(ExpressionParser::new()
.parse(
r#"((base "foo") AND (artist == "foo bar") AND (!(file == '/net/mp3/A/a.mp3')))"#
)
.is_ok());
eprintln!("==============================================================================");
eprintln!("{:#?}", ExpressionParser::new()
.parse(
r#"((base 'foo') AND (artist == "foo bar") AND ((!(file == "/net/mp3/A/a.mp3")) OR (file == "/pub/mp3/A/a.mp3")))"#
));
assert!(ExpressionParser::new()
.parse(
r#"((base 'foo') AND (artist == "foo bar") AND ((!(file == '/net/mp3/A/a.mp3')) OR (file == '/pub/mp3/A/a.mp3')))"#
)
.is_ok());
}
#[test]
fn test_disjunction() {
assert!(ExpressionParser::new().
parse(r#"((artist =~ 'Flogging Molly') OR (artist =~ 'Dropkick Murphys') OR (artist =~ 'Pogues'))"#)
.is_ok());
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum EvalOp {
And,
Or,
Not,
}
impl std::fmt::Display for EvalOp {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
EvalOp::And => write!(f, "And"),
EvalOp::Or => write!(f, "Or"),
EvalOp::Not => write!(f, "Not"),
}
}
}
#[derive(Debug)]
pub enum Error {
BadISO8601String {
text: Vec<u8>,
back: Backtrace,
},
ExpectQuoted {
text: String,
back: Backtrace,
},
FilterTypeErr {
text: String,
back: Backtrace,
},
InvalidOperand {
op: OpCode,
back: Backtrace,
},
OperatorOnStack {
op: EvalOp,
back: Backtrace,
},
RatingOverflow {
rating: usize,
back: Backtrace,
},
TooManyOperands {
num_ops: usize,
back: Backtrace,
},
NumericParse {
sticker: String,
source: std::num::ParseIntError,
back: Backtrace,
},
Client {
source: crate::clients::Error,
back: Backtrace,
},
}
impl std::fmt::Display for Error {
#[allow(unreachable_patterns)] fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::BadISO8601String { text, back: _ } => {
write!(f, "Bad ISO8601 timestamp: ``{:?}''", text)
}
Error::ExpectQuoted { text, back: _ } => write!(f, "Expected quote: ``{}''", text),
Error::FilterTypeErr { text, back: _ } => {
write!(f, "Un-expected type in filter ``{}''", text)
}
Error::InvalidOperand { op, back: _ } => write!(f, "Invalid operand {}", op),
Error::OperatorOnStack { op, back: _ } => {
write!(f, "Operator {} left on parse stack", op)
}
Error::RatingOverflow { rating, back: _ } => write!(f, "Rating {} overflows", rating),
Error::TooManyOperands { num_ops, back: _ } => {
write!(f, "Too many operands ({})", num_ops)
}
Error::NumericParse {
sticker,
source,
back: _,
} => write!(f, "While parsing sticker {}, got {}", sticker, source),
Error::Client { source, back: _ } => write!(f, "Client error: {}", source),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
Error::NumericParse {
sticker: _,
ref source,
back: _,
} => Some(source),
Error::Client {
ref source,
back: _,
} => Some(source),
_ => None,
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
fn peek(buf: &[u8]) -> Option<char> {
match buf.len() {
0 => None,
_ => Some(buf[0] as char),
}
}
fn take1(buf: &mut &[u8], i: usize) -> Result<()> {
if i > buf.len() {
return Err(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
});
}
let (_first, second) = buf.split_at(i);
*buf = second;
Ok(())
}
fn take2<T>(buf: &mut &[u8], i: usize) -> Result<T>
where
T: FromStr,
{
if i > buf.len() {
return Err(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
});
}
let (first, second) = buf.split_at(i);
*buf = second;
let s = std::str::from_utf8(first).map_err(|_| Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
})?;
s.parse::<T>().map_err(|_err| Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
}) }
pub fn parse_iso_8601(mut buf: &mut &[u8]) -> Result<i64> {
let year: i32 = take2(&mut buf, 4)?;
let mut month = 1;
let mut day = 1;
let mut hour = 0;
let mut minute = 0;
let mut second = 0;
if !buf.is_empty() {
let next = peek(buf);
if next != Some('T') {
let mut ext_fmt = false;
if next == Some('-') {
take1(buf, 1)?;
ext_fmt = true;
}
month = take2(&mut buf, 2)?;
if !buf.is_empty() {
if peek(buf) != Some('T') {
if ext_fmt {
take1(&mut buf, 1)?;
}
day = take2(&mut buf, 2)?;
}
}
}
if !buf.is_empty() {
take1(&mut buf, 1)?;
hour = take2(&mut buf, 2)?;
if !buf.is_empty() {
let mut ext_fmt = false;
if peek(buf) == Some(':') {
take1(&mut buf, 1)?;
ext_fmt = true;
}
minute = take2(&mut buf, 2)?;
if !buf.is_empty() {
if ext_fmt {
take1(&mut buf, 1)?;
}
second = take2(&mut buf, 2)?;
}
}
}
if !buf.is_empty() {
if peek(buf) == Some('Z') {
return Ok(Utc
.with_ymd_and_hms(year, month, day, hour, minute, second)
.single()
.ok_or(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
})?
.timestamp());
} else {
let next = peek(buf);
if next != Some('-') && next != Some('+') {
return Err(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
});
}
let west = next == Some('-');
take1(&mut buf, 1)?;
let hours: i32 = take2(&mut buf, 2)?;
let mut minutes = 0;
if !buf.is_empty() {
if peek(buf) == Some(':') {
take1(&mut buf, 1)?;
}
minutes = take2(&mut buf, 2)?;
}
if west {
return Ok(FixedOffset::west_opt(hours * 3600 + minutes * 60)
.ok_or(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
})?
.with_ymd_and_hms(year, month, day, hour, minute, second)
.single()
.ok_or(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
})?
.timestamp());
} else {
return Ok(FixedOffset::east_opt(hours * 3600 + minutes * 60)
.ok_or(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
})?
.with_ymd_and_hms(year, month, day, hour, minute, second)
.single()
.ok_or(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
})?
.timestamp());
}
}
}
}
Ok(Local
.with_ymd_and_hms(year, month, day, hour, minute, second)
.single()
.ok_or(Error::BadISO8601String {
text: buf.to_vec(),
back: Backtrace::new(),
})?
.timestamp())
}
#[cfg(test)]
mod iso_8601_tests {
use super::*;
#[test]
fn smoke_tests() {
let mut b = "19700101T00:00:00Z".as_bytes();
let t = parse_iso_8601(&mut b).unwrap();
assert!(t == 0);
let mut b = "19700101T00:00:01Z".as_bytes();
let t = parse_iso_8601(&mut b).unwrap();
assert!(t == 1);
let mut b = "20210327T02:26:53Z".as_bytes();
let t = parse_iso_8601(&mut b).unwrap();
assert_eq!(t, 1616812013);
let mut b = "20210327T07:29:05-07:00".as_bytes();
let t = parse_iso_8601(&mut b).unwrap();
assert_eq!(t, 1616855345);
let mut b = "2021".as_bytes();
parse_iso_8601(&mut b).unwrap();
}
}
pub fn expect_quoted(qtext: &str) -> Result<String> {
let mut iter = qtext.chars();
let quote = iter.next();
if quote.is_none() {
return Ok(String::new());
}
if quote != Some('\'') && quote != Some('"') {
return Err(Error::ExpectQuoted {
text: String::from(qtext),
back: Backtrace::new(),
});
}
let mut ret = String::new();
let mut this = iter.next();
while this != quote {
if this == Some('\\') {
this = iter.next();
}
match this {
Some(c) => ret.push(c),
None => {
return Err(Error::ExpectQuoted {
text: String::from(qtext),
back: Backtrace::new(),
});
}
}
this = iter.next();
}
Ok(ret)
}
#[cfg(test)]
mod quoted_tests {
use super::*;
#[test]
fn smoke_tests() {
let b = r#""foo bar \"splat!\"""#;
let s = expect_quoted(b).unwrap();
assert!(s == r#"foo bar "splat!""#);
}
}
fn make_numeric_closure<'a, T: 'a + PartialEq + PartialOrd + Copy>(
op: OpCode,
val: T,
) -> Result<impl Fn(T) -> bool + 'a> {
match op {
OpCode::Equality => Ok(Box::new(move |x: T| x == val) as Box<dyn Fn(T) -> bool>),
OpCode::Inequality => Ok(Box::new(move |x: T| x != val) as Box<dyn Fn(T) -> bool>),
OpCode::GreaterThan => Ok(Box::new(move |x: T| x > val) as Box<dyn Fn(T) -> bool>),
OpCode::LessThan => Ok(Box::new(move |x: T| x < val) as Box<dyn Fn(T) -> bool>),
OpCode::GreaterThanEqual => Ok(Box::new(move |x: T| x >= val) as Box<dyn Fn(T) -> bool>),
OpCode::LessThanEqual => Ok(Box::new(move |x: T| x <= val) as Box<dyn Fn(T) -> bool>),
_ => Err(Error::InvalidOperand {
op: op,
back: Backtrace::new(),
}),
}
}
async fn eval_numeric_sticker_term<
T: PartialEq + PartialOrd + Copy + FromStr<Err = std::num::ParseIntError> + std::fmt::Display,
>(
sticker: &str,
client: &mut Client,
op: OpCode,
numeric_val: T,
default_val: T,
) -> Result<HashSet<String>> {
let cmp = make_numeric_closure(op, numeric_val)?;
let mut m = client
.get_stickers(sticker)
.await
.map_err(|err| Error::Client {
source: err,
back: Backtrace::new(),
})?
.drain()
.map(|(k, v)| v.parse::<T>().map(|x| (k, x)))
.collect::<std::result::Result<HashMap<String, T>, _>>()
.map_err(|err| Error::NumericParse {
sticker: String::from(sticker),
source: err,
back: Backtrace::new(),
})?;
client
.get_all_songs()
.await
.map_err(|err| Error::Client {
source: err,
back: Backtrace::new(),
})?
.drain(..)
.for_each(|song| {
if m.get(&song).is_none() {
m.insert(song, default_val);
}
});
Ok(m.drain()
.filter_map(|(k, v)| cmp(v).as_some(k))
.collect::<HashSet<String>>())
}
pub struct FilterStickerNames<'a> {
rating: &'a str,
playcount: &'a str,
lastplayed: &'a str,
}
impl<'a> FilterStickerNames<'a> {
pub fn new(rating: &'a str, playcount: &'a str, lastplayed: &'a str) -> FilterStickerNames<'a> {
FilterStickerNames {
rating: rating,
playcount: playcount,
lastplayed: lastplayed,
}
}
}
async fn eval_term<'a>(
term: &Term,
case: bool,
client: &mut Client,
stickers: &FilterStickerNames<'a>,
) -> Result<HashSet<String>> {
match term {
Term::UnaryCondition(op, val) => Ok(client
.find1(&format!("{}", op), "e_value(&val), case)
.await
.map_err(|err| Error::Client {
source: err,
back: Backtrace::new(),
})?
.drain(..)
.collect()),
Term::BinaryCondition(attr, op, val) => {
if *attr == Selector::Rating {
match val {
Value::Uint(n) => {
if *n > 255 {
return Err(Error::RatingOverflow {
rating: *n,
back: Backtrace::new(),
});
}
Ok(eval_numeric_sticker_term(
stickers.rating,
client,
*op,
*n as u8,
0 as u8,
)
.await?)
}
_ => Err(Error::FilterTypeErr {
text: format!("filter ratings expect an unsigned int; got {:#?}", val),
back: Backtrace::new(),
}),
}
} else if *attr == Selector::PlayCount {
match val {
Value::Uint(n) => Ok(eval_numeric_sticker_term(
stickers.playcount,
client,
*op,
*n,
0 as usize,
)
.await?),
_ => Err(Error::FilterTypeErr {
text: format!("filter ratings expect an unsigned int; got {:#?}", val),
back: Backtrace::new(),
}),
}
} else if *attr == Selector::LastPlayed {
match val {
Value::UnixEpoch(t) => Ok(eval_numeric_sticker_term(
stickers.lastplayed,
client,
*op,
*t,
0 as i64,
)
.await?),
_ => Err(Error::FilterTypeErr {
text: format!("filter ratings expect an unsigned int; got {:#?}", val),
back: Backtrace::new(),
}),
}
} else {
Ok(client
.find2(
&format!("{}", attr),
&format!("{}", op),
"e_value(val),
case,
)
.await
.map_err(|err| Error::Client {
source: err,
back: Backtrace::new(),
})?
.drain(..)
.collect())
}
}
}
}
#[derive(Debug)]
enum EvalStackNode {
Op(EvalOp),
Result(HashSet<String>),
}
async fn negate_result(
res: &HashSet<String>,
client: &mut Client,
) -> std::result::Result<HashSet<String>, Error> {
Ok(client
.get_all_songs()
.await
.map_err(|err| Error::Client {
source: err,
back: Backtrace::new(),
})?
.drain(..)
.filter_map(|song| {
if !res.contains(&song) {
Some(song)
} else {
None
}
})
.collect::<HashSet<String>>())
}
async fn reduce(stack: &mut Vec<EvalStackNode>, client: &mut Client) -> Result<()> {
loop {
let mut reduced = false;
let n = stack.len();
if n > 1 {
let reduction = if let (EvalStackNode::Op(EvalOp::Not), EvalStackNode::Result(r)) =
(&stack[n - 2], &stack[n - 1])
{
Some(negate_result(&r, client).await?)
} else {
None
};
if let Some(res) = reduction {
stack.pop();
stack.pop();
stack.push(EvalStackNode::Result(res));
reduced = true;
}
}
let n = stack.len();
if n > 2 {
let and_reduction = if let (
EvalStackNode::Op(EvalOp::And),
EvalStackNode::Result(r1),
EvalStackNode::Result(r2),
) = (&stack[n - 3], &stack[n - 2], &stack[n - 1])
{
Some(r1.intersection(&r2).cloned().collect())
} else {
None
};
if let Some(res) = and_reduction {
stack.pop();
stack.pop();
stack.pop();
stack.push(EvalStackNode::Result(res));
reduced = true;
}
}
let n = stack.len();
if n > 2 {
let or_reduction = if let (
EvalStackNode::Op(EvalOp::Or),
EvalStackNode::Result(r1),
EvalStackNode::Result(r2),
) = (&stack[n - 3], &stack[n - 2], &stack[n - 1])
{
Some(r1.union(&r2).cloned().collect())
} else {
None
};
if let Some(res) = or_reduction {
stack.pop();
stack.pop();
stack.pop();
stack.push(EvalStackNode::Result(res));
reduced = true;
}
}
if !reduced {
break;
}
}
Ok(())
}
pub async fn evaluate<'a>(
expr: &Expression,
case: bool,
client: &mut Client,
stickers: &FilterStickerNames<'a>,
) -> Result<HashSet<String>> {
let mut sp = Vec::new();
sp.push(expr);
let mut se = Vec::new();
while !sp.is_empty() {
let node = sp.pop().unwrap();
match node {
Expression::Simple(bt) => se.push(EvalStackNode::Result(
eval_term(bt, case, client, stickers).await?,
)),
Expression::Negation(be) => {
se.push(EvalStackNode::Op(EvalOp::Not));
sp.push(&*be);
}
Expression::Conjunction(bc) => {
let mut conj = &**bc;
loop {
match conj {
Conjunction::Simple(bel, ber) => {
se.push(EvalStackNode::Op(EvalOp::And));
sp.push(&**ber);
sp.push(&**bel);
break;
}
Conjunction::Compound(bc, be) => {
se.push(EvalStackNode::Op(EvalOp::And));
sp.push(&**be);
conj = bc;
}
}
}
}
Expression::Disjunction(bt) => {
let mut disj = &**bt;
loop {
match disj {
Disjunction::Simple(bel, ber) => {
se.push(EvalStackNode::Op(EvalOp::Or));
sp.push(&ber);
sp.push(&bel);
break;
}
Disjunction::Compound(bd, be) => {
se.push(EvalStackNode::Op(EvalOp::Or));
sp.push(&**be);
disj = bd;
}
}
}
}
}
reduce(&mut se, client).await?;
}
reduce(&mut se, client).await?;
if 1 != se.len() {
debug!("Too many ({}) operands left on stack:", se.len());
se.iter()
.enumerate()
.for_each(|(i, x)| debug!(" {}: {:#?}", i, x));
return Err(Error::TooManyOperands {
num_ops: se.len(),
back: Backtrace::new(),
});
}
let ret = se.pop().unwrap();
match ret {
EvalStackNode::Result(result) => Ok(result),
EvalStackNode::Op(op) => {
debug!("Operator left on stack (!?): {:#?}", op);
Err(Error::OperatorOnStack {
op: op,
back: Backtrace::new(),
})
}
}
}
#[cfg(test)]
mod evaluation_tests {
use super::*;
use crate::filters::*;
use crate::clients::Client;
use crate::clients::test_mock::Mock;
#[tokio::test]
async fn smoke() {
let mock = Box::new(Mock::new(&[(
r#"find "(base \"foo\")""#,
"file: foo/a.mp3
Artist: The Foobars
file: foo/b.mp3
Title: b!
OK",
)]));
let mut cli = Client::new(mock).unwrap();
let stickers = FilterStickerNames::new(&"rating", &"playcount", &"lastplayed");
let expr = ExpressionParser::new().parse(r#"(base "foo")"#).unwrap();
let result = evaluate(&expr, true, &mut cli, &stickers).await;
assert!(result.is_ok());
let g: HashSet<String> = ["foo/a.mp3", "foo/b.mp3"]
.iter()
.map(|x| x.to_string())
.collect();
assert!(result.unwrap() == g);
}
}