Skip to main content

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;