tablo_core/lib.rs
1//! Tablo: a server-rendered admin toolkit on [Topcoat](topcoat) and [Toasty](toasty).
2//!
3//! A [`Panel`] serves one [`Resource`] per Toasty model. A resource names the model and the
4//! [`RecordForm`](derive@RecordForm) struct its create and edit submissions parse into, and
5//! declares the rest as one [`ResourceDef`] value: its [`Policy`], which denies by default, and
6//! any [`Table`], [`Schema`] or [`Detail`] that arranges or extends what the record form derives.
7//! The app mounts the panel on its own Topcoat router with [`RouterBuilderPanelExt::panel`], which
8//! builds each def once, binds it to the database schema and checks it first; one router mounts any
9//! number of panels at distinct prefixes.
10//!
11//! ```rust,no_run
12//! # use tablo_core::{Allow, Panel, Resource, ResourceDef, RouterBuilderPanelExt};
13//! # use toasty::Db;
14//! # use topcoat::router::{Router, RouterBuilderDiscoverExt};
15//! # fn main() -> topcoat::Result<()> {
16//! # #[derive(Debug, Clone, toasty::Model)]
17//! # pub struct Book {
18//! # #[key]
19//! # #[auto]
20//! # pub id: uuid::Uuid,
21//! # pub title: String,
22//! # }
23//! #[derive(tablo_core::RecordForm)]
24//! #[form(model = Book)]
25//! pub struct BookForm {
26//! pub title: String,
27//! }
28//!
29//! pub struct BookResource;
30//!
31//! impl Resource for BookResource {
32//! type Model = Book;
33//! type Form = BookForm;
34//!
35//! fn declare() -> ResourceDef<Self> {
36//! ResourceDef::new().policy(Allow)
37//! }
38//! }
39//!
40//! # let db: Db = todo!();
41//! let router = Router::builder()
42//! .discover()
43//! .app_context(db)
44//! .panel(Panel::new("admin").resource::<BookResource>())?
45//! .build();
46//! # let _ = router;
47//! # Ok(())
48//! # }
49//! ```
50//!
51//! The [user guide](https://y-l.fr/tablo/nightly/guide/) walks through a complete panel and each
52//! part of it. [`ResourceDef`] lists everything a resource declares, [`Resource`] the hooks it
53//! overrides, and [`Panel`] every builder call.
54
55// The derives emit `tablo_core::` paths; this lets them expand inside this
56// crate's own tests too.
57extern crate self as tablo_core;
58
59#[doc(hidden)]
60pub mod __macro {
61 pub use toasty::{
62 Executor, Result as DbResult,
63 schema::{Embed, Model},
64 stmt,
65 stmt::{List, Path},
66 };
67 pub use topcoat::context::Cx;
68
69 pub use crate::{
70 Lens,
71 detail::Detail,
72 extend::{Includes, OptionSource},
73 form::{
74 FieldError, FormField, FormScalar, NullableScalar, RecordForm, assert_form_scalar,
75 links_include, links_key, parse_list, parse_password, parse_scalar, write_links,
76 },
77 resource::{ActionInput, required_input},
78 schema::{
79 ChoiceField, CustomField, EmbeddedForm, Field, FieldResolver, FileField, IntoSchema,
80 Options, RepeaterField, RepeaterItem, Schema, TextField,
81 embedded::{
82 Embedded, EmbeddedBuilder, embedded_field, embedded_form, take_leaf, take_value,
83 },
84 form_key, parse_items,
85 tree::Retype,
86 write_items,
87 },
88 table::{
89 BooleanColumn, EmbeddedColumn, FileColumn, RelationColumn, RelationLens,
90 RepeaterColumn, Table, TextColumn,
91 },
92 toasty_compat::VariantId,
93 };
94}
95// The toolkit surface: Panel, Resource, Table, Schema, Notification,
96// Tenancy, CSRF, and the `Db` glue.
97pub mod auth;
98pub mod csrf;
99pub mod db;
100mod declaration;
101pub mod detail;
102mod error;
103pub mod extend;
104pub mod form;
105mod lens;
106mod naming;
107pub mod navigation;
108pub mod notification;
109mod page;
110pub mod panel;
111pub mod policy;
112mod query_term;
113pub mod resource;
114pub mod schema;
115pub mod table;
116pub mod tenancy;
117#[cfg(test)]
118mod test_support;
119mod toasty_compat;
120mod topcoat_compat;
121pub mod upload;
122
123pub use auth::{
124 Auth, Authenticator, LoginThrottle, PanelUser, PasswordAuth, Registrar, SignUp, membership,
125};
126pub use declaration::{
127 ActionInputFault, DeclarationError, DeclarationErrorKind, ManyToManyFault, MountError,
128 SegmentFault, SignUpFault, Site,
129};
130pub use detail::{Detail, IntoDetail};
131pub use form::{
132 FieldError, FieldErrorKind, FieldErrors, FormField, FormScalar, NoForm, Posted, RecordForm,
133};
134pub use lens::Lens;
135pub use navigation::{NavTarget, NavigationItem};
136pub use notification::{Notification, NotificationStatus};
137pub use page::{Page, header_action_buttons};
138pub use panel::{Brand, Panel, PanelHandle, RouterBuilderPanelExt, can_list, url};
139pub use policy::{Ability, Allow, Deny, Policy, ReadOnly, when};
140pub use resource::{
141 Action, ActionInput, Committed, ForeignKey, HeaderAction, HeaderActions, Mutation, Places,
142 PublicLink, Relation, Resource, ResourceDef, can, scoped_query, write_create, write_update,
143};
144pub use schema::{
145 ChoiceField, CustomField, EmbeddedForm, Field, FieldResolver, FileField, Grid, Group,
146 IntoOptions, IntoSchema, Options, RepeaterField, RepeaterItem, Schema, Section, Source,
147 TextField, Toggle, Watched,
148};
149pub use table::{
150 BooleanColumn, ColumnWidth, ComputedColumn, CountColumn, Cursor, DateFilter, EmbeddedColumn,
151 FileColumn, IntoColumns, IntoFilters, QueryFilter, RelationColumn, RelationLens,
152 RepeaterColumn, SelectFilter, Sort, Table, TablePage, TableState, TernaryFilter, TextColumn,
153 ToOneRelation, WiredTable, contains_expr,
154};
155/// Derives [`ActionInput`](trait@ActionInput) for the typed value an [`Action`] asks for
156/// before it runs.
157///
158/// ```rust
159/// # #[derive(Debug, Clone, toasty::Model)]
160/// # struct Post { #[key] #[auto] id: uuid::Uuid, status: String }
161/// # use tablo_core::{Action, ActionInput, NoForm, Resource};
162/// # use topcoat::{Result, context::Cx};
163/// # struct PostResource;
164/// # impl Resource for PostResource {
165/// # type Model = Post;
166/// # type Form = NoForm<Post>;
167/// # }
168/// #[derive(tablo_core::ActionInput)]
169/// pub struct Rejection {
170/// #[form(multiline = 4)]
171/// pub reason: String,
172/// pub notify_author: bool,
173/// }
174///
175/// struct Reject;
176///
177/// impl Action<PostResource> for Reject {
178/// type Input = Rejection;
179/// const NAME: &'static str = "reject";
180///
181/// async fn run(
182/// _cx: &Cx,
183/// posts: &[Post],
184/// rejection: Rejection,
185/// ex: &mut dyn toasty::Executor,
186/// ) -> Result<()> {
187/// // Store `rejection.reason`, notify when `rejection.notify_author` holds…
188/// # let _ = (posts, rejection.reason, rejection.notify_author, ex);
189/// Ok(())
190/// }
191/// }
192/// ```
193///
194/// Each field posts its own name, which no column binds, and renders the control its type
195/// picks: a `bool` a checkbox, `#[form(options)]` a choice over the field type's
196/// [`Options`](derive@Options), `#[form(options = T)]` one over `T`'s, and any other
197/// [`FormScalar`] a text input of the type's input type.
198///
199/// A field's **blank answer** is its `#[form(blank = ..)]`, `""` for an `#[form(optional)]`
200/// `String`, `None` for an `Option`, or `false` for a `bool`. A field with none is required:
201/// its control renders required and the parse refuses an empty submission.
202///
203/// # Per-field attributes
204///
205/// - `#[form(label = "Reason")]` — the control's label (default: the field name in sentence
206/// case).
207/// - `#[form(multiline = 4)]` — a `<textarea>` of 4 rows.
208/// - `#[form(placeholder = "rust, async")]` — a text input's placeholder.
209/// - `#[form(email)]` — a text input that refuses a malformed email address.
210/// - `#[form(password)]` — a masked text input on a `String`, read untrimmed, that a refused
211/// form renders empty.
212/// - `#[form(blank = ..)]`, `#[form(optional)]` — the blank answer.
213/// - `#[form(options)]`, `#[form(options = T)]` — a choice. An `Option<T>` field names its
214/// options type: `#[form(options = T)]`.
215///
216/// A generic struct, a tuple struct, an empty struct (name `()` instead), an unknown key, a
217/// text input's key with `options` or on a `bool`, `password` with `multiline`, and a field
218/// that is not a `FormScalar` are compile errors.
219/// [`RouterBuilderPanelExt::panel`] refuses a field named `csrf_token`, `confirm` or `ids`,
220/// the keys an action's POST carries besides its input.
221///
222/// A hand-written impl builds its fields with [`Field::text_input`], [`Field::choice_input`]
223/// and [`Field::toggle_input`]; its controls render without the `required` mark, though its
224/// `parse` still refuses what it requires.
225pub use tablo_macros::ActionInput;
226/// Derives `EmbeddedForm` for an embedded struct or enum.
227///
228/// Builds the schema node and converts the value through that node's keys.
229///
230/// ```rust,no_run
231/// # #[derive(Debug, Clone, toasty::Model)]
232/// # struct Post {
233/// # #[key] #[auto] id: uuid::Uuid,
234/// # publication: Publication,
235/// # }
236/// # use tablo_core::Section;
237/// #[derive(Debug, Clone, toasty::Embed, tablo_core::EmbeddedForm)]
238/// pub enum Publication {
239/// #[column(variant = 1)]
240/// Scheduled {
241/// #[shared(timestamp)]
242/// #[form(label = "Publication timestamp")]
243/// scheduled_at: String,
244/// scheduled_for: String,
245/// },
246/// #[column(variant = 2)]
247/// Published {
248/// #[shared(timestamp)]
249/// published_at: String,
250/// canonical_url: String,
251/// },
252/// }
253///
254/// // form declaration — no field bindings written by hand
255/// Section::new("Publication").schema(Publication::form(Post::fields().publication()));
256/// ```
257///
258/// # How a field is classified
259///
260/// A field marked `#[form(embed)]` is another **embedded value**, delegated to
261/// its own `EmbeddedForm`. Every other field is a **scalar**: one column, read
262/// and written through `FormScalar` (`String`, a `TypedValue` type, an
263/// `Options` enum, or an `Option` of one). A scalar of another type fails to compile at the
264/// field, naming the trait. An empty scalar is its blank answer — its declared
265/// `#[form(blank = ..)]`, `None` for an `Option`, `false` for a `bool`, `""`
266/// for an `#[form(optional)]` `String` — and a scalar with none is required:
267/// its control renders required and the parse refuses its key.
268///
269/// # Which variant an enum reads
270///
271/// A named discriminant always wins, and an undeclared one is refused;
272/// otherwise the first variant, in declaration order, with a **payload of its
273/// own** submitted — a `#[shared(..)]` column belongs to several variants and
274/// never selects one; otherwise the first variant.
275///
276/// # Per-field attributes
277///
278/// - `#[form(embed)]` — a nested `EmbeddedForm` value.
279/// - `#[form(label = "Canonical URL")]` — the control's label (default: the field name in
280/// sentence case, `canonical_url` → "Canonical url").
281/// - `#[form(multiline = 3)]` — a `<textarea>` of 3 rows.
282/// - `#[form(blank = ..)]` — what an empty submission reads as.
283/// - `#[form(optional)]` on a `String` — an empty submission reads as `""`.
284///
285/// Anything else in `#[form(..)]` is a compile error, as are `label`, `multiline`, `blank` and
286/// `optional` on an embedded value, `blank` or `optional` on an `Option`, and `optional` on a
287/// type other than `String`.
288pub use tablo_macros::EmbeddedForm;
289/// Derive `Options` for a unit-variant enum: the `(value, label)` list a
290/// choice field, a select filter and a column share, and the `FormScalar`
291/// that posts a variant as its value and reads it as its label.
292///
293/// ```rust
294/// # use tablo_core::Options;
295/// #[derive(Debug, Clone, Copy, PartialEq, Eq, toasty::Embed, tablo_core::Options)]
296/// pub enum Status {
297/// Draft,
298/// #[option(label = "Live")]
299/// Published,
300/// }
301///
302/// assert_eq!(Status::Published.value(), "published");
303/// assert_eq!(Status::Published.label(), "Live");
304/// assert_eq!(Status::from_value("draft"), Some(Status::Draft));
305/// ```
306///
307/// Each variant's value is its `snake_case` name and its label that name in
308/// sentence case. `#[option(value = "..")]` and `#[option(label = "..")]`
309/// override either. A generic enum, a variant with fields, two variants
310/// sharing one value or one label, and an unknown key are compile errors.
311///
312/// The enum, and an `Option` of it, is a form scalar through this derive, so
313/// it cannot also implement `TypedValue` or `FormScalar` itself.
314pub use tablo_macros::Options;
315/// Derive `RecordForm` for the typed value a resource's form writes.
316///
317/// One field per model column the form writes, named and typed like the
318/// model's field. A scalar (`String`, a `TypedValue` type, an `Options` enum,
319/// or an `Option` of one) binds the key its control posts; a `#[form(embed)]` field binds
320/// every key of an `EmbeddedForm` value and is written whole.
321///
322/// ```rust
323/// # #[derive(Debug, Clone, toasty::Model)]
324/// # pub struct User {
325/// # #[key] #[auto] id: uuid::Uuid,
326/// # name: String,
327/// # role: String,
328/// # age: i64,
329/// # }
330/// #[derive(tablo_core::RecordForm)]
331/// #[form(model = User)]
332/// pub struct UserForm {
333/// pub name: String,
334/// #[form(blank = "member")]
335/// pub role: String,
336/// #[form(blank = 0)]
337/// pub age: i64,
338/// }
339/// ```
340///
341/// The derive also emits `UserFormField`, one variant per field, which
342/// `Posted` and `Resource::validate_record`'s `FieldErrors` key on and
343/// `RecordForm::fields` answers with each variant's keys.
344/// It emits `UserFormControls`, one control per field chosen from the field —
345/// a `bool` is a toggle, `#[form(options = T)]` a choice over `T`'s options,
346/// `#[form(options)]` a choice over the field type's options,
347/// `#[form(relationship = R)]` a choice over `R`'s records (a multiple choice over a `Vec` of
348/// their keys), `#[form(file)]` a file field, `#[form(embed)]` the embedded value's schema,
349/// `#[form(repeat)]` a repeater, and any
350/// other field a text field — with `controls()` handing them over, typed by the form so only a
351/// `Schema<UserForm>` places them, and `RecordForm::control` answering one
352/// field's. `RecordForm::table` lists a
353/// sortable column per text field, searchable over a `String` or
354/// `Option<String>`, an options field by its option's label, and a toggle as
355/// yes or no. `RecordForm::detail` shows the same columns, plus a relationship's
356/// key, a many-to-many field's records by their titles, a file field as a link, an embedded
357/// value leaf by leaf and a repeater's items. A
358/// resource's `ResourceDef` defaults its form, table and detail page to them;
359/// `ResourceDef::form`, `ResourceDef::table` and `ResourceDef::view` arrange or extend them
360/// instead, and a form renders the controls it does not place after the ones it does.
361///
362/// # Attributes
363///
364/// - `#[form(model = User)]` on the struct: the model the form writes.
365/// - `#[form(blank = <expr>)]` on a scalar: the value an empty submission reads as.
366/// - `#[form(optional)]` on a `String`: an empty submission reads as `""`.
367/// - `#[form(options)]` on an `Options` enum or an `Option` of one: a choice over its options.
368/// Without it, the field is a text field.
369/// - `#[form(options = Status)]`: a choice over `Status::options()`.
370/// - `#[form(relationship = AuthorResource)]` on a foreign key: a choice over the source's
371/// records, each labelled by its `record_title`. The source is any `OptionSource`, which
372/// every `Resource` is.
373/// - `#[form(relationship = TagResource)]` on a `Vec` of the source's keys, named like the
374/// model's `#[has_many(via = joins.target)]` field: a many-to-many field, a multiple choice
375/// whose records the write links through the join model, one row each. No record is its
376/// blank answer; an edit that does not post it keeps the record's links.
377/// - `#[form(file)]` on a `String`: a file field.
378/// - `#[form(embed)]` on an `EmbeddedForm` value.
379/// - `#[form(repeat)]` on a `#[document]` list of [`RepeaterItem`](derive@RepeaterItem)
380/// values: a repeater, a row per item, which the browser adds, removes and moves. No rows is
381/// its blank answer.
382///
383/// A scalar's **blank answer** is its `blank`, `""` for an `optional` `String`, `None` for an
384/// `Option`, or `false` for a `bool`. A field with none is required: the panel renders its
385/// control required and the parse refuses an empty submission. No control declares presence.
386///
387/// A generic struct, a tuple struct, an empty struct, a `Deferred<_>` field,
388/// `blank` or `optional` on an `Option`, an embedded value or a many-to-many field, `optional`
389/// on a type other than `String`, and an unknown key are compile errors. So are a field the
390/// model lacks, a type the model's field does not have, a scalar that is not a `FormScalar`,
391/// and a many-to-many field whose keys are not its source's primary key.
392pub use tablo_macros::RecordForm;
393/// Derives [`RepeaterItem`](trait@RepeaterItem) for one item of a repeater: an embedded struct
394/// a `#[document]` list stores.
395///
396/// ```rust
397/// #[derive(Debug, Clone, toasty::Embed, tablo_core::RepeaterItem)]
398/// pub struct Link {
399/// pub label: String,
400/// #[form(label = "URL")]
401/// pub url: String,
402/// }
403///
404/// #[derive(Debug, Clone, toasty::Model)]
405/// pub struct Post {
406/// #[key]
407/// #[auto]
408/// id: uuid::Uuid,
409/// #[document]
410/// #[default(Vec::<Link>::new())]
411/// links: Vec<Link>,
412/// }
413///
414/// #[derive(tablo_core::RecordForm)]
415/// #[form(model = Post)]
416/// pub struct PostForm {
417/// #[form(repeat)]
418/// pub links: Vec<Link>,
419/// }
420/// ```
421///
422/// Its fields, controls and attributes are an [`ActionInput`](derive@ActionInput)'s: each row
423/// posts them under its own prefix (`links.0.url`). An item's blank answers and required
424/// fields work the same way, and each row answers to its controls' own rules, such as a choice
425/// offering only its options. A repeater takes at most [`MAX_ROWS`](schema::MAX_ROWS) rows.
426pub use tablo_macros::RepeaterItem;
427pub use tenancy::{Membership, Tenancy, Tenant, TenantColumn, TenantId, require_tenant, tenant_id};
428pub use upload::Uploader;