use std::{marker::PhantomData, num::NonZeroUsize, sync::Arc};
use toasty::stmt::{Expr, List, OrderByExpr};
use self::{column::BoxColumn, filter::BoxFilter};
use crate::{
DeclarationErrorKind, Lens,
form::FormScalar,
schema::{Binding, FieldResolver},
};
mod column;
mod export;
mod filter;
mod page;
mod render;
mod state;
mod wiring;
pub use self::{
column::{
BooleanColumn, Column, ColumnWidth, ComputedColumn, Includes, IntoColumns, TextColumn,
contains_expr,
},
filter::{
DateFilter, Filter, FilterInput, IntoFilters, QueryFilter, SelectFilter, TernaryFilter,
},
page::TablePage,
state::{Cursor, Sort, TableState},
wiring::WiredTable,
};
pub(crate) use self::{
page::{Past, row_exists_past},
state::{
ACTION_ROUTE_PARAM, ACTIONS_ROUTE_SEGMENT, BULK_DELETE_ROUTE_SEGMENT, CREATE_ROUTE_SEGMENT,
DASH_ROUTE_SEGMENT, DELETE_ROUTE_SEGMENT, EDIT_ROUTE_SEGMENT, RECORD_ROUTE_PARAM,
RETURN_PARAM, create_page_url, with_return,
},
};
pub(crate) type GroupKey<M> = Arc<dyn Fn(&M) -> String + Send + Sync>;
pub(crate) type RowPolicy<M> = Arc<dyn Fn(&M) -> RowActions + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RowActions {
pub view: bool,
pub edit: bool,
pub delete: bool,
}
impl RowActions {
pub const ALL: Self = Self {
view: true,
edit: true,
delete: true,
};
}
pub(crate) struct GroupDef<M> {
binding: Binding,
key: GroupKey<M>,
}
impl<M> Clone for GroupDef<M> {
fn clone(&self) -> Self {
Self {
binding: self.binding.clone(),
key: Arc::clone(&self.key),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct TableChrome {
pub(crate) delete: bool,
pub(crate) edit: bool,
pub(crate) view: bool,
pub(crate) actions: bool,
}
pub(crate) struct TableAction<M> {
pub(crate) name: &'static str,
pub(crate) label: String,
pub(crate) row: bool,
pub(crate) bulk: bool,
pub(crate) confirm: bool,
pub(crate) allowed: Arc<dyn Fn(&M) -> bool + Send + Sync>,
}
pub const DEFAULT_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(25).unwrap();
pub struct Table<M> {
columns: Vec<BoxColumn<M>>,
misdeclared: Vec<DeclarationErrorKind>,
filters: Vec<BoxFilter<M>>,
group_by: Option<GroupDef<M>>,
key: fn(&M) -> String,
addressable: bool,
page_size: NonZeroUsize,
hide_search: bool,
hide_filter_bar: bool,
_marker: PhantomData<M>,
}
impl<M> Clone for Table<M> {
fn clone(&self) -> Self {
Self {
columns: self.columns.clone(),
misdeclared: self.misdeclared.clone(),
filters: self.filters.clone(),
group_by: self.group_by.clone(),
key: self.key,
addressable: self.addressable,
page_size: self.page_size,
hide_search: self.hide_search,
hide_filter_bar: self.hide_filter_bar,
_marker: PhantomData,
}
}
}
impl<M> std::fmt::Debug for Table<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Table")
.field(
"columns",
&self.columns.iter().map(|c| c.name()).collect::<Vec<_>>(),
)
.field("filters", &self.filters.len())
.field("group_by", &self.group_by.is_some())
.field("page_size", &self.page_size)
.field("hide_search", &self.hide_search)
.field("hide_filter_bar", &self.hide_filter_bar)
.finish()
}
}
impl<M> Table<M> {
pub fn new(cols: impl IntoColumns<M>) -> Self
where
M: toasty::schema::Model + toasty::stmt::IntoExpr<M>,
{
Self {
columns: cols.into_columns(),
misdeclared: Vec::new(),
filters: Vec::new(),
group_by: None,
key: crate::toasty_compat::pk::pk_text::<M>,
addressable: !crate::toasty_compat::pk::pk_is_composite::<M>(),
page_size: DEFAULT_PAGE_SIZE,
hide_search: false,
hide_filter_bar: false,
_marker: PhantomData,
}
}
pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
self.columns.push(Arc::new(column));
self
}
pub fn filters(mut self, filters: impl IntoFilters<M>) -> Self
where
M: toasty::schema::Model,
{
let filters = filters.into_filters();
self.filters = filters;
self
}
pub(crate) fn include_relations(
&self,
mut query: toasty::stmt::Query<List<M>>,
) -> toasty::stmt::Query<List<M>>
where
M: toasty::schema::Model,
{
let mut seen: Vec<crate::toasty_compat::UntypedInclude> = Vec::new();
for include in self.columns.iter().flat_map(|c| c.includes().into_vec()) {
if !seen.contains(&include) {
query = query.include(include.clone());
seen.push(include);
}
}
query
}
pub(crate) fn filter_expr(&self, state: &TableState) -> Option<Expr<bool>>
where
M: toasty::schema::Model,
{
let mut exprs = Vec::new();
for f in &self.filters {
if let Some(v) = state.filters.get(f.name())
&& let Some(e) = f.to_expr(v)
{
exprs.push(e);
}
}
if exprs.is_empty() {
None
} else {
let mut iter = exprs.into_iter();
let first = iter.next().unwrap();
Some(iter.fold(first, |acc, e| acc.and(e)))
}
}
pub(crate) fn unapplied_filters(&self, state: &TableState) -> Vec<(String, String)>
where
M: toasty::schema::Model,
{
let mut out = Vec::new();
for (key, value) in &state.filters {
match self.filters.iter().find(|f| f.name() == key) {
None => out.push((format!("{key}:{value}"), "unknown filter".to_string())),
Some(f) if f.to_expr(value).is_none() && !f.is_noop_value(value) => {
out.push((format!("{key}:{value}"), "invalid value".to_string()))
}
Some(_) => {}
}
}
if state.filters_dropped {
out.push((
"dropped filters".to_string(),
format!(
"more than {}, over {} bytes, or the retired filters= form",
state::MAX_FILTERS,
state::MAX_FILTER_LEN
),
));
}
out.sort();
out
}
pub fn group_by<T>(mut self, lens: Lens<M, T>) -> Self
where
M: toasty::schema::Model + Send + Sync + 'static,
T: FormScalar + Send + Sync + 'static,
{
self.group_by = Some(GroupDef {
binding: Binding::of(lens.path()),
key: Arc::new(move |record| lens.read(record).to_form()),
});
self
}
fn effective_group_key(&self, state: &TableState) -> Option<GroupKey<M>> {
match (&self.group_by, &state.group_by) {
(Some(def), Some(want)) if def.binding.name() == want => Some(def.key.clone()),
_ => None,
}
}
pub(crate) fn normalize_state(&self, state: &TableState) -> TableState {
let mut out = state.clone();
if self.group_by.as_ref().map(|def| def.binding.name()) != out.group_by.as_deref() {
out.group_by = None;
}
out
}
pub fn paginate(mut self, per_page: usize) -> Self {
match NonZeroUsize::new(per_page) {
Some(size) => self.page_size = size,
None => self.misdeclared.push(DeclarationErrorKind::ZeroPageSize),
}
self
}
pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
let mut errors = self.misdeclared.clone();
errors.extend(
self.group_by
.as_ref()
.and_then(|def| def.binding.misdeclared()),
);
if self.columns.is_empty() {
errors.push(DeclarationErrorKind::NoColumns);
}
let mut seen = std::collections::HashSet::with_capacity(self.columns.len());
for column in &self.columns {
match column.misdeclared() {
Some(error) => errors.push(error),
None if !seen.insert(column.name()) => {
errors.push(DeclarationErrorKind::DuplicateColumn {
name: column.name().to_string(),
});
}
None => {}
}
}
let mut seen = std::collections::HashSet::with_capacity(self.filters.len());
for filter in &self.filters {
match filter.misdeclared() {
Some(error) => errors.push(error),
None if !seen.insert(filter.name()) => {
errors.push(DeclarationErrorKind::DuplicateFilter {
name: filter.name().to_string(),
});
}
None => {}
}
}
errors
}
pub fn bind(self, db: &toasty::Db) -> Self {
self.bind_with(&FieldResolver::of_db(db));
self
}
pub(crate) fn bind_with(&self, resolver: &FieldResolver) {
for column in &self.columns {
column.bind(resolver);
}
for filter in &self.filters {
filter.bind(resolver);
}
if let Some(def) = &self.group_by {
def.binding.bind(resolver);
}
}
pub fn page_size(&self) -> usize {
self.page_size.get()
}
pub(crate) fn key_of(&self, record: &M) -> String {
(self.key)(record)
}
pub fn hide_search(mut self) -> Self {
self.hide_search = true;
self
}
pub fn hide_filter_bar(mut self) -> Self {
self.hide_filter_bar = true;
self
}
pub(crate) fn search_expr(&self, term: &str) -> Option<Expr<bool>>
where
M: toasty::schema::Model,
{
let t = term.trim();
if t.is_empty() {
return None;
}
let mut exprs = self.columns.iter().filter_map(|c| c.search_expr(t));
let first = exprs.next()?;
Some(exprs.fold(first, |acc, e| acc.or(e)))
}
pub(crate) fn order_by(&self, descending: bool) -> Option<OrderByExpr>
where
M: toasty::schema::Model,
{
self.columns.iter().find_map(|c| c.order_by(descending))
}
pub(crate) fn order_bys_for(&self, state: &TableState) -> Vec<OrderByExpr>
where
M: toasty::schema::Model,
{
if let Some(sort) = &state.sort
&& let Some(col) = self
.columns
.iter()
.find(|c| c.is_sortable() && c.name() == sort.column)
&& let Some(ord) = col.order_by(sort.descending)
{
return vec![ord];
}
let out: Vec<OrderByExpr> = self.order_by(false).into_iter().collect();
if out.is_empty() {
return crate::toasty_compat::pk::pk_order_bys::<M>();
}
out
}
pub(crate) fn apply_declaration(
&self,
mut query: toasty::stmt::Query<List<M>>,
state: &TableState,
) -> toasty::stmt::Query<List<M>>
where
M: toasty::schema::Model,
{
if let Some(term) = &state.search
&& let Some(expr) = self.search_expr(term)
{
query = query.filter(expr);
}
if let Some(expr) = self.filter_expr(state) {
query = query.filter(expr);
}
for ord in self.order_bys_for(state) {
query = query.order_by(ord);
}
query
}
pub(crate) fn search_enabled(&self) -> bool
where
M: toasty::schema::Model,
{
!self.hide_search && self.columns.iter().any(|c| c.is_searchable())
}
pub(crate) fn filter_bar_enabled(&self) -> bool {
!self.hide_filter_bar && !self.filters.is_empty()
}
}
#[cfg(test)]
mod tests;