tablo_core/table/group.rs
1//! A table's groupings: the fields a reader may group its rows by. A grouping orders the query
2//! by its field ahead of the sort, so a group runs on across pages rather than within each.
3
4use std::sync::Arc;
5
6use derive_where::derive_where;
7use toasty::stmt::OrderByExpr;
8
9use super::{Table, TableState};
10use crate::{DeclarationErrorKind, Lens, schema::Binding, values::FormScalar};
11
12/// Group-label projection reads a row's group off one model instance: `None` for a row holding
13/// no value.
14pub(crate) type GroupKey<M> = Arc<dyn Fn(&M) -> Option<crate::Text> + Send + Sync>;
15
16/// A named grouping a `Table` offers.
17#[derive_where(Clone)]
18pub(crate) struct GroupDef<M> {
19 binding: Binding,
20 key: GroupKey<M>,
21 /// The field's ascending order-by, which the grouping puts ahead of the sort.
22 order: OrderByExpr,
23}
24
25impl<M> GroupDef<M> {
26 /// The `?group_by=` value naming this grouping: its field's name.
27 pub(crate) fn name(&self) -> &str {
28 self.binding.name()
29 }
30
31 /// The grouping's label, its field's.
32 pub(crate) fn label(&self) -> crate::Text {
33 self.binding.label()
34 }
35
36 /// The label of the group `record` falls in.
37 pub(crate) fn key(&self) -> &GroupKey<M> {
38 &self.key
39 }
40
41 /// The collapse token of the group labelled `label`, `None` for the rows holding no value:
42 /// the grouping's name, so a group collapsed under one grouping leaves another's be, then
43 /// the label, as [`bulk_token`](super::bulk_token) spells it. No token is empty, so none
44 /// matches inside another's commas.
45 pub(crate) fn token(&self, label: Option<&str>) -> String {
46 match label {
47 Some(label) => super::bulk_token(&format!("{}={label}", self.name())),
48 None => super::bulk_token(&format!("{}!", self.name())),
49 }
50 }
51
52 /// What the grouping reads of a record.
53 pub(super) fn reads(&self) -> crate::lens::Reads<M> {
54 self.binding.reads::<M>()
55 }
56
57 /// The ascending order-by putting each group's rows together.
58 pub(super) fn order(&self) -> &OrderByExpr {
59 &self.order
60 }
61}
62
63impl<M> Table<M> {
64 /// Offer grouping the rows by the field `lens` reads, under the field's name. Call it once
65 /// per field a reader may group by; `?group_by=` picks one, and the list groups by none
66 /// without it.
67 ///
68 /// ```rust
69 /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
70 /// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String, status: String, author: String }
71 /// use tablo_core::{Table, TextColumn, lens};
72 ///
73 /// Table::new(TextColumn::new(lens!(Post.title)))
74 /// .group_by(lens!(Post.status))
75 /// .group_by(lens!(Post.author));
76 /// ```
77 ///
78 /// A grouping orders the rows by its field ahead of the sort, so each group runs on across
79 /// pages, and its header shows its field's value, or "No value" over the rows holding none.
80 /// Two groupings by one field are a declaration error.
81 pub fn group_by<T>(mut self, lens: Lens<M, T>) -> Self
82 where
83 M: toasty::schema::Model + Send + Sync + 'static,
84 T: FormScalar + Send + Sync + 'static,
85 {
86 let order = lens.path().clone().asc();
87 self.groupings.push(GroupDef {
88 binding: Binding::of(lens.path()),
89 key: Arc::new(move |record| {
90 let value = lens.read(record);
91 (!value.to_form().is_empty()).then(|| value.to_label())
92 }),
93 order,
94 });
95 self
96 }
97
98 /// The groupings a reader may pick, in declaration order.
99 pub(crate) fn declared_groupings(&self) -> &[GroupDef<M>] {
100 &self.groupings
101 }
102
103 /// The grouping `state.group_by` names, if the table declares it.
104 pub(crate) fn active_grouping(&self, state: &TableState) -> Option<&GroupDef<M>> {
105 let want = state.group_by.as_deref()?;
106 self.groupings.iter().find(|def| def.name() == want)
107 }
108
109 /// Normalize `state.group_by` against the declared groupings.
110 pub(crate) fn normalize_state(&self, state: &TableState) -> TableState {
111 let mut out = state.clone();
112 if self.active_grouping(state).is_none() {
113 out.group_by = None;
114 }
115 out
116 }
117
118 /// What is wrong with the groupings: a path naming no field, or two on one field.
119 pub(super) fn grouping_errors(&self) -> Vec<DeclarationErrorKind> {
120 let mut errors = Vec::new();
121 let mut seen = std::collections::HashSet::new();
122 for def in &self.groupings {
123 if let Some(error) = def.binding.misdeclared() {
124 errors.push(error);
125 } else if !seen.insert(def.name()) {
126 errors.push(DeclarationErrorKind::DuplicateGrouping {
127 name: def.name().to_string(),
128 });
129 }
130 }
131 errors
132 }
133}
134
135#[cfg(test)]
136mod tests;