Skip to main content

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;