Skip to main content

flusso_query/handles/
scalar.rs

1//! Scalar value fields: the [`Bool`] exact-match field and the ordered
2//! [`Number`] / [`Date`] fields with their range operators.
3//!
4//! The value operators return small builders ([`EqQuery`], [`TermsQuery`],
5//! [`RangeQuery`]) that carry the universal `boost` / `name` modifiers (and, for
6//! ranges, `format` / `time_zone` / `relation`) and render lazily through
7//! [`AsQuery`] — so they drop straight into a clause, with or
8//! without options.
9
10use std::marker::PhantomData;
11
12use serde_json::{Map, Value};
13
14use super::{Common, Sort, SortOrder, common_opts, exists_q, single, wrap};
15use crate::query::{AsQuery, Query, Root};
16
17/// An exact-match (`term`) clause for a non-string value (number, bool, date),
18/// carrying the universal `boost` / `name` modifiers.
19#[derive(Debug, Clone)]
20pub struct EqQuery<S = Root> {
21    path: String,
22    value: Value,
23    common: Common,
24    _scope: PhantomData<fn() -> S>,
25}
26
27impl<S> EqQuery<S> {
28    fn new(path: &str, value: Value) -> Self {
29        Self {
30            path: path.to_string(),
31            value,
32            common: Common::default(),
33            _scope: PhantomData,
34        }
35    }
36
37    common_opts!(common);
38}
39
40impl<S> AsQuery<S> for EqQuery<S> {
41    fn into_query(self) -> Option<Query<S>> {
42        if self.common.is_empty() {
43            Some(single("term", &self.path, self.value))
44        } else {
45            let mut body = Map::new();
46            body.insert("value".to_string(), self.value);
47            self.common.write(&mut body);
48            Some(single("term", &self.path, Value::Object(body)))
49        }
50    }
51}
52
53/// A multi-value (`terms`) clause, carrying `boost` / `name`. Shared by the
54/// keyword and numeric `in_` operators.
55#[derive(Debug, Clone)]
56pub struct TermsQuery<S = Root> {
57    path: String,
58    values: Vec<Value>,
59    common: Common,
60    _scope: PhantomData<fn() -> S>,
61}
62
63impl<S> TermsQuery<S> {
64    pub(crate) fn new(path: &str, values: Vec<Value>) -> Self {
65        Self {
66            path: path.to_string(),
67            values,
68            common: Common::default(),
69            _scope: PhantomData,
70        }
71    }
72
73    common_opts!(common);
74}
75
76impl<S> AsQuery<S> for TermsQuery<S> {
77    fn into_query(self) -> Option<Query<S>> {
78        // `terms` carries `boost` / `_name` beside the field, not inside it.
79        let mut body = Map::new();
80        body.insert(self.path, Value::Array(self.values));
81        self.common.write(&mut body);
82        Some(wrap("terms", body))
83    }
84}
85
86/// A `range` clause with bounds plus the universal `boost` / `name` and the
87/// range-specific `format` / `time_zone` / `relation` modifiers.
88#[derive(Debug, Clone)]
89pub struct RangeQuery<S = Root> {
90    path: String,
91    bounds: Vec<(&'static str, Value)>,
92    extra: Map<String, Value>,
93    common: Common,
94    _scope: PhantomData<fn() -> S>,
95}
96
97impl<S> RangeQuery<S> {
98    pub(crate) fn new(path: &str, bounds: Vec<(&'static str, Value)>) -> Self {
99        Self {
100            path: path.to_string(),
101            bounds,
102            extra: Map::new(),
103            common: Common::default(),
104            _scope: PhantomData,
105        }
106    }
107
108    /// Date math / numeric `format` for the bounds (`date` fields).
109    #[must_use]
110    pub fn format(mut self, format: impl Into<String>) -> Self {
111        self.extra
112            .insert("format".to_string(), Value::String(format.into()));
113        self
114    }
115
116    /// Time zone applied to the bounds (`date` fields), e.g. `"+01:00"`.
117    #[must_use]
118    pub fn time_zone(mut self, time_zone: impl Into<String>) -> Self {
119        self.extra
120            .insert("time_zone".to_string(), Value::String(time_zone.into()));
121        self
122    }
123
124    /// How the range relates to range-typed field values
125    /// (`INTERSECTS` / `CONTAINS` / `WITHIN`).
126    #[must_use]
127    pub fn relation(mut self, relation: impl Into<String>) -> Self {
128        self.extra
129            .insert("relation".to_string(), Value::String(relation.into()));
130        self
131    }
132
133    common_opts!(common);
134}
135
136impl<S> AsQuery<S> for RangeQuery<S> {
137    fn into_query(self) -> Option<Query<S>> {
138        let mut body = self.extra;
139        for (key, value) in self.bounds {
140            body.insert(key.to_string(), value);
141        }
142        self.common.write(&mut body);
143        Some(single("range", &self.path, Value::Object(body)))
144    }
145}
146
147/// A boolean field.
148#[derive(Debug, Clone)]
149pub struct Bool<S = Root> {
150    path: String,
151    _scope: PhantomData<fn() -> S>,
152}
153
154impl<S> Bool<S> {
155    pub fn at(path: impl Into<String>) -> Self {
156        Self {
157            path: path.into(),
158            _scope: PhantomData,
159        }
160    }
161
162    /// Exact match.
163    pub fn eq(&self, value: bool) -> EqQuery<S> {
164        EqQuery::new(&self.path, Value::Bool(value))
165    }
166
167    /// The field has a non-null value.
168    pub fn exists(&self) -> Query<S> {
169        exists_q(&self.path)
170    }
171}
172
173/// A numeric field. `T` is the Rust scalar; `S` is the scope (defaults to
174/// [`Root`], so `Number<i64>` is a root-scope handle).
175#[derive(Debug, Clone)]
176pub struct Number<T, S = Root> {
177    path: String,
178    _marker: PhantomData<fn() -> (T, S)>,
179}
180
181impl<T, S> Number<T, S>
182where
183    T: Into<Value> + Copy,
184{
185    pub fn at(path: impl Into<String>) -> Self {
186        Self {
187            path: path.into(),
188            _marker: PhantomData,
189        }
190    }
191
192    /// Exact match.
193    pub fn eq(&self, value: T) -> EqQuery<S> {
194        EqQuery::new(&self.path, value.into())
195    }
196
197    /// Match any of the given values.
198    pub fn in_(&self, values: impl IntoIterator<Item = T>) -> TermsQuery<S> {
199        let array = values.into_iter().map(Into::into).collect();
200        TermsQuery::new(&self.path, array)
201    }
202
203    /// Strictly less than `value`.
204    pub fn lt(&self, value: T) -> RangeQuery<S> {
205        RangeQuery::new(&self.path, vec![("lt", value.into())])
206    }
207
208    /// Less than or equal to `value`.
209    pub fn lte(&self, value: T) -> RangeQuery<S> {
210        RangeQuery::new(&self.path, vec![("lte", value.into())])
211    }
212
213    /// Strictly greater than `value`.
214    pub fn gt(&self, value: T) -> RangeQuery<S> {
215        RangeQuery::new(&self.path, vec![("gt", value.into())])
216    }
217
218    /// Greater than or equal to `value`.
219    pub fn gte(&self, value: T) -> RangeQuery<S> {
220        RangeQuery::new(&self.path, vec![("gte", value.into())])
221    }
222
223    /// Inclusive range `[low, high]`.
224    pub fn between(&self, low: T, high: T) -> RangeQuery<S> {
225        RangeQuery::new(&self.path, vec![("gte", low.into()), ("lte", high.into())])
226    }
227
228    /// The field has a non-null value.
229    pub fn exists(&self) -> Query<S> {
230        exists_q(&self.path)
231    }
232
233    pub fn asc(&self) -> Sort {
234        Sort::new(&self.path, SortOrder::Asc)
235    }
236
237    pub fn desc(&self) -> Sort {
238        Sort::new(&self.path, SortOrder::Desc)
239    }
240}
241
242/// A `date`/`timestamp` field. Bounds are ISO-8601 strings.
243#[derive(Debug, Clone)]
244pub struct Date<S = Root> {
245    path: String,
246    _scope: PhantomData<fn() -> S>,
247}
248
249impl<S> Date<S> {
250    pub fn at(path: impl Into<String>) -> Self {
251        Self {
252            path: path.into(),
253            _scope: PhantomData,
254        }
255    }
256
257    /// Exact match.
258    pub fn eq(&self, value: impl Into<String>) -> EqQuery<S> {
259        EqQuery::new(&self.path, Value::String(value.into()))
260    }
261
262    /// Strictly before `value`.
263    pub fn lt(&self, value: impl Into<String>) -> RangeQuery<S> {
264        RangeQuery::new(&self.path, vec![("lt", Value::String(value.into()))])
265    }
266
267    /// At or before `value`.
268    pub fn lte(&self, value: impl Into<String>) -> RangeQuery<S> {
269        RangeQuery::new(&self.path, vec![("lte", Value::String(value.into()))])
270    }
271
272    /// Strictly after `value`.
273    pub fn gt(&self, value: impl Into<String>) -> RangeQuery<S> {
274        RangeQuery::new(&self.path, vec![("gt", Value::String(value.into()))])
275    }
276
277    /// At or after `value`.
278    pub fn gte(&self, value: impl Into<String>) -> RangeQuery<S> {
279        RangeQuery::new(&self.path, vec![("gte", Value::String(value.into()))])
280    }
281
282    /// Inclusive range `[low, high]`.
283    pub fn between(&self, low: impl Into<String>, high: impl Into<String>) -> RangeQuery<S> {
284        RangeQuery::new(
285            &self.path,
286            vec![
287                ("gte", Value::String(low.into())),
288                ("lte", Value::String(high.into())),
289            ],
290        )
291    }
292
293    /// The field has a non-null value.
294    pub fn exists(&self) -> Query<S> {
295        exists_q(&self.path)
296    }
297
298    pub fn asc(&self) -> Sort {
299        Sort::new(&self.path, SortOrder::Asc)
300    }
301
302    pub fn desc(&self) -> Sort {
303        Sort::new(&self.path, SortOrder::Desc)
304    }
305}