Skip to main content

vantage_mongodb/
operation.rs

1//! Operators that build [`MongoCondition`] filters from columns: `price.gt(100)`
2//! is `{ price: { "$gt": 100 } }`.
3//!
4//! [`MongoOperation`] is blanket-implemented for every `Expressive<T>`. The
5//! field name is the expression's template, so it fits columns and plain
6//! identifiers. A condition is negated with `eq_bool(false)` (or
7//! `ne_bool(true)`) on [`MongoCondition`].
8
9use bson::{Bson, doc};
10use vantage_expressions::Expressive;
11
12use crate::condition::MongoCondition;
13use crate::types::{AnyMongoType, MongoType};
14
15fn field_name<T>(expr: &(impl Expressive<T> + ?Sized)) -> String {
16    expr.expr().template.clone()
17}
18
19fn to_bson_val(value: impl Into<AnyMongoType>) -> Bson {
20    let any: AnyMongoType = value.into();
21    any.to_bson()
22}
23
24/// Wraps each field of a document in `$not`. A deferred condition passes
25/// through unchanged.
26fn negate(cond: MongoCondition) -> MongoCondition {
27    match cond {
28        MongoCondition::Doc(doc) => {
29            let mut negated = bson::Document::new();
30            for (key, val) in doc {
31                match val {
32                    Bson::Document(inner) => {
33                        negated.insert(key, doc! { "$not": inner });
34                    }
35                    other => {
36                        negated.insert(key, doc! { "$not": { "$eq": other } });
37                    }
38                }
39            }
40            MongoCondition::Doc(negated)
41        }
42        MongoCondition::And(conditions) => {
43            MongoCondition::And(conditions.into_iter().map(negate).collect())
44        }
45        // A deferred condition has no document to wrap until it resolves.
46        other => other,
47    }
48}
49
50/// Operators that produce a [`MongoCondition`]. Import this instead of
51/// `vantage_table::operation::Operation` when working with MongoDB.
52pub trait MongoOperation<T>: Expressive<T> {
53    /// `{ field: { "$eq": value } }`
54    fn eq(&self, value: impl Into<AnyMongoType>) -> MongoCondition
55    where
56        Self: Sized,
57    {
58        MongoCondition::Doc(doc! { field_name(self): { "$eq": to_bson_val(value) } })
59    }
60
61    /// `{ field: { "$ne": value } }`
62    fn ne(&self, value: impl Into<AnyMongoType>) -> MongoCondition
63    where
64        Self: Sized,
65    {
66        MongoCondition::Doc(doc! { field_name(self): { "$ne": to_bson_val(value) } })
67    }
68
69    /// `{ field: { "$gt": value } }`
70    fn gt(&self, value: impl Into<AnyMongoType>) -> MongoCondition
71    where
72        Self: Sized,
73    {
74        MongoCondition::Doc(doc! { field_name(self): { "$gt": to_bson_val(value) } })
75    }
76
77    /// `{ field: { "$gte": value } }`
78    fn gte(&self, value: impl Into<AnyMongoType>) -> MongoCondition
79    where
80        Self: Sized,
81    {
82        MongoCondition::Doc(doc! { field_name(self): { "$gte": to_bson_val(value) } })
83    }
84
85    /// `{ field: { "$lt": value } }`
86    fn lt(&self, value: impl Into<AnyMongoType>) -> MongoCondition
87    where
88        Self: Sized,
89    {
90        MongoCondition::Doc(doc! { field_name(self): { "$lt": to_bson_val(value) } })
91    }
92
93    /// `{ field: { "$lte": value } }`
94    fn lte(&self, value: impl Into<AnyMongoType>) -> MongoCondition
95    where
96        Self: Sized,
97    {
98        MongoCondition::Doc(doc! { field_name(self): { "$lte": to_bson_val(value) } })
99    }
100
101    /// `{ field: { "$in": [values...] } }`
102    fn in_<I, V>(&self, values: I) -> MongoCondition
103    where
104        Self: Sized,
105        I: IntoIterator<Item = V>,
106        V: Into<AnyMongoType>,
107    {
108        let arr: Vec<Bson> = values.into_iter().map(to_bson_val).collect();
109        MongoCondition::Doc(doc! { field_name(self): { "$in": arr } })
110    }
111
112    /// `{ field: null }`: matches documents where the field is null or missing.
113    fn is_null(&self) -> MongoCondition
114    where
115        Self: Sized,
116    {
117        MongoCondition::Doc(doc! { field_name(self): Bson::Null })
118    }
119
120    /// `{ field: { "$ne": null } }`: matches documents where the field exists and is non-null.
121    fn is_not_null(&self) -> MongoCondition
122    where
123        Self: Sized,
124    {
125        MongoCondition::Doc(doc! { field_name(self): { "$ne": Bson::Null } })
126    }
127}
128
129impl<T, S: Expressive<T>> MongoOperation<T> for S {}
130
131// A condition is not an expression; this impl only satisfies the blanket
132// bound so a chained operator call type-checks.
133impl Expressive<AnyMongoType> for MongoCondition {
134    fn expr(&self) -> vantage_expressions::Expression<AnyMongoType> {
135        vantage_expressions::Expression::new(format!("{:?}", self), vec![])
136    }
137}
138
139impl MongoCondition {
140    /// `eq_bool(false)` negates the condition; `eq_bool(true)` keeps it.
141    pub fn eq_bool(&self, value: bool) -> MongoCondition {
142        if value {
143            self.clone()
144        } else {
145            negate(self.clone())
146        }
147    }
148
149    /// `ne_bool(true)` negates the condition; `ne_bool(false)` keeps it.
150    pub fn ne_bool(&self, value: bool) -> MongoCondition {
151        if value {
152            negate(self.clone())
153        } else {
154            self.clone()
155        }
156    }
157}