Skip to main content

tablo_core/table/
model.rs

1//! [`ModelColumns`]: the table and detail page a model reads as by default.
2
3use toasty::{schema::Model, stmt::IntoExpr};
4
5use crate::{Detail, Table};
6
7/// The table and detail page a model reads as when its resource declares none: what a
8/// [`ResourceDef`](crate::ResourceDef) without a [`table`](crate::ResourceDef::table) lists and
9/// without a [`detail`](crate::ResourceDef::detail) shows.
10///
11/// Derive it with [`ModelColumns`](derive@crate::ModelColumns), which gives a column to each field
12/// a person reads, whether a form or Toasty writes it. An impl by hand names the model's tenant
13/// columns; one that keeps the defaults here lists no column, for a resource that declares its own
14/// table and detail page, or turns the view page off.
15#[diagnostic::on_unimplemented(
16    message = "`{Self}` declares no default columns",
17    label = "a resource's model implements `ModelColumns`",
18    note = "add `#[derive(tablo::ModelColumns)]` to `{Self}`, or `impl tablo::ModelColumns for \
19            {Self} {{ const TENANT_COLUMNS: &[&str] = &[..]; }}`, naming its `TenantId` fields, \
20            when its resources declare their own table and detail page"
21)]
22pub trait ModelColumns: Model + IntoExpr<Self> + Sized {
23    /// The model's [`TenantId`](crate::TenantId) fields, by name: the derive lists each field
24    /// typed `TenantId` or `Option<TenantId>`. Mounting a panel refuses a resource over a model
25    /// that lists one and declares no [`tenancy`](crate::ResourceDef::tenancy), which would serve
26    /// every tenant's rows. It has no default, so an impl by hand states it.
27    const TENANT_COLUMNS: &'static [&'static str];
28
29    /// The default table, which lists no column here.
30    fn table() -> Table<Self> {
31        Table::new(())
32    }
33
34    /// The default detail page, which lists no column here: a resource serving the view page then
35    /// declares its own, else mounting refuses it.
36    fn detail() -> Detail<Self> {
37        Detail::empty()
38    }
39
40    /// The record's `#[version]` counter, or `None` for a model without one.
41    ///
42    /// The edit form carries it, so a save refuses a record another save changed since the form
43    /// opened. The derive reads the model's `#[version]` field; an impl written by hand keeps
44    /// this default unless it returns the counter itself.
45    fn version(&self) -> Option<u64> {
46        None
47    }
48
49    /// Whether [`bare`](Self::bare) leaves out every relation the model holds. The derive
50    /// answers `true` when each relation is a `Deferred` one; an impl by hand keeps this default
51    /// unless its `bare` leaves them out.
52    ///
53    /// A panel then hands a closure over the row that declares it reads none, such as a
54    /// [`row_color`](crate::Table::row_color), a copy of the row without them, so the closure
55    /// mounts beside a column loading a record of a model a resource restricts a field of. Such a
56    /// closure reaching a relation anyway panics at render, where it read the relation another
57    /// column loaded before: declare the relations it reads.
58    ///
59    /// An eager relation, typed without `Deferred`, holds its records on every query, so a model
60    /// with one answers `false` and keeps the refusal.
61    const UNLOADS_RELATIONS: bool = false;
62
63    /// A copy of the record without its relations, holding its own fields: the derive builds it
64    /// field by field, leaving each `Deferred` relation unloaded rather than copying its records.
65    #[must_use]
66    fn bare(&self) -> Self
67    where
68        Self: Clone,
69    {
70        self.clone()
71    }
72
73    /// The record's foreign keys and [`TenantId`](crate::TenantId) fields, by name, each spelled
74    /// as the database stores it, `None` where it holds nothing.
75    ///
76    /// The default table leaves them out, so a [`ChangedField`](crate::ChangedField) reads them
77    /// here: moving a record to another owner or tenant is a change a write names. The derive
78    /// lists them; an impl by hand keeps this default, which lists none.
79    fn references(&self) -> Vec<(&'static str, Option<String>)> {
80        Vec::new()
81    }
82}
83
84/// A foreign key or tenant field's type, spelled for [`ModelColumns::references`].
85#[doc(hidden)]
86pub trait Reference {
87    /// The value as the database stores it, `None` for an empty optional one.
88    fn spell(&self) -> Option<String>;
89}
90
91/// Spells each key type a foreign key or tenant field holds, and an `Option` of one.
92macro_rules! reference {
93    ($($ty:ty),* $(,)?) => {
94        $(
95            impl Reference for $ty {
96                fn spell(&self) -> Option<String> {
97                    Some(self.to_string())
98                }
99            }
100
101            impl Reference for Option<$ty> {
102                fn spell(&self) -> Option<String> {
103                    self.as_ref().map(ToString::to_string)
104                }
105            }
106        )*
107    };
108}
109
110reference!(
111    uuid::Uuid,
112    crate::TenantId,
113    String,
114    i8,
115    i16,
116    i32,
117    i64,
118    isize,
119    u8,
120    u16,
121    u32,
122    u64,
123    usize,
124);
125
126/// A copy of `row` without its relations: see [`ModelColumns::UNLOADS_RELATIONS`].
127pub(crate) fn bare<M: ModelColumns + Clone>(row: &M) -> M {
128    row.bare()
129}
130
131#[doc(hidden)]
132pub mod pick;
133
134#[cfg(test)]
135mod tests;