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}