use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Context {
#[default]
Storage,
Execution,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Query {
pub root: Clause,
}
#[cfg(feature = "serde")]
impl serde::Serialize for Query {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.compile_for_storage())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Query {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let query = String::deserialize(deserializer)?;
Self::from_str(&query).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Clause {
Compound(CompoundClause),
Leaf(LeafClause),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CompoundClause {
pub clauses: Vec<Clause>,
pub kind: CompoundKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CompoundKind {
Or,
And,
}
impl CompoundKind {
#[must_use]
#[inline]
pub const fn operator(&self) -> &'static str {
match self {
Self::Or => " OR ",
Self::And => " AND ",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LeafClause {
pub left: Value,
pub operator: Operator,
pub right: Value,
}
impl LeafClause {
#[must_use]
#[inline]
pub const fn has_valid_operator(&self) -> bool {
match (&self.left, &self.right) {
(
Value::String(_) | Value::Field(Field::Album | Field::Title),
Value::String(_) | Value::Field(Field::Album | Field::Title),
) if matches!(
self.operator,
Operator::Contains
| Operator::ContainsNot
| Operator::Inside
| Operator::NotInside
| Operator::In
| Operator::NotIn
) =>
{
true
}
(
Value::String(_)
| Value::Int(_)
| Value::Field(Field::Album | Field::ReleaseYear | Field::Title),
Value::String(_)
| Value::Int(_)
| Value::Field(Field::Album | Field::ReleaseYear | Field::Title),
) => matches!(
self.operator,
Operator::Equal
| Operator::NotEqual
| Operator::Like
| Operator::NotLike
| Operator::LessThan
| Operator::LessThanOrEqual
| Operator::GreaterThan
| Operator::GreaterThanOrEqual,
),
(
Value::String(_)
| Value::Int(_)
| Value::Field(Field::Album | Field::ReleaseYear | Field::Title),
Value::Set(_) | Value::Field(Field::AlbumArtists | Field::Artists | Field::Genre),
) => matches!(
self.operator,
Operator::Inside | Operator::NotInside | Operator::In | Operator::NotIn
),
(
Value::Set(_) | Value::Field(Field::AlbumArtists | Field::Artists | Field::Genre),
Value::String(_)
| Value::Int(_)
| Value::Field(Field::Album | Field::ReleaseYear | Field::Title),
) => matches!(
self.operator,
Operator::Contains
| Operator::ContainsNot
| Operator::AllEqual
| Operator::AnyEqual
| Operator::AllLike
| Operator::AnyLike
),
(
Value::Set(_) | Value::Field(Field::AlbumArtists | Field::Artists | Field::Genre),
Value::Set(_) | Value::Field(Field::AlbumArtists | Field::Artists | Field::Genre),
) => matches!(
self.operator,
Operator::Contains
| Operator::ContainsAll
| Operator::ContainsAny
| Operator::ContainsNone
| Operator::AllInside
| Operator::AnyInside
| Operator::NoneInside
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Value {
String(String),
Int(i64),
Set(Vec<Self>),
Field(Field),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Field {
Title,
Artists,
Album,
AlbumArtists,
Genre,
ReleaseYear,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Operator {
Equal,
NotEqual,
AnyEqual,
AllEqual,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Like,
NotLike,
AnyLike,
AllLike,
In,
NotIn,
Contains,
ContainsNot,
ContainsAll,
ContainsAny,
ContainsNone,
Inside,
NotInside,
AllInside,
AnyInside,
NoneInside,
}
pub trait Compile {
fn compile(&self, context: Context) -> String;
#[inline]
fn compile_for_storage(&self) -> String {
self.compile(Context::Storage)
}
#[inline]
fn compile_for_execution(&self) -> String {
self.compile(Context::Execution)
}
}
macro_rules! impl_display {
($($t:ty),*) => {
$(
impl std::fmt::Display for $t {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.compile_for_storage())
}
}
)*
};
}
impl_display!(
Query,
Clause,
CompoundClause,
LeafClause,
Value,
Field,
Operator
);
impl Compile for Query {
#[inline]
fn compile(&self, context: Context) -> String {
self.root.compile(context)
}
}
impl Compile for Clause {
#[inline]
fn compile(&self, context: Context) -> String {
match self {
Self::Compound(compound) => compound.compile(context),
Self::Leaf(leaf) => leaf.compile(context),
}
}
}
impl Compile for CompoundClause {
#[inline]
fn compile(&self, context: Context) -> String {
debug_assert!(!self.clauses.is_empty());
debug_assert_eq!(self.clauses.len(), 2);
let operator = self.kind.operator();
let mut clauses = self
.clauses
.iter()
.map(|c| c.compile(context))
.collect::<Vec<_>>()
.join(operator);
if self.clauses.len() > 1 {
clauses = format!("({clauses})");
}
clauses
}
}
impl Compile for LeafClause {
#[inline]
fn compile(&self, context: Context) -> String {
format!(
"{} {} {}",
self.left.compile(context),
self.operator.compile(context),
self.right.compile(context)
)
}
}
impl Compile for Value {
#[inline]
fn compile(&self, context: Context) -> String {
match self {
Self::String(s) => format!("\"{s}\""),
Self::Int(i) => i.to_string(),
Self::Set(set) => {
let set = set
.iter()
.map(|v| v.compile(context))
.collect::<Vec<_>>()
.join(", ");
format!("[{set}]")
}
Self::Field(field) => field.compile(context),
}
}
}
impl Compile for Field {
#[inline]
fn compile(&self, context: Context) -> String {
match (self, context) {
(Self::Title, _) => "title".to_string(),
(Self::Album, _) => "album".to_string(),
(Self::Artists, Context::Storage) => "artist".to_string(),
(Self::Artists, Context::Execution) => "array::flatten([artist][? $this])".to_string(),
(Self::AlbumArtists, Context::Storage) => "album_artist".to_string(),
(Self::AlbumArtists, Context::Execution) => {
"array::flatten([album_artist][? $this])".to_string()
}
(Self::Genre, Context::Storage) => "genre".to_string(),
(Self::Genre, Context::Execution) => "array::flatten([genre][? $this])".to_string(),
(Self::ReleaseYear, _) => "release_year".to_string(),
}
}
}
impl Compile for Operator {
#[inline]
fn compile(&self, _: Context) -> String {
match self {
Self::Equal => "=".to_string(),
Self::NotEqual => "!=".to_string(),
Self::AnyEqual => "?=".to_string(),
Self::AllEqual => "*=".to_string(),
Self::GreaterThan => ">".to_string(),
Self::GreaterThanOrEqual => ">=".to_string(),
Self::LessThan => "<".to_string(),
Self::LessThanOrEqual => "<=".to_string(),
Self::Like => "~".to_string(),
Self::NotLike => "!~".to_string(),
Self::AnyLike => "?~".to_string(),
Self::AllLike => "*~".to_string(),
Self::In => "IN".to_string(),
Self::NotIn => "NOT IN".to_string(),
Self::Contains => "CONTAINS".to_string(),
Self::ContainsNot => "CONTAINSNOT".to_string(),
Self::ContainsAll => "CONTAINSALL".to_string(),
Self::ContainsAny => "CONTAINSANY".to_string(),
Self::ContainsNone => "CONTAINSNONE".to_string(),
Self::Inside => "INSIDE".to_string(),
Self::NotInside => "NOTINSIDE".to_string(),
Self::AllInside => "ALLINSIDE".to_string(),
Self::AnyInside => "ANYINSIDE".to_string(),
Self::NoneInside => "NONEINSIDE".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use std::marker::PhantomData;
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
use rstest_reuse::{apply, template};
#[template]
#[rstest]
#[case::operator(Operator::Equal, "=")]
#[case::operator(Operator::NotEqual, "!=")]
#[case::operator(Operator::AnyEqual, "?=")]
#[case::operator(Operator::AllEqual, "*=")]
#[case::operator(Operator::GreaterThan, ">")]
#[case::operator(Operator::GreaterThanOrEqual, ">=")]
#[case::operator(Operator::LessThan, "<")]
#[case::operator(Operator::LessThanOrEqual, "<=")]
#[case::operator(Operator::Like, "~")]
#[case::operator(Operator::NotLike, "!~")]
#[case::operator(Operator::AnyLike, "?~")]
#[case::operator(Operator::AllLike, "*~")]
#[case::operator(Operator::In, "IN")]
#[case::operator(Operator::NotIn, "NOT IN")]
#[case::operator(Operator::Contains, "CONTAINS")]
#[case::operator(Operator::ContainsNot, "CONTAINSNOT")]
#[case::operator(Operator::ContainsAll, "CONTAINSALL")]
#[case::operator(Operator::ContainsAny, "CONTAINSANY")]
#[case::operator(Operator::ContainsNone, "CONTAINSNONE")]
#[case::operator(Operator::Inside, "INSIDE")]
#[case::operator(Operator::NotInside, "NOTINSIDE")]
#[case::operator(Operator::AllInside, "ALLINSIDE")]
#[case::operator(Operator::AnyInside, "ANYINSIDE")]
#[case::operator(Operator::NoneInside, "NONEINSIDE")]
#[case::field(Field::Title, "title")]
#[case::field(Field::Artists, "artist")]
#[case::field(Field::Album, "album")]
#[case::field(Field::AlbumArtists, "album_artist")]
#[case::field(Field::Genre, "genre")]
#[case::field(Field::ReleaseYear, "release_year")]
#[case::value(Value::String("foo".to_string()), "\"foo\"")]
#[case::value(Value::Int(42), "42")]
#[case::value(Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)]), "[\"foo\", 42]")]
#[case::value(Value::Field(Field::Title), "title")]
#[case::leaf_clause(
LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
},
"title = \"foo\""
)]
#[case::leaf_clause(
LeafClause {
left: Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)]),
operator: Operator::Contains,
right: Value::Int(42)
},
"[\"foo\", 42] CONTAINS 42"
)]
#[case::compound_clause(
CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::String("bar".to_string()),
operator: Operator::Inside,
right: Value::Field(Field::Artists),
}),
],
kind: CompoundKind::And
},
"(title = \"foo\" AND \"bar\" INSIDE artist)"
)]
#[case::compound_clause(
CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::Contains,
right: Value::String("bar".to_string())
}),
],
kind: CompoundKind::Or
},
"(title = \"foo\" OR artist CONTAINS \"bar\")"
)]
#[case::query(
Query {
root: Clause::Compound(CompoundClause {
clauses: vec![
Clause::Compound(
CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}),
Clause::Compound(CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::ContainsNot,
right: Value::String("bar".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Album),
operator: Operator::Equal,
right: Value::String("baz".to_string())
}),
],
kind: CompoundKind::Or
}),
],
kind: CompoundKind::And
}
),
Clause::Leaf(LeafClause {
left: Value::Field(Field::ReleaseYear),
operator: Operator::GreaterThan,
right: Value::Int(2020)
}),
],
kind: CompoundKind::And
})
},
"((title = \"foo\" AND (artist CONTAINSNOT \"bar\" OR album = \"baz\")) AND release_year > 2020)"
)]
fn compilables<T: Compile>(#[case] input: T, #[case] expected: &str) {}
#[apply(compilables)]
fn test_compile<T: Compile>(#[case] input: T, #[case] expected: &str) {
let compiled = input.compile(Context::Storage);
assert_eq!(compiled, expected);
}
#[rstest]
#[case::field(PhantomData::<Field>, "title", "title")]
#[case::field(PhantomData::<Field>, "artist", "array::flatten([artist][? $this])")]
#[case::field(PhantomData::<Field>, "album", "album")]
#[case::field(PhantomData::<Field>, "album_artist", "array::flatten([album_artist][? $this])")]
#[case::field(PhantomData::<Field>, "genre", "array::flatten([genre][? $this])")]
#[case::field(PhantomData::<Field>, "release_year", "release_year")]
#[case::compound_query(PhantomData::<CompoundClause>, "(title = \"foo\" AND \"bar\" INSIDE artist)", "(title = \"foo\" AND \"bar\" INSIDE array::flatten([artist][? $this]))")]
#[case::complex_query(PhantomData::<Query>, "((title = \"foo\" AND (artist CONTAINSNOT \"bar\" OR album = \"baz\")) AND release_year > 2020)", "((title = \"foo\" AND (array::flatten([artist][? $this]) CONTAINSNOT \"bar\" OR album = \"baz\")) AND release_year > 2020)")]
fn test_compile_for_execution<T>(
#[case] _phantom: PhantomData<T>,
#[case] storage: &str,
#[case] expected: &str,
) where
T: Compile + FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
let parsed = T::from_str(storage).unwrap();
let compiled = parsed.compile(Context::Execution);
assert_eq!(compiled, expected);
}
#[apply(compilables)]
fn test_display<T: Compile + std::fmt::Display>(#[case] input: T, #[case] expected: &str) {
let displayed = format!("{input}");
assert_eq!(displayed, expected);
}
#[apply(compilables)]
fn test_from_str<T: Compile + std::str::FromStr + std::cmp::PartialEq + std::fmt::Debug>(
#[case] expected: T,
#[case] input: &str,
) where
<T as std::str::FromStr>::Err: std::fmt::Debug + PartialEq,
{
let parsed = T::from_str(input);
assert_eq!(parsed, Ok(expected));
}
}
macro_rules! impl_from_str {
($(($t:ty, $p:expr)),*) => {
$(
impl std::str::FromStr for $t {
type Err = pom::Error;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
$p.parse(s.as_bytes())
}
}
)*
};
}
impl_from_str!(
(Operator, parser::operator()),
(Field, parser::field()),
(Value, parser::value()),
(LeafClause, parser::leaf()),
(CompoundClause, parser::compound()),
(Clause, parser::clause()),
(Query, parser::query())
);
mod parser {
use std::str::FromStr;
use pom::parser::{Parser, call, end, list, none_of, one_of, seq, sym};
use super::{Clause, CompoundClause, CompoundKind, Field, LeafClause, Operator, Query, Value};
pub fn query<'a>() -> Parser<'a, u8, Query> {
clause().map(|root| Query { root }).name("query") - end()
}
pub fn clause<'a>() -> Parser<'a, u8, Clause> {
compound().map(Clause::Compound) | leaf().map(Clause::Leaf).name("clause")
}
pub fn compound<'a>() -> Parser<'a, u8, CompoundClause> {
(sym(b'(')
* space()
* (call(clause) - space() + (seq(b"AND") | seq(b"OR")) - space() + call(clause)).map(
|((left, sep), right)| CompoundClause {
clauses: vec![left, right],
kind: match sep {
b"AND" => CompoundKind::And,
b"OR" => CompoundKind::Or,
_ => unreachable!(),
},
},
)
- space()
- sym(b')'))
.name("compound clause")
}
pub fn leaf<'a>() -> Parser<'a, u8, LeafClause> {
(value() - space() + operator() - space() + value())
.convert(|((left, operator), right)| {
let parsed = LeafClause {
left,
operator,
right,
};
if parsed.has_valid_operator() {
Ok(parsed)
} else {
Err(pom::Error::Conversion {
position: 0,
message: format!(
"Invalid operator ({op}) for values: {left:?}, {right:?}",
left = parsed.left,
op = parsed.operator,
right = parsed.right
),
})
}
})
.name("leaf clause")
}
pub fn value<'a>() -> Parser<'a, u8, Value> {
(string().map(Value::String)
| int().map(Value::Int)
| set().map(Value::Set)
| field().map(Value::Field))
.name("value")
}
pub fn field<'a>() -> Parser<'a, u8, Field> {
(seq(b"title").map(|_| Field::Title)
| seq(b"artist").map(|_| Field::Artists)
| seq(b"album_artist").map(|_| Field::AlbumArtists)
| seq(b"album").map(|_| Field::Album)
| seq(b"genre").map(|_| Field::Genre)
| seq(b"release_year").map(|_| Field::ReleaseYear))
.name("field")
}
pub fn operator<'a>() -> Parser<'a, u8, Operator> {
(seq(b"!=").map(|_| Operator::NotEqual)
| seq(b"?=").map(|_| Operator::AnyEqual)
| seq(b"*=").map(|_| Operator::AllEqual)
| seq(b"=").map(|_| Operator::Equal)
| seq(b">=").map(|_| Operator::GreaterThanOrEqual)
| seq(b">").map(|_| Operator::GreaterThan)
| seq(b"<=").map(|_| Operator::LessThanOrEqual)
| seq(b"<").map(|_| Operator::LessThan)
| seq(b"!~").map(|_| Operator::NotLike)
| seq(b"?~").map(|_| Operator::AnyLike)
| seq(b"*~").map(|_| Operator::AllLike)
| seq(b"~").map(|_| Operator::Like)
| seq(b"NOTINSIDE").map(|_| Operator::NotInside)
| seq(b"ALLINSIDE").map(|_| Operator::AllInside)
| seq(b"ANYINSIDE").map(|_| Operator::AnyInside)
| seq(b"NONEINSIDE").map(|_| Operator::NoneInside)
| seq(b"INSIDE").map(|_| Operator::Inside)
| seq(b"NOT IN").map(|_| Operator::NotIn)
| seq(b"IN").map(|_| Operator::In)
| seq(b"CONTAINSNOT").map(|_| Operator::ContainsNot)
| seq(b"CONTAINSALL").map(|_| Operator::ContainsAll)
| seq(b"CONTAINSANY").map(|_| Operator::ContainsAny)
| seq(b"CONTAINSNONE").map(|_| Operator::ContainsNone)
| seq(b"CONTAINS").map(|_| Operator::Contains))
.name("operator")
}
pub fn string<'a>() -> Parser<'a, u8, String> {
let string_pf = |quote_sym, escaped_quote| {
let special_char = sym(b'\\')
| sym(b'/')
| sym(quote_sym)
| sym(b'b').map(|_| b'\x08')
| sym(b'f').map(|_| b'\x0C')
| sym(b'n').map(|_| b'\n')
| sym(b'r').map(|_| b'\r')
| sym(b't').map(|_| b'\t');
let escape_sequence = sym(b'\\') * special_char;
let char_string = (none_of(escaped_quote) | escape_sequence)
.repeat(1..)
.convert(String::from_utf8);
sym(quote_sym) * char_string.repeat(0..) - sym(quote_sym)
};
let string = string_pf(b'"', b"\\\"") | string_pf(b'\'', b"\\'");
string.map(|strings| strings.concat()).name("string")
}
pub fn int<'a>() -> Parser<'a, u8, i64> {
let number = sym(b'-').opt() + one_of(b"0123456789").repeat(1..);
number
.collect()
.convert(std::str::from_utf8)
.convert(i64::from_str)
.name("int")
}
pub fn set<'a>() -> Parser<'a, u8, Vec<Value>> {
let elems = list(call(value), sym(b',') * space());
(sym(b'[') * space() * elems - sym(b']')).name("set")
}
pub fn space<'a>() -> Parser<'a, u8, ()> {
one_of(b" \t\r\n").repeat(0..).discard().name("space")
}
#[cfg(test)]
mod tests {
use crate::db::schemas::dynamic::query::Context;
use super::super::Compile;
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case(Ok(Operator::Equal), "=")]
#[case(Ok(Operator::NotEqual), "!=")]
#[case(Ok(Operator::AnyEqual), "?=")]
#[case(Ok(Operator::AllEqual), "*=")]
#[case(Ok(Operator::GreaterThan), ">")]
#[case(Ok(Operator::GreaterThanOrEqual), ">=")]
#[case(Ok(Operator::LessThan), "<")]
#[case(Ok(Operator::LessThanOrEqual), "<=")]
#[case(Ok(Operator::Like), "~")]
#[case(Ok(Operator::NotLike), "!~")]
#[case(Ok(Operator::AnyLike), "?~")]
#[case(Ok(Operator::AllLike), "*~")]
#[case(Ok(Operator::Inside), "INSIDE")]
#[case(Ok(Operator::NotInside), "NOTINSIDE")]
#[case(Ok(Operator::AllInside), "ALLINSIDE")]
#[case(Ok(Operator::AnyInside), "ANYINSIDE")]
#[case(Ok(Operator::NoneInside), "NONEINSIDE")]
#[case(Ok(Operator::In), "IN")]
#[case(Ok(Operator::NotIn), "NOT IN")]
#[case(Ok(Operator::Contains), "CONTAINS")]
#[case(Ok(Operator::ContainsNot), "CONTAINSNOT")]
#[case(Ok(Operator::ContainsAll), "CONTAINSALL")]
#[case(Ok(Operator::ContainsAny), "CONTAINSANY")]
#[case(Ok(Operator::ContainsNone), "CONTAINSNONE")]
#[case(
Err(pom::Error::Custom { message: "failed to parse operator".to_string(), position:0, inner: Some(Box::new(pom::Error::Mismatch { message: "seq [67, 79, 78, 84, 65, 73, 78, 83] expect: 67, found: 105".to_string(), position: 0 }))}),
"invalid"
)]
fn test_operator_parse_compile(
#[case] expected: Result<Operator, pom::Error>,
#[case] s: &str,
) {
let parsed = operator().parse(s.as_bytes());
assert_eq!(parsed, expected);
if let Ok(operator) = parsed {
let compiled = operator.compile(Context::Storage);
assert_eq!(compiled, s);
}
}
#[rstest]
#[case(Ok(Field::Title), "title")]
#[case(Ok(Field::Artists), "artist")]
#[case(Ok(Field::Album), "album")]
#[case(Ok(Field::AlbumArtists), "album_artist")]
#[case(Ok(Field::Genre), "genre")]
#[case(Ok(Field::ReleaseYear), "release_year")]
#[case(Err(pom::Error::Custom{ message: "failed to parse field".to_string(), position:0, inner: Some(Box::new(pom::Error::Mismatch { message: "seq [114, 101, 108, 101, 97, 115, 101, 95, 121, 101, 97, 114] expect: 114, found: 105".to_string(), position: 0 }))}), "invalid")]
fn test_field_parse_compile(#[case] expected: Result<Field, pom::Error>, #[case] s: &str) {
let parsed = field().parse(s.as_bytes());
assert_eq!(parsed, expected);
if let Ok(field) = parsed {
let compiled = field.compile(Context::Storage);
assert_eq!(compiled, s);
}
}
#[rstest]
#[case(Ok(Value::String("foo".to_string())), "\"foo\"")]
#[case(Ok(Value::Int(42)), "42")]
#[case(Ok(Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)])), "[\"foo\", 42]")]
#[case::nested(
Ok(Value::Set(vec![
Value::String("foo".to_string()),
Value::Set(vec![Value::String("bar".to_string()), Value::Int(42)])
])),
"[\"foo\", [\"bar\", 42]]"
)]
#[case(Ok(Value::Field(Field::Title)), "title")]
#[case(Ok(Value::Field(Field::Artists)), "artist")]
#[case(Ok(Value::Field(Field::Album)), "album")]
#[case(Ok(Value::Field(Field::AlbumArtists)), "album_artist")]
#[case(Ok(Value::Field(Field::Genre)), "genre")]
#[case(Ok(Value::Field(Field::ReleaseYear)), "release_year")]
#[case(Err(pom::Error::Custom {message: "failed to parse field".to_string(), position: 0, inner: Some(Box::new(pom::Error::Mismatch { message: "seq [114, 101, 108, 101, 97, 115, 101, 95, 121, 101, 97, 114] expect: 114, found: 34".to_string(), position: 0 }))}), "\"foo")]
#[case(Err(pom::Error::Custom {message: "failed to parse field".to_string(), position: 0, inner: Some(Box::new(pom::Error::Mismatch { message: "seq [114, 101, 108, 101, 97, 115, 101, 95, 121, 101, 97, 114] expect: 114, found: 91".to_string(), position: 0 }))}), "[foo, 42")]
#[case(Err(pom::Error::Custom {message: "failed to parse field".to_string(), position: 0, inner: Some(Box::new(pom::Error::Mismatch { message: "seq [114, 101, 108, 101, 97, 115, 101, 95, 121, 101, 97, 114] expect: 114, found: 105".to_string(), position: 0 }))}), "invalid")]
fn test_value_parse_compile(#[case] expected: Result<Value, pom::Error>, #[case] s: &str) {
let parsed = value().parse(s.as_bytes());
assert_eq!(parsed, expected);
if let Ok(value) = parsed {
let compiled = value.compile(Context::Storage);
assert_eq!(compiled, s);
}
}
#[rstest]
#[case(Ok(Value::String("foo bar".to_string())), "\"foo bar\"")]
#[case(Ok(Value::String("foo bar".to_string())), "'foo bar'")]
#[case(Err(pom::Error::Custom {message: "failed to parse field".to_string(), position: 0, inner: Some(Box::new(pom::Error::Mismatch { message: "seq [114, 101, 108, 101, 97, 115, 101, 95, 121, 101, 97, 114] expect: 114, found: 34".to_string(), position: 0 }))}), "\"foo")]
#[case(Err(pom::Error::Custom {message: "failed to parse field".to_string(), position: 0, inner: Some(Box::new(pom::Error::Mismatch { message: "seq [114, 101, 108, 101, 97, 115, 101, 95, 121, 101, 97, 114] expect: 114, found: 39".to_string(), position: 0 }))}), "'foo")]
fn test_value_parse_string(#[case] expected: Result<Value, pom::Error>, #[case] s: &str) {
let parsed = value().parse(s.as_bytes());
assert_eq!(parsed, expected);
}
#[rstest]
#[case(Ok(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}), "title = \"foo\"")]
#[case(Ok(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::Int(42)
}), "title = 42")]
#[case(Ok(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Inside,
right: Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)])
}), "title INSIDE [\"foo\", 42]")]
#[case(Err(
pom::Error::Custom {
message: "failed to parse leaf clause".to_string(),
position: 0,
inner: Some(Box::new(pom::Error::
Conversion {
message: "Conversion error: Conversion { message: \"Invalid operator (=) for values: Field(Title), Field(Artists)\", position: 0 }".to_string(),
position: 0,
}
)),
}
), "title = artist")]
#[case(Err(pom::Error::Custom{message:"failed to parse operator".to_string(),position:5, inner:Some(Box::new(pom::Error::Incomplete))}), "title")]
#[case(Err(pom::Error::Custom{message: "failed to parse field".to_string(),position: 0, inner: Some(Box::new(pom::Error:: Mismatch { message: "seq [114, 101, 108, 101, 97, 115, 101, 95, 121, 101, 97, 114] expect: 114, found: 32".to_string(), position: 0 }))}), " = \"foo\"")]
#[case(Err(pom::Error::Custom{message:"failed to parse field".to_string(),position:8, inner:Some(Box::new(pom::Error::Incomplete))}), "title = ")]
#[case(Err(pom::Error::Custom{message: "failed to parse operator".to_string(),position: 6, inner: Some(Box::new(pom::Error:: Mismatch { message: "seq [67, 79, 78, 84, 65, 73, 78, 83] expect: 67, found: 105".to_string(), position: 6 }))}), "title invalid \"foo\"")]
#[case::left_has_spaces(Ok(LeafClause {
left: Value::String("foo bar".to_string()),
operator: Operator::Equal,
right: Value::Int(42)
}), "\"foo bar\" = 42")]
#[case::operator_has_spaces(Ok(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::NotIn,
right: Value::String("foo bar".to_string())
}), "title NOT IN \"foo bar\"")]
fn test_leaf_clause_parse(
#[case] expected: Result<LeafClause, pom::Error>,
#[case] s: &str,
) {
let parsed = leaf().parse(s.as_bytes());
assert_eq!(parsed, expected);
if let Ok(clause) = parsed {
let compiled = clause.compile(Context::Storage);
assert_eq!(compiled, s);
}
}
#[rstest]
#[case::value_to_value("artist = \"foo\"", Err(pom::Error::Custom {message:"failed to parse leaf clause".to_string(), position: 0, inner: Some(Box::new(pom::Error::Conversion { position: 0, message: "Conversion error: Conversion { message: \"Invalid operator (=) for values: Field(Artists), String(\\\"foo\\\")\", position: 0 }".to_string() }))}))]
#[case::value_to_value("title = \"foo\"", Ok(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}))]
#[case::value_to_set("42 IN [\"foo\", 42]", Ok(LeafClause {
left: Value::Int(42),
operator: Operator::In,
right: Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)])
}))]
#[case::set_to_value("[\"foo\", 42] CONTAINS 42", Ok(LeafClause {
left: Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)]),
operator: Operator::Contains,
right: Value::Int(42)
}))]
#[case::set_to_set("[\"foo\", 42] CONTAINSALL [\"foo\", 42]", Ok(LeafClause {
left: Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)]),
operator: Operator::ContainsAll,
right: Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)])
}))]
#[case::string_to_string("\"foo\" IN \"foo\"", Ok(LeafClause {
left: Value::String("foo".to_string()),
operator: Operator::In,
right: Value::String("foo".to_string())
}))]
fn test_operator_checking(
#[case] input: &str,
#[case] expected: Result<LeafClause, pom::Error>,
) {
let parsed = leaf().parse(input.as_bytes());
assert_eq!(parsed, expected);
}
#[rstest]
#[case(Ok(CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::AllLike,
right: Value::String("bar".to_string())
}),
],
kind: CompoundKind::And
}), "(title = \"foo\" AND artist *~ \"bar\")")]
#[case(Ok(CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::AnyLike,
right: Value::String("bar".to_string())
}),
],
kind: CompoundKind::Or
}), "(title = \"foo\" OR artist ?~ \"bar\")")]
#[case(Err(pom::Error::Custom { message: "failed to parse compound clause".to_string(), position: 0, inner: Some(Box::new(pom::Error::Incomplete))}), "(title = \"foo\"")]
fn test_compound_clause_parse(
#[case] expected: Result<CompoundClause, pom::Error>,
#[case] s: &str,
) {
let parsed = compound().parse(s.as_bytes());
assert_eq!(parsed, expected);
if let Ok(clause) = parsed {
let compiled = clause.compile(Context::Storage);
assert_eq!(compiled, s);
}
}
#[rstest]
#[case(Ok(Query {
root: Clause::Compound(CompoundClause {
clauses: vec![
Clause::Compound(
CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}),
Clause::Compound(CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::Contains,
right: Value::String("bar".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Album),
operator: Operator::Equal,
right: Value::String("baz".to_string())
}),
],
kind: CompoundKind::Or
}),
],
kind: CompoundKind::And
}
),
Clause::Leaf(LeafClause {
left: Value::Field(Field::ReleaseYear),
operator: Operator::GreaterThan,
right: Value::Int(2020)
}),
],
kind: CompoundKind::And
})
},), "((title = \"foo\" AND (artist CONTAINS \"bar\" OR album = \"baz\")) AND release_year > 2020)")]
fn test_query_parse(#[case] expected: Result<Query, pom::Error>, #[case] s: &str) {
let parsed = query().parse(s.as_bytes());
assert_eq!(parsed, expected);
if let Ok(query) = parsed {
let compiled = query.compile(Context::Storage);
assert_eq!(compiled, s);
}
}
}
}