Skip to main content

flusso_query/handles/
sort.rs

1//! Sort keys: [`SortOrder`], [`SortMode`], and the [`Sort`] builder produced by
2//! `.asc()` / `.desc()` on a sortable handle (or `Geo::distance_sort`,
3//! [`Sort::score`], [`Sort::script`]).
4//!
5//! A [`Sort`] carries the key it sorts on (a field path, `_score`,
6//! `_geo_distance`, or `_script`) plus its options (`missing`, `mode`,
7//! `unmapped_type`, …); `.missing_first()` / `.mode(..)` chain onto it, and it
8//! renders to one entry in the `sort` array. The typed handle is always the
9//! entry point — there is no public string-path sort.
10
11use std::marker::PhantomData;
12
13use serde_json::{Map, Value};
14
15use super::{Geo, GeoPoint, NumericType, ScriptSortType};
16use crate::query::{AsQuery, Root};
17use crate::{FlussoDocument, nested_boundaries};
18
19/// Sort direction.
20#[derive(Debug, Clone, Copy)]
21pub enum SortOrder {
22    /// Ascending.
23    Asc,
24    /// Descending.
25    Desc,
26}
27
28impl SortOrder {
29    pub(crate) fn as_str(self) -> &'static str {
30        match self {
31            SortOrder::Asc => "asc",
32            SortOrder::Desc => "desc",
33        }
34    }
35}
36
37/// How a multi-valued field collapses to one sort value.
38#[derive(Debug, Clone, Copy)]
39pub enum SortMode {
40    /// Smallest value.
41    Min,
42    /// Largest value.
43    Max,
44    /// Arithmetic mean (numeric fields).
45    Avg,
46    /// Sum (numeric fields).
47    Sum,
48    /// Median (numeric fields).
49    Median,
50}
51
52impl SortMode {
53    fn as_str(self) -> &'static str {
54        match self {
55            SortMode::Min => "min",
56            SortMode::Max => "max",
57            SortMode::Avg => "avg",
58            SortMode::Sum => "sum",
59            SortMode::Median => "median",
60        }
61    }
62}
63
64/// A single sort key. Produced by `.asc()` / `.desc()` on a sortable handle, by
65/// [`Sort::score`] / [`Sort::script`], or by `Geo::distance_sort`; chain the
66/// option setters (`missing_first`, `mode`, `unmapped_type`, …) onto it.
67#[derive(Debug, Clone)]
68pub struct Sort {
69    key: String,
70    body: Map<String, Value>,
71    /// What [`SortBuilder`] dedups on. Equals `key` for a normal sort; a
72    /// `_script` map-key sort sets it to its field path so several still coexist.
73    dedup_id: String,
74    /// `Some` for a map-key `_script` sort — redirects `missing_*` into the
75    /// script's `params.missing` (the `missing` body field is ignored on a
76    /// `_script` sort) with a direction-correct sentinel for the value kind.
77    script_kind: Option<MapSortValueKind>,
78}
79
80impl Sort {
81    /// A field/order sort: `{ "<field>": { "order": "asc"|"desc" } }`.
82    pub(crate) fn new(field: &str, order: SortOrder) -> Self {
83        let mut body = Map::new();
84        body.insert(
85            "order".to_string(),
86            Value::String(order.as_str().to_string()),
87        );
88        Self::plain(field.to_string(), body)
89    }
90
91    /// A sort with no script-missing redirection, deduped on its render key.
92    fn plain(key: String, body: Map<String, Value>) -> Self {
93        Self {
94            dedup_id: key.clone(),
95            key,
96            body,
97            script_kind: None,
98        }
99    }
100
101    /// Sort by relevance `_score` (descending by default).
102    #[must_use]
103    pub fn score() -> Self {
104        let mut body = Map::new();
105        body.insert("order".to_string(), Value::String("desc".to_string()));
106        Self::plain("_score".to_string(), body)
107    }
108
109    /// Sort by a computed script value. `script_type` is the emitted value type
110    /// ([`ScriptSortType::Number`] / [`ScriptSortType::String`]); `source` is
111    /// the painless expression.
112    #[must_use]
113    pub fn script(
114        script_type: ScriptSortType,
115        source: impl Into<String>,
116        order: SortOrder,
117    ) -> Self {
118        let mut script = Map::new();
119        script.insert("source".to_string(), Value::String(source.into()));
120        let mut body = Map::new();
121        body.insert(
122            "type".to_string(),
123            Value::String(script_type.as_str().to_string()),
124        );
125        body.insert("script".to_string(), Value::Object(script));
126        body.insert(
127            "order".to_string(),
128            Value::String(order.as_str().to_string()),
129        );
130        Self::plain("_script".to_string(), body)
131    }
132
133    /// A pre-built sort clause (e.g. `_geo_distance`).
134    pub(crate) fn from_parts(key: String, body: Map<String, Value>) -> Self {
135        Self::plain(key, body)
136    }
137
138    /// A map-key `_script` sort: deduped on `dedup_id` (the field path, so two
139    /// map sorts coexist) with `missing_*` redirecting into `params.missing`.
140    pub(crate) fn map_script(
141        dedup_id: String,
142        body: Map<String, Value>,
143        kind: MapSortValueKind,
144    ) -> Self {
145        Self {
146            key: "_script".to_string(),
147            body,
148            dedup_id,
149            script_kind: Some(kind),
150        }
151    }
152
153    /// A field sort that is **nesting-aware**: it reads the scope `S`'s path and,
154    /// when the field sits inside one or more `nested` arrays, wraps the sort in
155    /// the matching `nested` chain (and defaults `mode` from the direction —
156    /// `asc → min`, `desc → max`). A root or flattened-object field (empty path)
157    /// renders a plain sort. Backs every [`Sortable`] handle.
158    pub(crate) fn field<S: FlussoDocument>(path: &str, order: SortOrder) -> Self {
159        let mut sort = Sort::new(path, order);
160        let boundaries = nested_boundaries(S::PATH);
161        if let Some(nested) = nested_clause(&boundaries) {
162            sort.body.insert("nested".to_string(), nested);
163            sort.body.insert(
164                "mode".to_string(),
165                Value::String(default_mode(order).to_string()),
166            );
167        }
168        sort
169    }
170
171    /// Sort ascending.
172    #[must_use]
173    pub fn asc(mut self) -> Self {
174        self.body
175            .insert("order".to_string(), Value::String("asc".to_string()));
176        self
177    }
178
179    /// Sort descending.
180    #[must_use]
181    pub fn desc(mut self) -> Self {
182        self.body
183            .insert("order".to_string(), Value::String("desc".to_string()));
184        self
185    }
186
187    /// Place documents missing this field first. On a map-key sort this resolves
188    /// to a direction-correct sentinel in `params.missing` (a `_script` sort
189    /// ignores the `missing` body field).
190    #[must_use]
191    pub fn missing_first(mut self) -> Self {
192        match self.script_kind {
193            Some(kind) => {
194                let value = missing_sentinel(false, self.current_order(), kind);
195                self.set_script_missing(value);
196            }
197            None => {
198                self.body
199                    .insert("missing".to_string(), Value::String("_first".to_string()));
200            }
201        }
202        self
203    }
204
205    /// Place documents missing this field last. On a map-key sort this resolves
206    /// to a direction-correct sentinel in `params.missing`.
207    #[must_use]
208    pub fn missing_last(mut self) -> Self {
209        match self.script_kind {
210            Some(kind) => {
211                let value = missing_sentinel(true, self.current_order(), kind);
212                self.set_script_missing(value);
213            }
214            None => {
215                self.body
216                    .insert("missing".to_string(), Value::String("_last".to_string()));
217            }
218        }
219        self
220    }
221
222    /// Substitute a literal value for documents missing this field. On a map-key
223    /// sort this becomes the `params.missing` fallback value.
224    #[must_use]
225    pub fn missing(mut self, value: impl Into<Value>) -> Self {
226        let value = value.into();
227        match self.script_kind {
228            Some(_) => self.set_script_missing(value),
229            None => {
230                self.body.insert("missing".to_string(), value);
231            }
232        }
233        self
234    }
235
236    /// This sort's current direction, read back from the rendered body.
237    fn current_order(&self) -> SortOrder {
238        match self.body.get("order").and_then(Value::as_str) {
239            Some("desc") => SortOrder::Desc,
240            _ => SortOrder::Asc,
241        }
242    }
243
244    /// Write `params.missing` for a `_script` sort (no-op if the shape is unexpected).
245    fn set_script_missing(&mut self, value: Value) {
246        if let Some(params) = self
247            .body
248            .get_mut("script")
249            .and_then(Value::as_object_mut)
250            .and_then(|script| script.get_mut("params"))
251            .and_then(Value::as_object_mut)
252        {
253            params.insert("missing".to_string(), value);
254        }
255    }
256
257    /// How a multi-valued field reduces to one sort value.
258    #[must_use]
259    pub fn mode(mut self, mode: SortMode) -> Self {
260        self.body
261            .insert("mode".to_string(), Value::String(mode.as_str().to_string()));
262        self
263    }
264
265    /// Type to assume when the field is unmapped on some shard (instead of
266    /// failing the search), e.g. `"long"`. A no-op on a map-key `_script` sort —
267    /// it's a field-sort option with no meaning there.
268    #[must_use]
269    pub fn unmapped_type(mut self, unmapped_type: impl Into<String>) -> Self {
270        if self.script_kind.is_none() {
271            self.body.insert(
272                "unmapped_type".to_string(),
273                Value::String(unmapped_type.into()),
274            );
275        }
276        self
277    }
278
279    /// Numeric type to sort as ([`NumericType`]), for cross-index type coercion.
280    /// A no-op on a map-key `_script` sort — a field-sort-only option.
281    #[must_use]
282    pub fn numeric_type(mut self, numeric_type: NumericType) -> Self {
283        if self.script_kind.is_none() {
284            self.body.insert(
285                "numeric_type".to_string(),
286                Value::String(numeric_type.as_str().to_string()),
287            );
288        }
289        self
290    }
291
292    /// Date `format` for a `date` field sort. A no-op on a map-key `_script`
293    /// sort — a field-sort-only option.
294    #[must_use]
295    pub fn format(mut self, format: impl Into<String>) -> Self {
296        if self.script_kind.is_none() {
297            self.body
298                .insert("format".to_string(), Value::String(format.into()));
299        }
300        self
301    }
302
303    /// Sort by a field inside a `nested` array scoped to `path`, considering
304    /// only elements matching `filter`. An escape hatch for the rare
305    /// filter-scoped nested sort; ordinary nested sorts come from a
306    /// [`Sortable`] handle, which derives the (possibly multi-level) `nested`
307    /// chain from the field's scope automatically.
308    #[must_use]
309    pub fn nested_filtered<S>(mut self, path: impl Into<String>, filter: impl AsQuery<S>) -> Self {
310        let mut nested = Map::new();
311        nested.insert("path".to_string(), Value::String(path.into()));
312        if let Some(query) = filter.into_query() {
313            nested.insert("filter".to_string(), query.to_value());
314        }
315        self.body
316            .insert("nested".to_string(), Value::Object(nested));
317        self
318    }
319
320    pub(crate) fn to_value(&self) -> Value {
321        let mut outer = Map::new();
322        outer.insert(self.key.clone(), Value::Object(self.body.clone()));
323        Value::Object(outer)
324    }
325
326    /// What [`SortBuilder`] dedups on — the render key for a normal sort, or the
327    /// field path for a map-key `_script` sort (so several still coexist).
328    pub(crate) fn dedup_id(&self) -> &str {
329        &self.dedup_id
330    }
331
332    /// Drop the `nested` chain (and its companion `mode`) for use inside
333    /// `inner_hits`: there the sort already runs within the nested document, so
334    /// the field path is relative and no wrapper applies. A plain or `_score`
335    /// sort is unchanged.
336    pub(crate) fn without_nested_context(mut self) -> Self {
337        if self.body.remove("nested").is_some() {
338            self.body.remove("mode");
339        }
340        self
341    }
342}
343
344/// Wrap a plain sort in the `nested` chain for `boundaries` (cumulative dotted
345/// paths, outermost first). `None` when there is no nesting.
346fn nested_clause(boundaries: &[String]) -> Option<Value> {
347    let (path, rest) = boundaries.split_first()?;
348    let mut clause = Map::new();
349    clause.insert("path".to_string(), Value::String(path.clone()));
350    if let Some(inner) = nested_clause(rest) {
351        clause.insert("nested".to_string(), inner);
352    }
353    Some(Value::Object(clause))
354}
355
356/// The `mode` a nested sort defaults to for `order`: the smallest element value
357/// when ascending, the largest when descending (so the chosen element is the one
358/// that sorts the parent extremally).
359fn default_mode(order: SortOrder) -> &'static str {
360    match order {
361        SortOrder::Asc => "min",
362        SortOrder::Desc => "max",
363    }
364}
365
366/// A handle that can produce a [`Sort`]. The compile-time gate for
367/// [`SortBuilder::by`] / [`tiebreak`](SortBuilder::tiebreak): implemented for the
368/// orderable leaf handles (`Keyword`, `Text`, `Number<K>`, `Date`, `Bool`) and
369/// for a [`MapKeySort`] (`Type::field().sort_key(..)`), but **not** for a bare
370/// `Geo` / `Object` / map handle — so `by(geo_handle, …)` or `by(map_handle, …)`
371/// fails to compile (geo sorts go through [`SortBuilder::near`] /
372/// [`raw`](SortBuilder::raw); a map sorts via its `sort_key`).
373///
374/// `.asc()` / `.desc()` are nesting-aware: a field (or map) inside one or more
375/// `nested` arrays renders the matching `nested` chain automatically, from the
376/// scope.
377pub trait Sortable {
378    /// Sort ascending.
379    fn asc(&self) -> Sort;
380    /// Sort descending.
381    fn desc(&self) -> Sort;
382}
383
384/// Where to place documents missing the sorted field (a field-sort `missing`).
385#[derive(Debug, Clone)]
386pub enum Missing {
387    /// Missing values sort first (`_first`).
388    First,
389    /// Missing values sort last (`_last`).
390    Last,
391    /// Missing values take this substitute value.
392    Value(Value),
393}
394
395/// The doc-values shape of a `map`'s values, which decides how
396/// [`MapKeySort`] reads a key: the exact `.keyword` subfield + lowercasing for a
397/// string map, the bare numeric/date field otherwise.
398#[derive(Debug, Clone, Copy)]
399pub(crate) enum MapSortValueKind {
400    /// `text`/`keyword` values — sort on the dynamic `.keyword` subfield,
401    /// lowercased for case-insensitive order (parity with scalar string sort).
402    String,
403    /// Numeric values — sort on the bare key field.
404    Number,
405    /// Date values — sort on the bare key field, by epoch millis.
406    Date,
407}
408
409/// `for (def f : params.fields) { if present → return … }` then the `missing`
410/// fallback. Walking `params.fields` in order is the whole point: it's the
411/// **key fallback** (sort by the first preferred key a document actually has).
412const STRING_SOURCE: &str = "for (def f : params.fields) { if (doc.containsKey(f) && doc[f].size() > 0) { return doc[f].value.toLowerCase(); } } return params.missing;";
413const NUMBER_SOURCE: &str = "for (def f : params.fields) { if (doc.containsKey(f) && doc[f].size() > 0) { return doc[f].value; } } return params.missing;";
414const DATE_SOURCE: &str = "for (def f : params.fields) { if (doc.containsKey(f) && doc[f].size() > 0) { return doc[f].value.toInstant().toEpochMilli(); } } return params.missing;";
415
416/// The `params.missing` sentinel that places key-less documents first or last on
417/// a map-key `_script` sort, given the direction and value kind. The extreme
418/// flips with direction so the rule holds either way: missing-last is a high
419/// value under `asc`, a low value under `desc`.
420fn missing_sentinel(last: bool, order: SortOrder, kind: MapSortValueKind) -> Value {
421    let high = last == matches!(order, SortOrder::Asc);
422    match kind {
423        MapSortValueKind::String => Value::String(if high {
424            "\u{10ffff}".to_string()
425        } else {
426            String::new()
427        }),
428        MapSortValueKind::Number | MapSortValueKind::Date => {
429            Value::from(if high { i64::MAX } else { i64::MIN })
430        }
431    }
432}
433
434/// A sort over a dynamic-key `map` field by an **ordered fallback of keys** —
435/// "sort by `it`, else `en`, …". Built with `*Map::sort_key("it").or("en")` and
436/// used like any other sortable handle: `SortBuilder::by(handle, dir)`, or
437/// `.asc()` / `.desc()` for a bare [`Sort`].
438///
439/// It renders a `_script` sort whose painless source walks the keys in order and
440/// sorts by the value of the **first one a document has** — true fallback, so a
441/// row with only `en` still orders by `en` (not lexicographic tiers). String
442/// maps sort case-insensitively on the dynamic `.keyword` subfield; numeric/date
443/// maps on the bare key (epoch millis for dates). Nesting-aware via scope `S`,
444/// exactly like a field sort.
445///
446/// Documents with **none** of the keys sort first under `.asc()` / last under
447/// `.desc()` by default; place them explicitly with `.missing_first()` /
448/// `.missing_last()` / `.missing(value)` on the produced [`Sort`] (or via the
449/// [`OrderBy`] passed to [`SortBuilder::by`]) — these redirect into the script's
450/// fallback value, so they work despite a `_script` sort ignoring `missing`.
451///
452/// ```
453/// use flusso_query::{Root, SortBuilder, SortOrder, TextMap};
454///
455/// // Italian, falling back to English — through the normal `by`.
456/// let sorts = SortBuilder::new()
457///     .by(TextMap::<Root>::at("name").sort_key("it").or("en"), SortOrder::Desc)
458///     .build();
459/// assert_eq!(sorts.len(), 1);
460/// ```
461#[derive(Debug, Clone)]
462pub struct MapKeySort<S = Root> {
463    path: String,
464    keys: Vec<String>,
465    kind: MapSortValueKind,
466    _scope: PhantomData<fn() -> S>,
467}
468
469impl<S> MapKeySort<S> {
470    pub(crate) fn new(path: String, key: impl Into<String>, kind: MapSortValueKind) -> Self {
471        Self {
472            path,
473            keys: vec![key.into()],
474            kind,
475            _scope: PhantomData,
476        }
477    }
478
479    /// Add the next fallback key, tried only for documents missing every key so
480    /// far. Chain several for a longer preference order (`it` → `en` → `de`).
481    #[must_use]
482    pub fn or(mut self, key: impl Into<String>) -> Self {
483        self.keys.push(key.into());
484        self
485    }
486
487    fn leaf_field(&self, key: &str) -> String {
488        match self.kind {
489            MapSortValueKind::String => format!("{}.{key}.keyword", self.path),
490            MapSortValueKind::Number | MapSortValueKind::Date => format!("{}.{key}", self.path),
491        }
492    }
493}
494
495impl<S: FlussoDocument> MapKeySort<S> {
496    fn build(&self, order: SortOrder) -> Sort {
497        let fields: Vec<Value> = self
498            .keys
499            .iter()
500            .map(|key| Value::String(self.leaf_field(key)))
501            .collect();
502
503        let (sort_type, source, default_missing) = match self.kind {
504            MapSortValueKind::String => ("string", STRING_SOURCE, Value::String(String::new())),
505            MapSortValueKind::Number => ("number", NUMBER_SOURCE, Value::from(0)),
506            MapSortValueKind::Date => ("number", DATE_SOURCE, Value::from(0)),
507        };
508
509        let mut params = Map::new();
510        params.insert("fields".to_string(), Value::Array(fields));
511        params.insert("missing".to_string(), default_missing);
512
513        let mut script = Map::new();
514        script.insert("source".to_string(), Value::String(source.to_string()));
515        script.insert("params".to_string(), Value::Object(params));
516
517        let mut body = Map::new();
518        body.insert("type".to_string(), Value::String(sort_type.to_string()));
519        body.insert("script".to_string(), Value::Object(script));
520        body.insert(
521            "order".to_string(),
522            Value::String(order.as_str().to_string()),
523        );
524
525        let boundaries = nested_boundaries(S::PATH);
526        if let Some(nested) = nested_clause(&boundaries) {
527            body.insert("nested".to_string(), nested);
528            body.insert(
529                "mode".to_string(),
530                Value::String(default_mode(order).to_string()),
531            );
532        }
533
534        Sort::map_script(self.path.clone(), body, self.kind)
535    }
536}
537
538/// `.asc()` / `.desc()` build the `_script` sort; a bare value defaults to `asc`.
539impl<S: FlussoDocument> Sortable for MapKeySort<S> {
540    fn asc(&self) -> Sort {
541        self.build(SortOrder::Asc)
542    }
543    fn desc(&self) -> Sort {
544        self.build(SortOrder::Desc)
545    }
546}
547
548impl<S: FlussoDocument> From<MapKeySort<S>> for Sort {
549    fn from(map_sort: MapKeySort<S>) -> Self {
550        map_sort.build(SortOrder::Asc)
551    }
552}
553
554impl<S: FlussoDocument> From<MapKeySort<S>> for Option<Sort> {
555    fn from(map_sort: MapKeySort<S>) -> Self {
556        Some(map_sort.into())
557    }
558}
559
560/// A field sort minus the field — a direction plus the field-sort modifiers,
561/// ready to attach to whatever handle [`SortBuilder::by`] is given.
562///
563/// This is what a consumer converts its own request enum into, once
564/// (`impl From<MyDir> for OrderBy`). It carries full parity with [`Sort`]'s
565/// field-sort options; everything but the direction defaults to unset.
566#[derive(Debug, Clone)]
567pub struct OrderBy {
568    order: SortOrder,
569    missing: Option<Missing>,
570    mode: Option<SortMode>,
571    numeric_type: Option<NumericType>,
572    unmapped_type: Option<String>,
573    format: Option<String>,
574}
575
576impl OrderBy {
577    /// Ascending, no modifiers.
578    #[must_use]
579    pub fn asc() -> Self {
580        Self::new(SortOrder::Asc)
581    }
582
583    /// Descending, no modifiers.
584    #[must_use]
585    pub fn desc() -> Self {
586        Self::new(SortOrder::Desc)
587    }
588
589    fn new(order: SortOrder) -> Self {
590        Self {
591            order,
592            missing: None,
593            mode: None,
594            numeric_type: None,
595            unmapped_type: None,
596            format: None,
597        }
598    }
599
600    /// Place documents missing this field first.
601    #[must_use]
602    pub fn missing_first(mut self) -> Self {
603        self.missing = Some(Missing::First);
604        self
605    }
606
607    /// Place documents missing this field last.
608    #[must_use]
609    pub fn missing_last(mut self) -> Self {
610        self.missing = Some(Missing::Last);
611        self
612    }
613
614    /// Substitute a literal value for documents missing this field.
615    #[must_use]
616    pub fn missing(mut self, value: impl Into<Value>) -> Self {
617        self.missing = Some(Missing::Value(value.into()));
618        self
619    }
620
621    /// How a multi-valued field reduces to one sort value.
622    #[must_use]
623    pub fn mode(mut self, mode: SortMode) -> Self {
624        self.mode = Some(mode);
625        self
626    }
627
628    /// Numeric type to sort as, for cross-index type coercion.
629    #[must_use]
630    pub fn numeric_type(mut self, numeric_type: NumericType) -> Self {
631        self.numeric_type = Some(numeric_type);
632        self
633    }
634
635    /// Type to assume when the field is unmapped on some shard.
636    #[must_use]
637    pub fn unmapped_type(mut self, unmapped_type: impl Into<String>) -> Self {
638        self.unmapped_type = Some(unmapped_type.into());
639        self
640    }
641
642    /// Date `format` for a `date` field sort.
643    #[must_use]
644    pub fn format(mut self, format: impl Into<String>) -> Self {
645        self.format = Some(format.into());
646        self
647    }
648
649    /// Build the field [`Sort`] for `handle`, in this order with these modifiers.
650    fn into_sort<H: Sortable>(self, handle: &H) -> Sort {
651        let mut sort = match self.order {
652            SortOrder::Asc => handle.asc(),
653            SortOrder::Desc => handle.desc(),
654        };
655        sort = match self.missing {
656            Some(Missing::First) => sort.missing_first(),
657            Some(Missing::Last) => sort.missing_last(),
658            Some(Missing::Value(value)) => sort.missing(value),
659            None => sort,
660        };
661        if let Some(mode) = self.mode {
662            sort = sort.mode(mode);
663        }
664        if let Some(numeric_type) = self.numeric_type {
665            sort = sort.numeric_type(numeric_type);
666        }
667        if let Some(unmapped_type) = self.unmapped_type {
668            sort = sort.unmapped_type(unmapped_type);
669        }
670        if let Some(format) = self.format {
671            sort = sort.format(format);
672        }
673        sort
674    }
675}
676
677impl From<SortOrder> for OrderBy {
678    fn from(order: SortOrder) -> Self {
679        Self::new(order)
680    }
681}
682
683/// The optionality carrier for [`SortBuilder::by`]: an absent order skips the
684/// field. A local newtype because coherence forbids `impl From<…> for Option<_>`.
685///
686/// `SortOrder`, `OrderBy`, and — via the umbrella impl — `Option<T: Into<OrderBy>>`
687/// all flow in, so a consumer's `Option<MyDir>` self-skips on `None` after one
688/// `impl From<MyDir> for OrderBy`.
689#[derive(Debug, Clone)]
690pub struct MaybeOrderBy(Option<OrderBy>);
691
692impl From<OrderBy> for MaybeOrderBy {
693    fn from(order: OrderBy) -> Self {
694        Self(Some(order))
695    }
696}
697
698impl From<SortOrder> for MaybeOrderBy {
699    fn from(order: SortOrder) -> Self {
700        Self(Some(order.into()))
701    }
702}
703
704impl<T: Into<OrderBy>> From<Option<T>> for MaybeOrderBy {
705    fn from(order: Option<T>) -> Self {
706        Self(order.map(Into::into))
707    }
708}
709
710/// Builds the `sort` array, one fluent verb per concern — each absorbing its own
711/// optionality so a request maps straight through with no per-field `if let`.
712///
713/// `by`/`near`/`tiebreak`/`or_default` **dedup** by sort key (first wins), so a
714/// field added twice — or an explicit sort that a tiebreak/default would repeat —
715/// appears once; `raw` is exempt. `or_default` only contributes when the builder
716/// would otherwise be empty.
717///
718/// ```
719/// use flusso_query::{SortBuilder, SortOrder, OrderBy};
720/// # use flusso_query::{Keyword, Number, kind, Root};
721/// # fn keyword(p: &str) -> Keyword<Root> { Keyword::at(p) }
722/// # fn count() -> Number<kind::Long, Root> { Number::at("orderCount") }
723/// let sorts = SortBuilder::new()
724///     .score_if(true)
725///     .by(count(), SortOrder::Desc)
726///     .by(keyword("city"), None::<OrderBy>)   // skipped
727///     .tiebreak(keyword("id"))
728///     .build();
729/// assert_eq!(sorts.len(), 3);                 // _score, orderCount, id
730/// ```
731#[derive(Debug, Default)]
732pub struct SortBuilder {
733    sorts: Vec<Sort>,
734    fallback: Option<Sort>,
735}
736
737impl SortBuilder {
738    /// An empty builder.
739    #[must_use]
740    pub fn new() -> Self {
741        Self::default()
742    }
743
744    /// Push `sort` unless its dedup id is already present (first wins).
745    fn push_unique(&mut self, sort: Sort) {
746        if !self
747            .sorts
748            .iter()
749            .any(|existing| existing.dedup_id() == sort.dedup_id())
750        {
751            self.sorts.push(sort);
752        }
753    }
754
755    /// Sort by a field — or by a map key with fallback
756    /// (`Type::field().sort_key("it").or("en")`). `dir` accepts a [`SortOrder`],
757    /// an [`OrderBy`], or an `Option` of either (a `None` skips it), so a
758    /// request's `Option<dir>` flows straight in. Nesting-aware: a field (or map)
759    /// inside `nested` arrays renders the right `nested` chain from its scope.
760    ///
761    /// Map-key sorts render a `_script` sort but dedup on the field path, so
762    /// several still coexist; an [`OrderBy`]'s `missing_first`/`missing_last`
763    /// resolves to a direction-correct fallback value (a `_script` sort can't use
764    /// the `missing` field). `numeric_type`/`unmapped_type`/`format` are dropped
765    /// (field-sort-only); `mode` is kept (it's valid on a `_script` sort).
766    #[must_use]
767    pub fn by<H: Sortable>(mut self, handle: H, dir: impl Into<MaybeOrderBy>) -> Self {
768        if let Some(order) = dir.into().0 {
769            self.push_unique(order.into_sort(&handle));
770        }
771        self
772    }
773
774    /// Sort by distance from `center` (`_geo_distance`, nearest first). A `None`
775    /// center skips it. Pass a unit / script geo sort through [`raw`](Self::raw).
776    #[must_use]
777    pub fn near<S>(mut self, handle: Geo<S>, center: impl Into<Option<GeoPoint>>) -> Self {
778        if let Some(center) = center.into() {
779            self.push_unique(handle.distance_from(center));
780        }
781        self
782    }
783
784    /// Sort by relevance `_score` (descending).
785    #[must_use]
786    pub fn score(mut self) -> Self {
787        self.push_unique(Sort::score());
788        self
789    }
790
791    /// Sort by `_score` only when `cond` holds (e.g. a free-text query is present).
792    #[must_use]
793    pub fn score_if(self, cond: bool) -> Self {
794        if cond { self.score() } else { self }
795    }
796
797    /// Append a pre-built [`Sort`] verbatim — the escape hatch for sorts the
798    /// typed verbs don't cover (`_script`, a geo sort with options). A `None`
799    /// adds nothing. **Not** deduped.
800    #[must_use]
801    pub fn raw(mut self, sort: impl Into<Option<Sort>>) -> Self {
802        if let Some(sort) = sort.into() {
803            self.sorts.push(sort);
804        }
805        self
806    }
807
808    /// A stable final sort key (ascending) — append a unique field so equal
809    /// leading keys still page deterministically.
810    #[must_use]
811    pub fn tiebreak<H: Sortable>(mut self, handle: H) -> Self {
812        self.push_unique(handle.asc());
813        self
814    }
815
816    /// A fallback used only if nothing else lands in the builder.
817    #[must_use]
818    pub fn or_default(mut self, sort: impl Into<Sort>) -> Self {
819        if self.fallback.is_none() {
820            self.fallback = Some(sort.into());
821        }
822        self
823    }
824
825    /// Finish: the `sort` array (the fallback, if set, when otherwise empty).
826    #[must_use]
827    pub fn build(mut self) -> Vec<Sort> {
828        if self.sorts.is_empty()
829            && let Some(fallback) = self.fallback
830        {
831            self.sorts.push(fallback);
832        }
833        self.sorts
834    }
835}
836
837impl IntoIterator for SortBuilder {
838    type Item = Sort;
839    type IntoIter = std::vec::IntoIter<Sort>;
840
841    fn into_iter(self) -> Self::IntoIter {
842        self.build().into_iter()
843    }
844}