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;