Skip to main content

flusso_query/
query.rs

1//! The [`Query`] type: an OpenSearch query clause, tagged with the **scope** it
2//! was built in, composed with `and` / `or` / `not`.
3//!
4//! Scope `S` is the query context a handle belongs to. Root fields and flattened
5//! object / to-one-join sub-fields are all [`Root`]; a `nested` array introduces
6//! its own scope (the element type), so a nested query must be lifted
7//! ([`Nested::any`](crate::Nested::any)/[`all`](crate::Nested::all)) before it can
8//! join a `Root` query — the compiler enforces it.
9
10use std::marker::PhantomData;
11
12use serde_json::{Map, Value};
13
14/// The default query scope — the document root. Root fields and flattened
15/// object / to-one-join sub-fields share it.
16#[derive(Debug, Clone, Copy)]
17pub struct Root;
18
19/// A composable query clause in scope `S`.
20///
21/// Handles produce a `Query<S>` (`User::email().eq(…)` → `Query<Root>`,
22/// `Order::status().eq(…)` → `Query<Order>`). `and`/`or`/`not` and the
23/// [`crate::Search`] clauses only combine the **same** scope; a nested query is
24/// lifted to its parent scope through the nested handle.
25#[derive(Debug, Clone)]
26pub struct Query<S = Root> {
27    inner: Inner,
28    _scope: PhantomData<fn() -> S>,
29}
30
31/// The scope-free internal representation (the scope is purely a type-level tag).
32#[derive(Debug, Clone)]
33enum Inner {
34    Leaf(Value),
35    Bool(BoolInner),
36}
37
38#[derive(Debug, Clone, Default)]
39pub(crate) struct BoolInner {
40    must: Vec<Inner>,
41    filter: Vec<Inner>,
42    should: Vec<Inner>,
43    must_not: Vec<Inner>,
44}
45
46#[derive(Debug, Clone, Copy)]
47enum Clause {
48    Must,
49    Filter,
50    Should,
51    MustNot,
52}
53
54impl BoolInner {
55    pub(crate) fn is_empty(&self) -> bool {
56        self.must.is_empty()
57            && self.filter.is_empty()
58            && self.should.is_empty()
59            && self.must_not.is_empty()
60    }
61
62    fn push(&mut self, clause: Clause, inner: Inner) {
63        match clause {
64            Clause::Must => self.must.push(inner),
65            Clause::Filter => self.filter.push(inner),
66            Clause::Should => self.should.push(inner),
67            Clause::MustNot => self.must_not.push(inner),
68        }
69    }
70
71    /// True when only `clause`'s list is populated, so a new entry can be
72    /// appended without changing the query's meaning.
73    fn is_pure(&self, clause: Clause) -> bool {
74        match clause {
75            Clause::Must => {
76                self.filter.is_empty() && self.should.is_empty() && self.must_not.is_empty()
77            }
78            Clause::Filter => {
79                self.must.is_empty() && self.should.is_empty() && self.must_not.is_empty()
80            }
81            Clause::Should => {
82                self.must.is_empty() && self.filter.is_empty() && self.must_not.is_empty()
83            }
84            Clause::MustNot => {
85                self.must.is_empty() && self.filter.is_empty() && self.should.is_empty()
86            }
87        }
88    }
89
90    pub(crate) fn to_value(&self) -> Value {
91        let mut body = Map::new();
92        insert_clause(&mut body, "must", &self.must);
93        insert_clause(&mut body, "filter", &self.filter);
94        insert_clause(&mut body, "should", &self.should);
95        insert_clause(&mut body, "must_not", &self.must_not);
96        let mut outer = Map::new();
97        outer.insert("bool".to_string(), Value::Object(body));
98        Value::Object(outer)
99    }
100}
101
102fn insert_clause(target: &mut Map<String, Value>, key: &str, clauses: &[Inner]) {
103    if clauses.is_empty() {
104        return;
105    }
106    let array = clauses.iter().map(Inner::to_value).collect();
107    target.insert(key.to_string(), Value::Array(array));
108}
109
110impl Inner {
111    fn to_value(&self) -> Value {
112        match self {
113            Inner::Leaf(value) => value.clone(),
114            Inner::Bool(bool_inner) => bool_inner.to_value(),
115        }
116    }
117}
118
119/// Combine `a` and `b` under `clause`, flattening when `a` is already a pure
120/// bool for that clause (so `x.and(y).and(z)` is one bool with three `must`).
121fn combine(a: Inner, b: Inner, clause: Clause) -> Inner {
122    if let Inner::Bool(mut bool_inner) = a {
123        if bool_inner.is_pure(clause) {
124            bool_inner.push(clause, b);
125            return Inner::Bool(bool_inner);
126        }
127        let mut combined = BoolInner::default();
128        combined.push(clause, Inner::Bool(bool_inner));
129        combined.push(clause, b);
130        return Inner::Bool(combined);
131    }
132    let mut combined = BoolInner::default();
133    combined.push(clause, a);
134    combined.push(clause, b);
135    Inner::Bool(combined)
136}
137
138impl<S> Query<S> {
139    /// Wrap a leaf clause value. Crate-internal: handles call this.
140    pub(crate) fn leaf(value: Value) -> Self {
141        Query {
142            inner: Inner::Leaf(value),
143            _scope: PhantomData,
144        }
145    }
146
147    fn wrap(inner: Inner) -> Self {
148        Query {
149            inner,
150            _scope: PhantomData,
151        }
152    }
153
154    /// `self AND other`, within the same scope.
155    #[must_use]
156    pub fn and(self, other: impl AsQuery<S>) -> Query<S> {
157        match other.into_query() {
158            Some(other) => Query::wrap(combine(self.inner, other.inner, Clause::Must)),
159            None => self,
160        }
161    }
162
163    /// `self OR other`, within the same scope.
164    #[must_use]
165    pub fn or(self, other: impl AsQuery<S>) -> Query<S> {
166        match other.into_query() {
167            Some(other) => Query::wrap(combine(self.inner, other.inner, Clause::Should)),
168            None => self,
169        }
170    }
171
172    /// `NOT self`.
173    #[must_use]
174    #[allow(clippy::should_implement_trait)]
175    pub fn not(self) -> Query<S> {
176        Query::wrap(Inner::Bool(BoolInner {
177            must_not: vec![self.inner],
178            ..BoolInner::default()
179        }))
180    }
181
182    /// Render to the OpenSearch query DSL.
183    #[must_use]
184    pub fn to_value(&self) -> Value {
185        self.inner.to_value()
186    }
187
188    /// The scope-free inner clause. Crate-internal — `Search` collects these.
189    pub(crate) fn into_inner(self) -> InnerClause {
190        InnerClause(self.inner)
191    }
192}
193
194/// An opaque scope-free clause, handed from a [`Query`] to [`crate::Search`].
195pub(crate) struct InnerClause(Inner);
196
197/// A bool builder over scope-free clauses, used by [`crate::Search`] (root scope).
198#[derive(Debug, Clone, Default)]
199pub(crate) struct BoolBuilder {
200    bool_inner: BoolInner,
201}
202
203impl BoolBuilder {
204    pub(crate) fn push_must(&mut self, clause: InnerClause) {
205        self.bool_inner.push(Clause::Must, clause.0);
206    }
207    pub(crate) fn push_filter(&mut self, clause: InnerClause) {
208        self.bool_inner.push(Clause::Filter, clause.0);
209    }
210    pub(crate) fn push_should(&mut self, clause: InnerClause) {
211        self.bool_inner.push(Clause::Should, clause.0);
212    }
213    pub(crate) fn push_must_not(&mut self, clause: InnerClause) {
214        self.bool_inner.push(Clause::MustNot, clause.0);
215    }
216    pub(crate) fn is_empty(&self) -> bool {
217        self.bool_inner.is_empty()
218    }
219    pub(crate) fn to_value(&self) -> Value {
220        self.bool_inner.to_value()
221    }
222}
223
224/// Anything that can become a query clause in scope `S`. A clause may be absent
225/// ([`into_query`](AsQuery::into_query) returns `None`) — that's what makes an
226/// `Option<Query<S>>` a first-class optional filter.
227pub trait AsQuery<S> {
228    /// The clause this produces, or `None` to contribute nothing.
229    fn into_query(self) -> Option<Query<S>>;
230}
231
232impl<S> AsQuery<S> for Query<S> {
233    fn into_query(self) -> Option<Query<S>> {
234        Some(self)
235    }
236}
237
238impl<S, T: AsQuery<S>> AsQuery<S> for Option<T> {
239    fn into_query(self) -> Option<Query<S>> {
240        self.and_then(AsQuery::into_query)
241    }
242}