use bson::{Bson, Document, doc};
use vantage_core::{Result, error};
use vantage_vista::{Capability, FilterOp, NativeCondition, unsupported};
use crate::condition::MongoCondition;
use crate::id::{eq_filter, id_variants};
use crate::mongodb::impls::table_source::regex_escape;
fn with_counterpart(value: &Bson) -> Vec<Bson> {
let mut out = vec![value.clone()];
out.extend(id_variants(value));
out
}
fn expand_all(items: &[Bson]) -> Vec<Bson> {
items.iter().flat_map(with_counterpart).collect()
}
pub(crate) fn op_filter(path: &str, op: FilterOp, value: &Bson) -> Result<Document> {
let filter = match op {
FilterOp::Eq => eq_filter(path, value),
FilterOp::Ne => match id_variants(value) {
Some(alt) => doc! { path: { "$nin": [value.clone(), alt] } },
None => doc! { path: { "$ne": value.clone() } },
},
FilterOp::Gt => doc! { path: { "$gt": value.clone() } },
FilterOp::Gte => doc! { path: { "$gte": value.clone() } },
FilterOp::Lt => doc! { path: { "$lt": value.clone() } },
FilterOp::Lte => doc! { path: { "$lte": value.clone() } },
FilterOp::InSet => doc! { path: { "$in": operand_array(op, value)? } },
FilterOp::NotInSet => doc! { path: { "$nin": operand_array(op, value)? } },
FilterOp::Like => {
let text = vantage_vista::operand_text(&crate::vista::cbor::bson_to_cbor(value));
doc! { path: { "$regex": regex_escape(&text), "$options": "i" } }
}
_ => return unsupported(Capability::FilterOps),
};
Ok(filter)
}
pub(crate) fn search_filter(paths: &[String], text: &str) -> Document {
let escaped = regex_escape(text);
let mut clauses: Vec<Document> = paths
.iter()
.map(|path| doc! { path: { "$regex": &escaped, "$options": "i" } })
.collect();
match clauses.len() {
0 => doc! { "_id": { "$exists": false } },
1 => clauses.remove(0),
_ => doc! { "$or": clauses },
}
}
pub(crate) fn native_condition(condition: NativeCondition) -> Result<MongoCondition> {
let condition = match condition.downcast::<Document>() {
Ok(doc) => return Ok(MongoCondition::Doc(doc)),
Err(other) => other,
};
condition.downcast::<MongoCondition>().map_err(|other| {
error!(
"MongoDB takes a bson::Document or MongoCondition as a native condition",
key = other.key()
)
.mark_incorrect_usage()
})
}
#[cfg(any(feature = "rhai", test))]
pub(crate) fn native_key(filter: &Document) -> String {
Bson::Document(filter.clone())
.into_canonical_extjson()
.to_string()
}
fn operand_array(op: FilterOp, value: &Bson) -> Result<Vec<Bson>> {
match value {
Bson::Array(items) => Ok(expand_all(items)),
_ => Err(error!(
"set operator needs an array operand",
operator = format!("{op:?}")
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use bson::oid::ObjectId;
fn s(v: &str) -> Bson {
Bson::String(v.into())
}
#[test]
fn comparison_operators_render() {
let v = Bson::Int32(100);
assert_eq!(
op_filter("price", FilterOp::Eq, &v).unwrap(),
doc! { "price": 100 }
);
assert_eq!(
op_filter("price", FilterOp::Ne, &v).unwrap(),
doc! { "price": { "$ne": 100 } }
);
assert_eq!(
op_filter("price", FilterOp::Gt, &v).unwrap(),
doc! { "price": { "$gt": 100 } }
);
assert_eq!(
op_filter("price", FilterOp::Gte, &v).unwrap(),
doc! { "price": { "$gte": 100 } }
);
assert_eq!(
op_filter("price", FilterOp::Lt, &v).unwrap(),
doc! { "price": { "$lt": 100 } }
);
assert_eq!(
op_filter("price", FilterOp::Lte, &v).unwrap(),
doc! { "price": { "$lte": 100 } }
);
}
#[test]
fn set_operators_render() {
let v = Bson::Array(vec![s("a"), s("b")]);
assert_eq!(
op_filter("n", FilterOp::InSet, &v).unwrap(),
doc! { "n": { "$in": ["a", "b"] } }
);
assert_eq!(
op_filter("n", FilterOp::NotInSet, &v).unwrap(),
doc! { "n": { "$nin": ["a", "b"] } }
);
}
#[test]
fn set_operator_rejects_non_array() {
assert!(op_filter("n", FilterOp::InSet, &s("a")).is_err());
assert!(op_filter("n", FilterOp::NotInSet, &Bson::Int32(1)).is_err());
}
#[test]
fn like_escapes_and_ignores_case() {
assert_eq!(
op_filter("name", FilterOp::Like, &s("a.b")).unwrap(),
doc! { "name": { "$regex": "a\\.b", "$options": "i" } }
);
}
#[test]
fn search_spans_paths_with_escaped_literal() {
let paths = vec!["name".to_string(), "address.city".to_string()];
assert_eq!(
search_filter(&paths, "a.b"),
doc! { "$or": [
{ "name": { "$regex": "a\\.b", "$options": "i" } },
{ "address.city": { "$regex": "a\\.b", "$options": "i" } },
] }
);
}
#[test]
fn search_without_paths_matches_nothing() {
assert_eq!(
search_filter(&[], "x"),
doc! { "_id": { "$exists": false } }
);
}
#[test]
fn eq_on_hex_string_matches_both_spellings() {
let oid = ObjectId::new();
assert_eq!(
op_filter("_id", FilterOp::Eq, &s(&oid.to_hex())).unwrap(),
doc! { "_id": { "$in": [oid.to_hex(), oid] } }
);
}
#[test]
fn ne_on_object_id_excludes_both_spellings() {
let oid = ObjectId::new();
assert_eq!(
op_filter("_id", FilterOp::Ne, &Bson::ObjectId(oid)).unwrap(),
doc! { "_id": { "$nin": [oid, oid.to_hex()] } }
);
}
#[test]
fn in_set_extends_ids_with_counterparts() {
let oid = ObjectId::new();
let v = Bson::Array(vec![s(&oid.to_hex()), s("plain")]);
assert_eq!(
op_filter("_id", FilterOp::InSet, &v).unwrap(),
doc! { "_id": { "$in": [oid.to_hex(), oid, "plain"] } }
);
}
#[test]
fn native_condition_accepts_document_and_mongo_condition() {
let from_doc = native_condition(NativeCondition::new(doc! { "a": 1 }, "a")).unwrap();
assert!(matches!(from_doc, MongoCondition::Doc(d) if d == doc! { "a": 1 }));
let from_cond = native_condition(NativeCondition::new(
MongoCondition::Doc(doc! { "b": 2 }),
"b",
))
.unwrap();
assert!(matches!(from_cond, MongoCondition::Doc(d) if d == doc! { "b": 2 }));
}
#[test]
fn native_condition_rejects_other_types() {
let err = native_condition(NativeCondition::new(42_i32, "n")).unwrap_err();
assert!(err.is_incorrect_usage(), "{err:?}");
assert!(err.to_string().contains("bson::Document or MongoCondition"));
}
#[test]
fn native_key_is_canonical_json() {
assert_eq!(
native_key(&doc! { "n": { "$gte": 2 } }),
r#"{"n":{"$gte":{"$numberInt":"2"}}}"#
);
assert_eq!(native_key(&doc! { "a": 1 }), native_key(&doc! { "a": 1 }));
assert_ne!(
native_key(&doc! { "a": 1 }),
native_key(&doc! { "a": 1_i64 })
);
}
#[test]
fn ordering_operators_are_not_expanded() {
let oid = ObjectId::new();
assert_eq!(
op_filter("_id", FilterOp::Gt, &s(&oid.to_hex())).unwrap(),
doc! { "_id": { "$gt": oid.to_hex() } }
);
}
}