use parse_rust_core::{ParseError, ParseValue};
#[derive(Debug, Clone)]
pub enum Comparison {
Equal(ParseValue),
NotEqual(ParseValue),
GreaterThan(ParseValue),
GreaterThanOrEqual(ParseValue),
LessThan(ParseValue),
LessThanOrEqual(ParseValue),
In(Vec<ParseValue>),
NotIn(Vec<ParseValue>),
Exists(bool),
}
impl Comparison {
pub fn from_operator(op: &str, value: ParseValue) -> Result<Self, ParseError> {
Ok(match op {
"$ne" => Comparison::NotEqual(value),
"$gt" => Comparison::GreaterThan(value),
"$gte" => Comparison::GreaterThanOrEqual(value),
"$lt" => Comparison::LessThan(value),
"$lte" => Comparison::LessThanOrEqual(value),
"$in" | "$nin" => {
let items = match value {
ParseValue::Array(items) => items,
_ => {
return Err(ParseError::invalid_query(format!(
"bad {op} value: expected an array"
)))
}
};
if op == "$in" {
Comparison::In(items)
} else {
Comparison::NotIn(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),
}
}
}
#[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>>,
}
impl Default for QueryOptions {
fn default() -> Self {
Self {
limit: Some(DEFAULT_LIMIT),
skip: None,
order: Vec::new(),
keys: None,
}
}
}
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("$exists", ParseValue::Bool(true)).is_ok());
}
#[test]
fn an_unsupported_operator_is_an_error_not_a_no_op() {
for op in [
"$regex",
"$select",
"$inQuery",
"$all",
"$nearSphere",
"$text",
] {
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("$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());
}
}