use super::types::{CompoundFilter, Filter, FilterExpr, Operator, Value};
use miniserde::json::{Number, Value as JsonValue};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
InvalidJson,
UnknownOperator(String),
ExpectedObject,
ExpectedArray,
ExpectedValue,
EmptyFieldName,
EmptyFilter,
InvalidOperatorValue {
op: String,
expected: &'static str,
},
NotRequiresOneCondition,
}
pub fn parse_filter(json_str: &str) -> Result<FilterExpr, ParseError> {
FilterExpr::parse(json_str)
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidJson => write!(f, "Invalid JSON syntax or encoding"),
Self::UnknownOperator(op) => write!(f, "Unknown operator '{op}'"),
Self::ExpectedObject => write!(f, "Expected JSON object"),
Self::ExpectedArray => write!(f, "Expected JSON array"),
Self::ExpectedValue => write!(f, "Expected a value"),
Self::EmptyFieldName => write!(f, "Field name cannot be empty"),
Self::EmptyFilter => write!(f, "Filter object cannot be empty"),
Self::InvalidOperatorValue { op, expected } => {
write!(f, "Operator '{op}' expects {expected}")
},
Self::NotRequiresOneCondition => {
write!(f, "$not requires exactly one condition")
},
}
}
}
impl std::error::Error for ParseError {}
impl Operator {
#[must_use]
pub fn from_mongo(s: &str) -> Option<Self> {
let s = s.strip_prefix('$').unwrap_or(s);
match s {
"eq" => Some(Self::Eq),
"ne" => Some(Self::Ne),
"gt" => Some(Self::Gt),
"gte" => Some(Self::Gte),
"lt" => Some(Self::Lt),
"lte" => Some(Self::Lte),
"in" => Some(Self::In),
"nin" => Some(Self::NotIn),
"like" => Some(Self::Like),
"ilike" => Some(Self::ILike),
"regex" => Some(Self::Regex),
"startsWith" | "starts_with" => Some(Self::StartsWith),
"endsWith" | "ends_with" => Some(Self::EndsWith),
"contains" => Some(Self::Contains),
"between" => Some(Self::Between),
_ => None,
}
}
}
impl Value {
#[must_use]
pub fn from_json(json: &JsonValue) -> Option<Self> {
match json {
JsonValue::Null => Some(Self::Null),
JsonValue::Bool(b) => Some(Self::Bool(*b)),
JsonValue::Number(n) => match n {
Number::I64(i) => Some(Self::Int(*i)),
Number::U64(u) => i64::try_from(*u).ok().map(Self::Int),
Number::F64(f) => Some(Self::Float(*f)),
},
JsonValue::String(s) => Some(Self::String(s.clone())),
JsonValue::Array(arr) => {
let values: Option<Vec<Self>> = arr.iter().map(Self::from_json).collect();
values.map(Self::Array)
},
JsonValue::Object(_) => None, }
}
}
impl FilterExpr {
pub fn parse(json_str: &str) -> Result<Self, ParseError> {
let json: JsonValue =
miniserde::json::from_str(json_str).map_err(|_| ParseError::InvalidJson)?;
Self::from_json(&json)
}
pub fn parse_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
let s = std::str::from_utf8(bytes).map_err(|_| ParseError::InvalidJson)?;
Self::parse(s)
}
pub fn from_json(json: &JsonValue) -> Result<Self, ParseError> {
let obj = match json {
JsonValue::Object(o) => o,
_ => return Err(ParseError::ExpectedObject),
};
if obj.is_empty() {
return Err(ParseError::EmptyFilter);
}
let mut filters = Vec::new();
for (key, value) in obj {
if key.is_empty() {
return Err(ParseError::EmptyFieldName);
}
if key.starts_with('$') {
match key.as_str() {
"$and" => {
let exprs = parse_filter_array(value)?;
filters.push(Self::Compound(CompoundFilter::and(exprs)));
},
"$or" => {
let exprs = parse_filter_array(value)?;
filters.push(Self::Compound(CompoundFilter::or(exprs)));
},
"$not" => {
let inner = Self::from_json(value)?;
filters.push(Self::Compound(CompoundFilter::not(inner)));
},
_ => return Err(ParseError::UnknownOperator(key.clone())),
}
} else {
let filter = parse_field_filter(key, value)?;
filters.push(filter);
}
}
Ok(match filters.len() {
0 => return Err(ParseError::EmptyFilter),
1 => filters.remove(0),
_ => Self::Compound(CompoundFilter::and(filters)),
})
}
}
fn parse_filter_array(json: &JsonValue) -> Result<Vec<FilterExpr>, ParseError> {
let arr = match json {
JsonValue::Array(a) => a,
_ => return Err(ParseError::ExpectedArray),
};
arr.iter().map(FilterExpr::from_json).collect()
}
fn parse_field_filter(field: &str, value: &JsonValue) -> Result<FilterExpr, ParseError> {
if let JsonValue::Object(obj) = value {
if let Some((op_key, op_value)) = obj.iter().next()
&& op_key.starts_with('$')
{
let op = Operator::from_mongo(op_key)
.ok_or_else(|| ParseError::UnknownOperator(op_key.clone()))?;
let val = parse_operator_value(op, op_value)?;
return Ok(FilterExpr::Simple(Filter {
field: field.to_string(),
op,
value: val,
}));
}
return Err(ParseError::ExpectedValue);
}
let val = Value::from_json(value).ok_or(ParseError::ExpectedValue)?;
Ok(FilterExpr::Simple(Filter {
field: field.to_string(),
op: Operator::Eq,
value: val,
}))
}
fn parse_operator_value(op: Operator, value: &JsonValue) -> Result<Value, ParseError> {
match op {
Operator::In | Operator::NotIn => match value {
JsonValue::Array(arr) => {
let values: Option<Vec<Value>> = arr.iter().map(Value::from_json).collect();
values
.map(Value::Array)
.ok_or_else(|| ParseError::InvalidOperatorValue {
op: format!("${op:?}").to_lowercase(),
expected: "array of values",
})
},
_ => Err(ParseError::InvalidOperatorValue {
op: "$in/$nin".to_string(),
expected: "array",
}),
},
Operator::Between => match value {
JsonValue::Array(arr) if arr.len() == 2 => {
let values: Option<Vec<Value>> = arr.iter().map(Value::from_json).collect();
values
.map(Value::Array)
.ok_or_else(|| ParseError::InvalidOperatorValue {
op: "$between".to_string(),
expected: "array of 2 values",
})
},
JsonValue::Array(_) => Err(ParseError::InvalidOperatorValue {
op: "$between".to_string(),
expected: "array of exactly 2 values",
}),
_ => Err(ParseError::InvalidOperatorValue {
op: "$between".to_string(),
expected: "array of 2 values",
}),
},
_ => Value::from_json(value).ok_or(ParseError::ExpectedValue),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LogicalOp;
use miniserde::json::{self, Array as JsonArray};
#[test]
fn test_operator_from_mongo_with_prefix() {
assert_eq!(Operator::from_mongo("$eq"), Some(Operator::Eq));
assert_eq!(Operator::from_mongo("$ne"), Some(Operator::Ne));
assert_eq!(Operator::from_mongo("$gt"), Some(Operator::Gt));
assert_eq!(Operator::from_mongo("$gte"), Some(Operator::Gte));
assert_eq!(Operator::from_mongo("$lt"), Some(Operator::Lt));
assert_eq!(Operator::from_mongo("$lte"), Some(Operator::Lte));
assert_eq!(Operator::from_mongo("$in"), Some(Operator::In));
assert_eq!(Operator::from_mongo("$nin"), Some(Operator::NotIn));
assert_eq!(Operator::from_mongo("$like"), Some(Operator::Like));
assert_eq!(Operator::from_mongo("$ilike"), Some(Operator::ILike));
assert_eq!(Operator::from_mongo("$regex"), Some(Operator::Regex));
assert_eq!(Operator::from_mongo("$between"), Some(Operator::Between));
}
#[test]
fn test_operator_from_mongo_without_prefix() {
assert_eq!(Operator::from_mongo("eq"), Some(Operator::Eq));
assert_eq!(Operator::from_mongo("gte"), Some(Operator::Gte));
}
#[test]
fn test_operator_from_mongo_camel_case() {
assert_eq!(
Operator::from_mongo("$startsWith"),
Some(Operator::StartsWith)
);
assert_eq!(
Operator::from_mongo("$starts_with"),
Some(Operator::StartsWith)
);
assert_eq!(Operator::from_mongo("$endsWith"), Some(Operator::EndsWith));
assert_eq!(Operator::from_mongo("$ends_with"), Some(Operator::EndsWith));
}
#[test]
fn test_operator_from_mongo_unknown() {
assert_eq!(Operator::from_mongo("$unknown"), None);
assert_eq!(Operator::from_mongo("$foo"), None);
}
#[test]
fn test_value_from_json_primitives() {
assert_eq!(Value::from_json(&JsonValue::Null), Some(Value::Null));
assert_eq!(
Value::from_json(&JsonValue::Bool(true)),
Some(Value::Bool(true))
);
assert_eq!(
Value::from_json(&JsonValue::Number(Number::I64(42))),
Some(Value::Int(42))
);
assert_eq!(
Value::from_json(&JsonValue::Number(Number::F64(2.5))),
Some(Value::Float(2.5))
);
assert_eq!(
Value::from_json(&JsonValue::String("hello".into())),
Some(Value::String("hello".into()))
);
}
#[test]
fn test_value_from_json_array() {
let mut arr = JsonArray::new();
arr.push(JsonValue::String("a".into()));
arr.push(JsonValue::String("b".into()));
let json_arr = JsonValue::Array(arr);
assert_eq!(
Value::from_json(&json_arr),
Some(Value::Array(vec![
Value::String("a".into()),
Value::String("b".into()),
]))
);
}
#[test]
fn test_simple_equality() {
let json: JsonValue = json::from_str(r#"{"name": "Alice"}"#).unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Simple(Filter {
ref field,
op: Operator::Eq,
value: Value::String(ref s),
}) if field == "name" && s == "Alice"
));
}
#[test]
fn test_explicit_operator() {
let json: JsonValue = json::from_str(r#"{"age": {"$gte": 18}}"#).unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Simple(Filter {
ref field,
op: Operator::Gte,
value: Value::Int(18),
}) if field == "age"
));
}
#[test]
fn test_multiple_fields_implicit_and() {
let json: JsonValue = json::from_str(r#"{"name": "Alice", "age": 30}"#).unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Compound(CompoundFilter {
op: LogicalOp::And,
..
})
));
}
#[test]
fn test_explicit_and() {
let json: JsonValue =
json::from_str(r#"{"$and": [{"name": "Alice"}, {"age": 30}]}"#).unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Compound(CompoundFilter {
op: LogicalOp::And,
..
})
));
}
#[test]
fn test_explicit_or() {
let json: JsonValue =
json::from_str(r#"{"$or": [{"status": "active"}, {"status": "pending"}]}"#).unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Compound(CompoundFilter {
op: LogicalOp::Or,
..
})
));
}
#[test]
fn test_explicit_not() {
let json: JsonValue = json::from_str(r#"{"$not": {"deleted": true}}"#).unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Compound(CompoundFilter {
op: LogicalOp::Not,
..
})
));
}
#[test]
fn test_in_operator() {
let json: JsonValue = json::from_str(r#"{"status": {"$in": ["a", "b", "c"]}}"#).unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Simple(Filter {
op: Operator::In,
value: Value::Array(_),
..
})
));
}
#[test]
fn test_between_operator() {
let json: JsonValue = json::from_str(r#"{"age": {"$between": [18, 65]}}"#).unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Simple(Filter {
op: Operator::Between,
value: Value::Array(ref arr),
..
}) if arr.len() == 2
));
}
#[test]
fn test_nested_logical() {
let json: JsonValue = json::from_str(
r#"{"$and": [{"active": true}, {"$or": [{"role": "admin"}, {"role": "mod"}]}]}"#,
)
.unwrap();
let filter = FilterExpr::from_json(&json).unwrap();
assert!(matches!(
filter,
FilterExpr::Compound(CompoundFilter {
op: LogicalOp::And,
..
})
));
}
#[test]
fn test_error_not_object() {
let json: JsonValue = json::from_str(r"[1, 2, 3]").unwrap();
assert!(matches!(
FilterExpr::from_json(&json),
Err(ParseError::ExpectedObject)
));
}
#[test]
fn test_error_empty_filter() {
let json: JsonValue = json::from_str(r"{}").unwrap();
assert!(matches!(
FilterExpr::from_json(&json),
Err(ParseError::EmptyFilter)
));
}
#[test]
fn test_error_unknown_operator() {
let json: JsonValue = json::from_str(r#"{"field": {"$foo": 1}}"#).unwrap();
assert!(matches!(
FilterExpr::from_json(&json),
Err(ParseError::UnknownOperator(_))
));
}
#[test]
fn test_error_between_wrong_count() {
let json: JsonValue = json::from_str(r#"{"age": {"$between": [18]}}"#).unwrap();
assert!(matches!(
FilterExpr::from_json(&json),
Err(ParseError::InvalidOperatorValue { .. })
));
}
#[test]
fn test_error_in_not_array() {
let json: JsonValue = json::from_str(r#"{"status": {"$in": "not-array"}}"#).unwrap();
assert!(matches!(
FilterExpr::from_json(&json),
Err(ParseError::InvalidOperatorValue { .. })
));
}
}