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 and turns the detail page off, for a
14/// resource that declares both.
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 is off here.
35    fn detail() -> Detail<Self> {
36        Detail::empty()
37    }
38
39    /// The record's `#[version]` counter, or `None` for a model without one.
40    ///
41    /// The edit form carries it, so a save refuses a record another save changed since the form
42    /// opened. The derive reads the model's `#[version]` field; an impl written by hand keeps
43    /// this default unless it returns the counter itself.
44    fn version(&self) -> Option<u64> {
45        None
46    }
47}
48
49/// How the [`ModelColumns`](derive@crate::ModelColumns) derive picks a field's column from its
50/// type, a choice no trait bound can make: each `Pick*` trait is implemented on
51/// [`ModelField`](pick::ModelField) behind one more reference than the next, so method lookup on
52/// `&&&&&&ModelField` takes the first whose bounds the field's type meets.
53#[doc(hidden)]
54pub mod pick {
55    use toasty::{
56        schema::{Field, Model},
57        stmt::List,
58    };
59
60    use crate::{
61        BooleanColumn, DateColumn, DateValue, Detail, EmbeddedColumn, Lens, RepeaterColumn, Table,
62        TextColumn,
63        schema::{EmbeddedForm, FormInput},
64        values::FormScalar,
65    };
66
67    /// One model field, as the derive hands it to the `Pick*` traits.
68    pub struct ModelField<M, T, P = T>(pub Lens<M, T, P>);
69
70    /// The type a field of type `T` names its path by: `T`, except `List<T>` for a `Vec<T>`.
71    pub type PathOf<T> = <T as Field>::ExprTarget;
72
73    /// A [`DateValue`]: a sortable calendar day in the table, the stored value on the detail page,
74    /// which keeps a timestamp's time.
75    pub trait PickDate<M> {
76        fn table(&self, table: Table<M>) -> Table<M>;
77        fn detail(&self, detail: Detail<M>) -> Detail<M>;
78    }
79
80    impl<M, T> PickDate<M> for &&&&&&ModelField<M, T>
81    where
82        M: Model + Send + Sync + 'static,
83        T: DateValue + FormScalar,
84    {
85        fn table(&self, table: Table<M>) -> Table<M> {
86            table.column(DateColumn::new(self.0.clone()).sortable())
87        }
88
89        fn detail(&self, detail: Detail<M>) -> Detail<M> {
90            detail.column(TextColumn::new(self.0.clone()))
91        }
92    }
93
94    /// A `bool`: a sortable yes or no.
95    pub trait PickToggle<M> {
96        fn table(&self, table: Table<M>) -> Table<M>;
97        fn detail(&self, detail: Detail<M>) -> Detail<M>;
98    }
99
100    impl<M> PickToggle<M> for &&&&&ModelField<M, bool>
101    where
102        M: Model + Send + Sync + 'static,
103    {
104        fn table(&self, table: Table<M>) -> Table<M> {
105            table.column(BooleanColumn::new(self.0.clone()).sortable())
106        }
107
108        fn detail(&self, detail: Detail<M>) -> Detail<M> {
109            detail.column(BooleanColumn::new(self.0.clone()))
110        }
111    }
112
113    /// A `String` or `Option<String>`: a sortable, searchable text.
114    pub trait PickString<M> {
115        fn table(&self, table: Table<M>) -> Table<M>;
116        fn detail(&self, detail: Detail<M>) -> Detail<M>;
117    }
118
119    impl<M, T> PickString<M> for &&&&ModelField<M, T>
120    where
121        M: Model + Send + Sync + 'static,
122        T: FormScalar + Field<Inner = String> + Send + Sync + 'static,
123    {
124        fn table(&self, table: Table<M>) -> Table<M> {
125            table.column(TextColumn::new(self.0.clone()).sortable().searchable())
126        }
127
128        fn detail(&self, detail: Detail<M>) -> Detail<M> {
129            detail.column(TextColumn::new(self.0.clone()))
130        }
131    }
132
133    /// Any other [`FormScalar`]: a sortable text, an `Options` enum's by its label.
134    pub trait PickScalar<M> {
135        fn table(&self, table: Table<M>) -> Table<M>;
136        fn detail(&self, detail: Detail<M>) -> Detail<M>;
137    }
138
139    impl<M, T> PickScalar<M> for &&&ModelField<M, T>
140    where
141        M: Model + Send + Sync + 'static,
142        T: FormScalar + Send + Sync + 'static,
143    {
144        fn table(&self, table: Table<M>) -> Table<M> {
145            table.column(TextColumn::new(self.0.clone()).sortable())
146        }
147
148        fn detail(&self, detail: Detail<M>) -> Detail<M> {
149            detail.column(TextColumn::new(self.0.clone()))
150        }
151    }
152
153    /// An [`EmbeddedForm`] value: its leaves on the detail page, no table column.
154    pub trait PickEmbedded<M> {
155        fn table(&self, table: Table<M>) -> Table<M>;
156        fn detail(&self, detail: Detail<M>) -> Detail<M>;
157    }
158
159    impl<M, T> PickEmbedded<M> for &&ModelField<M, T>
160    where
161        M: Model + Send + Sync + 'static,
162        T: EmbeddedForm + Send + Sync + 'static,
163    {
164        fn table(&self, table: Table<M>) -> Table<M> {
165            table
166        }
167
168        fn detail(&self, detail: Detail<M>) -> Detail<M> {
169            detail.column(EmbeddedColumn::new(self.0.clone()))
170        }
171    }
172
173    /// A list of [`FormInput`] values: its items on the detail page, no table column.
174    pub trait PickRepeater<M> {
175        fn table(&self, table: Table<M>) -> Table<M>;
176        fn detail(&self, detail: Detail<M>) -> Detail<M>;
177    }
178
179    impl<M, T> PickRepeater<M> for &ModelField<M, Vec<T>, List<T>>
180    where
181        M: Model + Send + Sync + 'static,
182        T: FormInput,
183    {
184        fn table(&self, table: Table<M>) -> Table<M> {
185            table
186        }
187
188        fn detail(&self, detail: Detail<M>) -> Detail<M> {
189            detail.column(RepeaterColumn::new(self.0.clone()))
190        }
191    }
192
193    /// Any other type, which no column reads: nowhere.
194    pub trait PickNone<M> {
195        fn table(&self, table: Table<M>) -> Table<M>;
196        fn detail(&self, detail: Detail<M>) -> Detail<M>;
197    }
198
199    impl<M, T, P> PickNone<M> for ModelField<M, T, P> {
200        fn table(&self, table: Table<M>) -> Table<M> {
201            table
202        }
203
204        fn detail(&self, detail: Detail<M>) -> Detail<M> {
205            detail
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests;