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