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