Skip to main content

vantage_mongodb/
condition.rs

1//! MongoDB condition type — analogous to `Expression<T>` for SQL backends.
2//!
3//! `MongoCondition` can hold immediate `bson::Document` filters, async-deferred
4//! values (resolved at query time via `DeferredFn`), or nested combinations.
5
6use bson::{Bson, Document};
7use vantage_expressions::{DeferredFn, ExpressiveEnum};
8
9use crate::types::AnyMongoType;
10
11/// A MongoDB filter condition that can contain deferred values.
12///
13/// # Variants
14///
15/// - `Doc` — an immediate `bson::Document` filter, e.g. `doc! { "price": { "$gt": 100 } }`
16/// - `Deferred` — resolves async at query time via `DeferredFn<AnyMongoType>`.
17///   The resolved `AnyMongoType` must be a `Bson::Document`.
18/// - `And` — combines multiple conditions with `$and` semantics.
19#[derive(Clone)]
20pub enum MongoCondition {
21    /// Immediate filter document.
22    Doc(Document),
23    /// Async-resolved filter — the `DeferredFn` should produce an `AnyMongoType`
24    /// wrapping a `Bson::Document`.
25    Deferred(DeferredFn<AnyMongoType>),
26    /// Multiple conditions combined with `$and`.
27    And(Vec<MongoCondition>),
28}
29
30impl MongoCondition {
31    /// Resolve all deferred values and merge into a single `bson::Document`.
32    pub fn resolve(
33        &self,
34    ) -> std::pin::Pin<
35        Box<dyn std::future::Future<Output = vantage_core::Result<Document>> + Send + '_>,
36    > {
37        Box::pin(async move {
38            match self {
39                MongoCondition::Doc(doc) => Ok(doc.clone()),
40                MongoCondition::Deferred(deferred) => {
41                    let result = deferred.call().await?;
42                    let resolved = match result {
43                        ExpressiveEnum::Scalar(val) => val,
44                        other => {
45                            return Err(vantage_core::error!(
46                                "MongoCondition::Deferred resolved to non-scalar",
47                                result = format!("{:?}", other)
48                            ));
49                        }
50                    };
51                    bson_to_document(resolved.into_value())
52                }
53                MongoCondition::And(conditions) => {
54                    let mut docs = Vec::with_capacity(conditions.len());
55                    for c in conditions {
56                        docs.push(c.resolve().await?);
57                    }
58                    merge_documents(docs)
59                }
60            }
61        })
62    }
63}
64
65// ── Conversions ──────────────────────────────────────────────────────
66
67impl From<Document> for MongoCondition {
68    fn from(doc: Document) -> Self {
69        MongoCondition::Doc(doc)
70    }
71}
72
73impl std::fmt::Debug for MongoCondition {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            MongoCondition::Doc(doc) => write!(f, "Doc({doc})"),
77            MongoCondition::Deferred(_) => write!(f, "Deferred(...)"),
78            MongoCondition::And(conditions) => f.debug_tuple("And").field(conditions).finish(),
79        }
80    }
81}
82
83// ── Helpers ──────────────────────────────────────────────────────────
84
85/// Convert a `Bson` value into a `Document`, or error if it's not one.
86fn bson_to_document(value: Bson) -> vantage_core::Result<Document> {
87    match value {
88        Bson::Document(doc) => Ok(doc),
89        other => Err(vantage_core::error!(
90            "Expected Bson::Document from deferred condition",
91            actual = format!("{:?}", other)
92        )),
93    }
94}
95
96/// Merge multiple documents into a single filter.
97///
98/// - Empty list → `{}`
99/// - Single doc → returned as-is
100/// - Multiple docs → `{ "$and": [ doc1, doc2, ... ] }`
101fn merge_documents(docs: Vec<Document>) -> vantage_core::Result<Document> {
102    Ok(match docs.len() {
103        0 => Document::new(),
104        1 => docs.into_iter().next().unwrap(),
105        _ => {
106            let array: Vec<Bson> = docs.into_iter().map(Bson::Document).collect();
107            bson::doc! { "$and": array }
108        }
109    })
110}
111
112/// Resolve an iterator of `MongoCondition` references into a single filter document.
113pub async fn resolve_conditions<'a>(
114    conditions: impl Iterator<Item = &'a MongoCondition>,
115) -> vantage_core::Result<Document> {
116    let mut docs = Vec::new();
117    for c in conditions {
118        docs.push(c.resolve().await?);
119    }
120    merge_documents(docs)
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_from_document() {
129        let doc = bson::doc! { "price": { "$gt": 100 } };
130        let cond: MongoCondition = doc.clone().into();
131        match cond {
132            MongoCondition::Doc(d) => assert_eq!(d, doc),
133            _ => panic!("expected Doc variant"),
134        }
135    }
136
137    #[tokio::test]
138    async fn test_resolve_doc() {
139        let cond = MongoCondition::Doc(bson::doc! { "active": true });
140        let resolved = cond.resolve().await.unwrap();
141        assert_eq!(resolved, bson::doc! { "active": true });
142    }
143
144    #[tokio::test]
145    async fn test_resolve_and() {
146        let cond = MongoCondition::And(vec![
147            bson::doc! { "a": 1 }.into(),
148            bson::doc! { "b": 2 }.into(),
149        ]);
150        let resolved = cond.resolve().await.unwrap();
151        assert_eq!(resolved, bson::doc! { "$and": [{ "a": 1 }, { "b": 2 }] });
152    }
153
154    #[tokio::test]
155    async fn test_resolve_and_single() {
156        let cond = MongoCondition::And(vec![bson::doc! { "x": 1 }.into()]);
157        let resolved = cond.resolve().await.unwrap();
158        assert_eq!(resolved, bson::doc! { "x": 1 });
159    }
160
161    #[tokio::test]
162    async fn test_resolve_and_empty() {
163        let cond = MongoCondition::And(vec![]);
164        let resolved = cond.resolve().await.unwrap();
165        assert_eq!(resolved, bson::doc! {});
166    }
167
168    #[tokio::test]
169    async fn test_resolve_conditions_helper() {
170        let conds = [
171            MongoCondition::Doc(bson::doc! { "a": 1 }),
172            MongoCondition::Doc(bson::doc! { "b": 2 }),
173        ];
174        let resolved = resolve_conditions(conds.iter()).await.unwrap();
175        assert_eq!(resolved, bson::doc! { "$and": [{ "a": 1 }, { "b": 2 }] });
176    }
177
178    #[tokio::test]
179    async fn test_deferred_resolves_document() {
180        let deferred = DeferredFn::new(move || {
181            Box::pin(async move {
182                let doc = bson::doc! { "status": "active" };
183                Ok(ExpressiveEnum::Scalar(AnyMongoType::untyped(
184                    Bson::Document(doc),
185                )))
186            })
187        });
188        let cond = MongoCondition::Deferred(deferred);
189        let resolved = cond.resolve().await.unwrap();
190        assert_eq!(resolved, bson::doc! { "status": "active" });
191    }
192
193    #[tokio::test]
194    async fn test_nested_and_with_deferred() {
195        let deferred = DeferredFn::new(move || {
196            Box::pin(async move {
197                let doc = bson::doc! { "owner_id": { "$in": ["a", "b"] } };
198                Ok(ExpressiveEnum::Scalar(AnyMongoType::untyped(
199                    Bson::Document(doc),
200                )))
201            })
202        });
203        let cond = MongoCondition::And(vec![
204            bson::doc! { "active": true }.into(),
205            MongoCondition::Deferred(deferred),
206        ]);
207        let resolved = cond.resolve().await.unwrap();
208        assert_eq!(
209            resolved,
210            bson::doc! { "$and": [
211                { "active": true },
212                { "owner_id": { "$in": ["a", "b"] } }
213            ] }
214        );
215    }
216}