use indexmap::IndexMap;
use parse_rust_core::{ParseError, ParseValue};
#[derive(Debug, Clone)]
pub enum Comparison {
Equal(ParseValue),
EqualOperator(ParseValue),
NotEqual(ParseValue),
GreaterThan(ParseValue),
GreaterThanOrEqual(ParseValue),
LessThan(ParseValue),
LessThanOrEqual(ParseValue),
In(Vec<ParseValue>),
NotIn(Vec<ParseValue>),
Exists(bool),
All(Vec<ParseValue>),
Regex {
pattern: String,
options: Option<String>,
},
}
impl Comparison {
pub fn from_operator(op: &str, value: ParseValue) -> Result<Self, ParseError> {
Ok(match op {
"$eq" => Comparison::EqualOperator(value),
"$ne" => Comparison::NotEqual(value),
"$gt" => Comparison::GreaterThan(value),
"$gte" => Comparison::GreaterThanOrEqual(value),
"$lt" => Comparison::LessThan(value),
"$lte" => Comparison::LessThanOrEqual(value),
"$in" | "$nin" | "$all" => {
let items = match value {
ParseValue::Array(items) => items,
_ => {
return Err(ParseError::invalid_query(format!(
"bad {op} value: expected an array"
)))
}
};
match op {
"$in" => Comparison::In(items),
"$nin" => Comparison::NotIn(items),
_ => Comparison::All(items),
}
}
"$exists" => match value {
ParseValue::Bool(b) => Comparison::Exists(b),
_ => {
return Err(ParseError::invalid_query(
"bad $exists value: expected a boolean".to_string(),
))
}
},
other => {
return Err(ParseError::invalid_query(format!(
"unsupported query operator: {other}"
)))
}
})
}
}
#[derive(Debug, Clone)]
pub struct Constraint {
pub field: String,
pub comparison: Comparison,
}
impl Constraint {
pub fn equal(field: impl Into<String>, value: ParseValue) -> Self {
Self {
field: field.into(),
comparison: Comparison::Equal(value),
}
}
pub fn one_of(field: impl Into<String>, values: Vec<ParseValue>) -> Self {
Self {
field: field.into(),
comparison: Comparison::In(values),
}
}
}
#[derive(Debug, Clone)]
pub enum Clause {
Field(Constraint),
Or(Vec<Query>),
And(Vec<Query>),
Nor(Vec<Query>),
}
#[derive(Debug, Clone, Default)]
pub struct Query {
pub clauses: Vec<Clause>,
}
impl Query {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.clauses.is_empty()
}
pub fn push(&mut self, clause: Clause) {
self.clauses.push(clause);
}
pub fn push_constraint(&mut self, constraint: Constraint) {
self.clauses.push(Clause::Field(constraint));
}
pub fn extend(&mut self, other: Query) {
self.clauses.extend(other.clauses);
}
pub fn conjoin(&mut self, other: Query) {
let mut nested = Vec::new();
for clause in other.clauses {
match &clause {
Clause::Field(constraint) if self.constrains_field(&constraint.field) => {
nested.push(Query {
clauses: vec![clause],
});
}
_ => self.clauses.push(clause),
}
}
if !nested.is_empty() {
self.clauses.push(Clause::And(nested));
}
}
pub fn constrains_field(&self, field: &str) -> bool {
self.top_level_constraints().any(|c| c.field == field)
}
pub fn any_of(alternatives: Vec<Query>) -> Query {
let mut alternatives: Vec<Query> =
alternatives.into_iter().filter(|q| !q.is_empty()).collect();
match alternatives.len() {
0 => Query::new(),
1 => alternatives.remove(0),
_ => {
let mut q = Query::new();
q.push(Clause::Or(alternatives));
q
}
}
}
pub fn from_constraints(constraints: Vec<Constraint>) -> Query {
Query {
clauses: constraints.into_iter().map(Clause::Field).collect(),
}
}
pub fn top_level_constraints(&self) -> impl Iterator<Item = &Constraint> {
self.clauses.iter().filter_map(|c| match c {
Clause::Field(f) => Some(f),
_ => None,
})
}
}
impl From<Vec<Constraint>> for Query {
fn from(constraints: Vec<Constraint>) -> Self {
Query::from_constraints(constraints)
}
}
#[derive(Debug, Clone)]
pub enum UpdateValue {
SetOnInsert(ParseValue),
Set(ParseValue),
Increment(f64),
Add(Vec<ParseValue>),
AddUnique(Vec<ParseValue>),
Remove(Vec<ParseValue>),
Unset,
}
impl UpdateValue {
pub fn echoes_result(&self) -> bool {
!matches!(self, UpdateValue::Set(_) | UpdateValue::Unset)
}
}
pub type Update = IndexMap<String, UpdateValue>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortDirection {
Ascending,
Descending,
}
pub const DEFAULT_LIMIT: u32 = 100;
#[derive(Debug, Clone)]
pub struct QueryOptions {
pub limit: Option<u32>,
pub skip: Option<u32>,
pub order: Vec<(String, SortDirection)>,
pub keys: Option<Vec<String>>,
pub case_insensitive: bool,
}
impl Default for QueryOptions {
fn default() -> Self {
Self {
limit: Some(DEFAULT_LIMIT),
skip: None,
order: Vec::new(),
keys: None,
case_insensitive: false,
}
}
}
impl QueryOptions {
pub fn parse_order(order: &str) -> Vec<(String, SortDirection)> {
order
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|k| match k.strip_prefix('-') {
Some(rest) => (rest.to_string(), SortDirection::Descending),
None => (k.to_string(), SortDirection::Ascending),
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn supported_operators_map() {
for op in ["$ne", "$gt", "$gte", "$lt", "$lte"] {
assert!(
Comparison::from_operator(op, ParseValue::Number(1.0)).is_ok(),
"{op}"
);
}
assert!(Comparison::from_operator("$in", ParseValue::Array(vec![])).is_ok());
assert!(Comparison::from_operator("$nin", ParseValue::Array(vec![])).is_ok());
assert!(Comparison::from_operator("$all", ParseValue::Array(vec![])).is_ok());
assert!(Comparison::from_operator("$exists", ParseValue::Bool(true)).is_ok());
}
#[test]
fn an_unsupported_operator_is_an_error_not_a_no_op() {
for op in [
"$select",
"$dontSelect",
"$inQuery",
"$notInQuery",
"$nearSphere",
"$text",
"$containedBy",
"$geoWithin",
] {
let e = Comparison::from_operator(op, ParseValue::Null).unwrap_err();
assert_eq!(e.code, parse_rust_core::ErrorCode::InvalidQuery, "{op}");
assert!(e.message.contains(op), "the message must name the operator");
}
}
#[test]
fn in_requires_an_array_and_exists_requires_a_boolean() {
assert!(Comparison::from_operator("$in", ParseValue::Number(1.0)).is_err());
assert!(Comparison::from_operator("$all", ParseValue::Number(1.0)).is_err());
assert!(Comparison::from_operator("$exists", ParseValue::Number(1.0)).is_err());
}
#[test]
fn the_default_limit_is_a_hundred_not_unlimited() {
assert_eq!(QueryOptions::default().limit, Some(DEFAULT_LIMIT));
assert_eq!(DEFAULT_LIMIT, 100);
}
#[test]
fn order_parsing_handles_the_minus_prefix() {
assert_eq!(
QueryOptions::parse_order("name,-createdAt, score"),
vec![
("name".to_string(), SortDirection::Ascending),
("createdAt".to_string(), SortDirection::Descending),
("score".to_string(), SortDirection::Ascending),
]
);
assert!(QueryOptions::parse_order("").is_empty());
}
#[test]
fn a_single_alternative_disjunction_collapses() {
let one = Query::from_constraints(vec![Constraint::equal(
"owner",
ParseValue::String("u1".into()),
)]);
let q = Query::any_of(vec![one]);
assert_eq!(q.clauses.len(), 1);
assert!(matches!(q.clauses[0], Clause::Field(_)));
let two = Query::any_of(vec![
Query::from_constraints(vec![Constraint::equal("a", ParseValue::Null)]),
Query::from_constraints(vec![Constraint::equal("b", ParseValue::Null)]),
]);
assert!(matches!(two.clauses.as_slice(), [Clause::Or(alts)] if alts.len() == 2));
}
#[test]
fn an_empty_alternative_is_dropped_and_an_empty_disjunction_is_unconstrained() {
assert!(Query::any_of(vec![]).is_empty());
assert!(Query::any_of(vec![Query::new(), Query::new()]).is_empty());
}
#[test]
fn conjoin_nests_a_colliding_field_and_splices_everything_else() {
let mut client = Query::from_constraints(vec![Constraint::equal(
"owner",
ParseValue::String("u1".into()),
)]);
client.conjoin(Query::from_constraints(vec![
Constraint::equal("owner", ParseValue::String("u1".into())),
Constraint::equal("state", ParseValue::String("open".into())),
]));
assert!(matches!(
client.clauses.as_slice(),
[Clause::Field(a), Clause::Field(b), Clause::And(nested)]
if a.field == "owner" && b.field == "state" && nested.len() == 1
));
}
#[test]
fn conjoin_stays_flat_when_no_field_collides() {
let mut q = Query::from_constraints(vec![Constraint::equal(
"title",
ParseValue::String("a".into()),
)]);
q.conjoin(Query::from_constraints(vec![Constraint::equal(
"owner",
ParseValue::String("u1".into()),
)]));
assert!(matches!(
q.clauses.as_slice(),
[Clause::Field(_), Clause::Field(_)]
));
}
#[test]
fn conjoin_only_looks_at_top_level_fields() {
let mut q = Query::new();
q.push(Clause::Or(vec![Query::from_constraints(vec![
Constraint::equal("owner", ParseValue::String("u2".into())),
])]));
q.conjoin(Query::from_constraints(vec![Constraint::equal(
"owner",
ParseValue::String("u1".into()),
)]));
assert!(matches!(
q.clauses.as_slice(),
[Clause::Or(_), Clause::Field(_)]
));
}
#[test]
fn only_ops_echo_their_result_back() {
assert!(!UpdateValue::Set(ParseValue::Null).echoes_result());
assert!(!UpdateValue::Unset.echoes_result());
assert!(UpdateValue::Increment(1.0).echoes_result());
assert!(UpdateValue::Add(vec![]).echoes_result());
assert!(UpdateValue::AddUnique(vec![]).echoes_result());
assert!(UpdateValue::Remove(vec![]).echoes_result());
}
}