Skip to main content

flusso_query/handles/
mod.rs

1//! Field handles. Each handle carries a field path **and a scope** `S`, and
2//! exposes only the operators its mapping type supports; every operator builds a
3//! [`Query`]`<S>`.
4//!
5//! Root fields and flattened object / to-one-join sub-fields are scope
6//! [`Root`](crate::Root); a `nested` array's element handles carry the element type as their
7//! scope, so their queries must be lifted with [`Nested::any`]/[`Nested::all`]
8//! before joining a parent query. (The derive picks the right scope; hand-written
9//! handles default to `Root`.)
10//!
11//! The handles are grouped by field family:
12//!
13//! - `string` — [`Keyword`], [`Text`], and the cross-field [`multi_match`].
14//! - `scalar` — [`Bool`], [`Number`], [`Date`] (exact/range value fields).
15//! - `map` — [`TextMap`]/[`KeywordMap`] dynamic-key objects and [`MapSearch`].
16//! - `nested` — [`Nested`] arrays and their [`NestedProjection`].
17//! - `object` — [`Object`] sub-documents and the opaque [`Binary`]/[`Json`] fields.
18//! - `geo` — [`Geo`] points and [`GeoPoint`].
19//! - `sort` — [`Sort`]/[`SortOrder`].
20//!
21//! This module holds the pieces they share: the leaf-query builders and the
22//! [`trait@FlussoValue`] type-kind machinery.
23
24use serde_json::{Map, Value};
25
26use crate::query::Query;
27
28mod compound;
29mod extra;
30mod geo;
31mod map;
32mod nested;
33mod object;
34mod params;
35mod scalar;
36mod sort;
37mod string;
38
39pub use compound::{
40    BoostingQuery, ConstantScoreQuery, DisMaxQuery, FunctionScoreQuery, boosting, constant_score,
41    dis_max, function_score,
42};
43pub use extra::{
44    CombinedFieldsQuery, DistanceFeatureQuery, IdsQuery, MoreLikeThisQuery, QueryStringQuery,
45    RankFeatureQuery, ScriptQuery, ScriptScoreQuery, SimpleQueryStringQuery, combined_fields,
46    distance_feature, ids, more_like_this, query_string, rank_feature, script, script_score,
47    simple_query_string,
48};
49pub use geo::{Distance, DistanceUnit, Geo, GeoDistanceQuery, GeoPoint};
50pub use map::{DateMap, KeywordMap, MapSearch, NumberMap, TextMap};
51pub use nested::{Nested, NestedProjection, NestedQuery};
52pub use object::{Binary, Json, Object};
53pub use params::{
54    BoostMode, DistanceType, Fuzziness, MinimumShouldMatch, MultiMatchType, NestedScoreMode,
55    NumericType, Operator, RangeRelation, ScoreMode, ScriptSortType, ValidationMethod,
56    ZeroTermsQuery,
57};
58pub use scalar::{Bool, Date, EqQuery, Number, RangeQuery, TermsQuery};
59pub use sort::{
60    MapKeySort, MaybeOrderBy, Missing, OrderBy, Sort, SortBuilder, SortMode, SortOrder, Sortable,
61};
62pub use string::{
63    FuzzyQuery, Keyword, MapKey, MatchQuery, MultiMatchQuery, NoSubfields, PrefixQuery,
64    RegexpQuery, TermQuery, Text, WildcardQuery, WithSubfields, multi_match,
65};
66
67/// `{ "<wrapper>": { "<path>": <value> } }`.
68fn single<S>(wrapper: &str, path: &str, value: Value) -> Query<S> {
69    let mut inner = Map::new();
70    inner.insert(path.to_string(), value);
71    wrap(wrapper, inner)
72}
73
74/// `{ "<wrapper>": { <body> } }`.
75fn wrap<S>(wrapper: &str, body: Map<String, Value>) -> Query<S> {
76    let mut outer = Map::new();
77    outer.insert(wrapper.to_string(), Value::Object(body));
78    Query::leaf(Value::Object(outer))
79}
80
81/// The universal leaf-query modifiers every builder carries: `boost` (a
82/// relevance multiplier) and `name` (`_name`, surfaced in a hit's
83/// `matched_queries`). Builders embed one and call [`Common::write`] when
84/// rendering.
85#[derive(Debug, Clone, Default)]
86pub(crate) struct Common {
87    boost: Option<f32>,
88    name: Option<String>,
89}
90
91impl Common {
92    pub(crate) fn set_boost(&mut self, boost: f32) {
93        self.boost = Some(boost);
94    }
95
96    pub(crate) fn set_name(&mut self, name: String) {
97        self.name = Some(name);
98    }
99
100    /// Whether neither modifier is set (so a builder may render the shorthand).
101    pub(crate) fn is_empty(&self) -> bool {
102        self.boost.is_none() && self.name.is_none()
103    }
104
105    /// Write `boost` / `_name` into an option map, if set.
106    pub(crate) fn write(&self, map: &mut Map<String, Value>) {
107        if let Some(boost) = self.boost {
108            map.insert("boost".to_string(), Value::from(boost));
109        }
110        if let Some(name) = &self.name {
111            map.insert("_name".to_string(), Value::String(name.clone()));
112        }
113    }
114}
115
116/// Emit the universal `boost` / `name` setters on a builder whose [`Common`]
117/// lives in `self.$field`. Keeps the two methods identical across every builder.
118macro_rules! common_opts {
119    ($field:ident) => {
120        /// Multiply this clause's relevance score by `boost`.
121        #[must_use]
122        pub fn boost(mut self, boost: f32) -> Self {
123            self.$field.set_boost(boost);
124            self
125        }
126
127        /// Tag this clause with `_name`, surfaced in a hit's `matched_queries`.
128        #[must_use]
129        pub fn name(mut self, name: impl Into<String>) -> Self {
130            self.$field.set_name(name.into());
131            self
132        }
133    };
134}
135pub(crate) use common_opts;
136
137/// Render `{ wrapper: { path: <value> } }` (the DSL shorthand) when `opts` is
138/// empty, else `{ wrapper: { path: { <key>: <value>, ...opts } } }`. The shared
139/// shape for the value-bearing leaf queries (`term`/`prefix`/`wildcard`/… with
140/// `key = "value"`; `match`/`match_phrase`/… with `key = "query"`).
141fn keyed_value_query<S>(
142    wrapper: &str,
143    path: &str,
144    key: &str,
145    value: Value,
146    mut opts: Map<String, Value>,
147) -> Query<S> {
148    if opts.is_empty() {
149        single(wrapper, path, value)
150    } else {
151        opts.insert(key.to_string(), value);
152        single(wrapper, path, Value::Object(opts))
153    }
154}
155
156/// `{ "exists": { "field": "<path>" } }`.
157fn exists_q<S>(path: &str) -> Query<S> {
158    let mut inner = Map::new();
159    inner.insert("field".to_string(), Value::String(path.to_string()));
160    let mut outer = Map::new();
161    outer.insert("exists".to_string(), Value::Object(inner));
162    Query::leaf(Value::Object(outer))
163}
164
165/// `{ "match_all": {} }`.
166pub(crate) fn match_all_value() -> Value {
167    let mut outer = Map::new();
168    outer.insert("match_all".to_string(), Value::Object(Map::new()));
169    Value::Object(outer)
170}
171
172/// Field-category markers for [`trait@FlussoValue`]. Zero-size and uninhabited — they
173/// exist only as the `K` type parameter, so one type can be a valid value for
174/// several kinds (e.g. `String` is a [`kind::Keyword`], [`kind::Text`], and
175/// [`kind::Date`] value).
176pub mod kind {
177    /// A `keyword` field — an exact string.
178    #[derive(Debug)]
179    pub enum Keyword {}
180    /// A `text` field — an analyzed string.
181    #[derive(Debug)]
182    pub enum Text {}
183    /// A `boolean` field.
184    #[derive(Debug)]
185    pub enum Bool {}
186    /// A `byte` field (`i8`).
187    #[derive(Debug)]
188    pub enum Byte {}
189    /// A `short` field (`i16`).
190    #[derive(Debug)]
191    pub enum Short {}
192    /// An `integer` field (`i32`).
193    #[derive(Debug)]
194    pub enum Integer {}
195    /// A `long` field (`i64`).
196    #[derive(Debug)]
197    pub enum Long {}
198    /// A `float` / `half_float` field (`f32`).
199    #[derive(Debug)]
200    pub enum Float {}
201    /// A `double` field (`f64`).
202    #[derive(Debug)]
203    pub enum Double {}
204    /// A `decimal` / `scaled_float` field (`rust_decimal::Decimal`).
205    #[derive(Debug)]
206    pub enum Decimal {}
207    /// A `date`/`timestamp` field — an ISO-8601 string.
208    #[derive(Debug)]
209    pub enum Date {}
210}
211
212/// A Rust type usable where a field of kind `K` is expected: as the field type
213/// in a `#[derive(FlussoDocument)]` struct, and as a query value — for
214/// [`kind::Keyword`] on [`Keyword::eq`]/[`Keyword::any_of`], and for
215/// [`kind::Date`] on [`Date::eq`]/[`Date::gte`]/… (`String`/`&str`, or the
216/// `chrono` date types behind the `chrono` feature).
217///
218/// Built-in leaf types are pre-implemented (`String`/`&str` for keyword, the
219/// numeric primitives for number, …). Custom enums and newtype wrappers opt in
220/// with `#[derive(FlussoValue)]` (e.g. a `Pro`/`Enterprise`/`Free` tier enum →
221/// `Account::tier().eq(AccountTier::Pro)`, matched against its serde string).
222/// `FlussoDocument` emits a deferred bound on this trait for any non-primitive
223/// field type, so a document only compiles when the type genuinely fits.
224#[diagnostic::on_unimplemented(
225    message = "`{Self}` is not a valid value for a `{K}` field",
226    label = "unsupported field type",
227    note = "use a built-in leaf type, or add `#[derive(FlussoValue)]` (with the matching kind) to `{Self}`"
228)]
229pub trait FlussoValue<K>: serde::Serialize {}
230
231impl FlussoValue<kind::Keyword> for String {}
232impl FlussoValue<kind::Keyword> for &str {}
233#[cfg(feature = "uuid")]
234impl FlussoValue<kind::Keyword> for uuid::Uuid {}
235#[cfg(feature = "uuid")]
236impl FlussoValue<kind::Keyword> for &uuid::Uuid {}
237
238impl FlussoValue<kind::Text> for String {}
239impl FlussoValue<kind::Text> for &str {}
240
241impl FlussoValue<kind::Bool> for bool {}
242
243// A numeric type is a valid value for a numeric kind iff it widens into that
244// kind's Rust leaf **without loss** — so `Order::age().eq(5)` works on an `i64`
245// field, an `f64` field rejects an `i64` (precision), and an integer field
246// rejects a float. (`i32`→`f64` and any int→`Decimal` are lossless, so bare
247// literals work on `double`/`decimal`; `byte`/`short` need a typed literal.)
248macro_rules! number_values {
249    ($kind:path: $($ty:ty),+ $(,)?) => { $(impl FlussoValue<$kind> for $ty {})+ };
250}
251number_values!(kind::Byte: i8);
252number_values!(kind::Short: i8, i16);
253number_values!(kind::Integer: i8, i16, i32);
254number_values!(kind::Long: i8, i16, i32, i64);
255number_values!(kind::Float: i8, i16, f32);
256number_values!(kind::Double: i8, i16, i32, f32, f64);
257number_values!(kind::Decimal: i8, i16, i32, i64);
258#[cfg(feature = "decimal")]
259impl FlussoValue<kind::Decimal> for crate::Decimal {}
260
261impl FlussoValue<kind::Date> for String {}
262impl FlussoValue<kind::Date> for &str {}
263#[cfg(feature = "chrono")]
264impl FlussoValue<kind::Date> for chrono::NaiveDate {}
265#[cfg(feature = "chrono")]
266impl FlussoValue<kind::Date> for chrono::NaiveDateTime {}
267#[cfg(feature = "chrono")]
268impl FlussoValue<kind::Date> for chrono::DateTime<chrono::Utc> {}
269
270/// A Rust type usable as the **document type** of a `map` field of value kind
271/// `K`: a dynamic-key object whose values are all of kind `K`.
272///
273/// The canonical map type — `HashMap<String, V>` where `V` is a `K` value — is
274/// pre-implemented via a blanket impl, so `HashMap<String, String>` is a valid
275/// `text`/`keyword` map and `HashMap<String, i64>` a valid `long` map with no
276/// extra code. A whole-map newtype wrapper (`struct Translations(HashMap<…>)`)
277/// opts in with `#[derive(FlussoMap)]`. `FlussoDocument` emits a deferred bound
278/// on this trait for a `map` field, so the document only compiles when its type
279/// genuinely fits the declared value kind.
280#[diagnostic::on_unimplemented(
281    message = "`{Self}` is not a valid map for a `{K}` field",
282    label = "unsupported map type",
283    note = "use `HashMap<String, V>` with a `{K}` value type, or add `#[derive(FlussoMap)]` (with the matching kind) to `{Self}`"
284)]
285pub trait FlussoMap<K> {}
286
287impl<K, V: FlussoValue<K>> FlussoMap<K> for std::collections::HashMap<String, V> {}