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`] or [`Schema`] that arranges or extends what the record form derives. The app
7//! mounts the panel on its own Topcoat router with [`RouterBuilderPanelExt::panel`], which builds
8//! 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::Path,
66 };
67 pub use toasty_core::schema::app::VariantId;
68 pub use topcoat::context::Cx;
69
70 pub use crate::{
71 Lens,
72 form::{FieldError, FormField, FormScalar, RecordForm, assert_form_scalar, parse_scalar},
73 schema::{
74 ChoiceField, CustomField, EmbeddedForm, Field, FileField, IntoSchema, Options, Schema,
75 TextField,
76 embedded::{
77 Embedded, EmbeddedBuilder, embedded_keys, parse_leaf, take_leaf, take_value,
78 },
79 form_key,
80 },
81 table::{BooleanColumn, Table, TextColumn},
82 };
83}
84// The toolkit surface: Panel, Resource, Table, Schema, Notification,
85// Tenancy, CSRF, and the `Db` glue.
86pub mod auth;
87pub mod csrf;
88pub mod db;
89mod declaration;
90mod error;
91pub mod form;
92mod lens;
93mod naming;
94pub mod navigation;
95pub mod notification;
96mod page;
97pub mod panel;
98pub mod policy;
99mod query_term;
100pub mod resource;
101pub mod schema;
102pub mod table;
103pub mod tenancy;
104#[cfg(test)]
105mod test_support;
106mod toasty_compat;
107mod topcoat_compat;
108pub mod upload;
109
110pub use auth::{Auth, Authenticator, PanelUser, PasswordAuth, membership};
111pub use declaration::{DeclarationError, DeclarationErrorKind, MountError, SegmentFault, Site};
112pub use form::{
113 FieldError, FieldErrorKind, FieldErrors, FormField, FormScalar, NoForm, Posted, RecordForm,
114};
115pub use lens::Lens;
116pub use navigation::{NavTarget, NavigationItem};
117pub use notification::{Notification, NotificationStatus};
118pub use page::Page;
119pub use panel::{Brand, Panel, RouterBuilderPanelExt, can_list, url};
120pub use policy::{Ability, Allow, Deny, Policy, ReadOnly, when};
121pub use resource::{
122 Action, Committed, ForeignKey, Mutation, Relation, Resource, ResourceDef, can, scoped_query,
123 scoped_view_query, write_create, write_update,
124};
125pub use schema::{
126 ChoiceField, Control, ControlInput, CustomField, EmbeddedForm, Field, FileField, Grid, Group,
127 IntoOptions, IntoSchema, Options, Repeater, Schema, Section, Source, TextField, Toggle,
128 declare,
129};
130pub use table::{
131 BooleanColumn, Column, ColumnWidth, ComputedColumn, Cursor, DateFilter, Filter, FilterInput,
132 Includes, IntoColumns, IntoFilters, QueryFilter, SelectFilter, Sort, Table, TablePage,
133 TableState, TernaryFilter, TextColumn, contains_expr,
134};
135/// Derives `EmbeddedForm` for an embedded struct or enum.
136///
137/// Builds the schema node and converts the value through that node's keys.
138///
139/// ```rust,no_run
140/// # #[derive(Debug, Clone, toasty::Model)]
141/// # struct Post {
142/// # #[key] #[auto] id: uuid::Uuid,
143/// # publication: Publication,
144/// # }
145/// # use tablo_core::Section;
146/// #[derive(Debug, Clone, toasty::Embed, tablo_core::EmbeddedForm)]
147/// pub enum Publication {
148/// #[column(variant = 1)]
149/// Scheduled {
150/// #[shared(timestamp)]
151/// #[form(label = "Publication timestamp")]
152/// scheduled_at: String,
153/// scheduled_for: String,
154/// },
155/// #[column(variant = 2)]
156/// Published {
157/// #[shared(timestamp)]
158/// published_at: String,
159/// canonical_url: String,
160/// },
161/// }
162///
163/// // form declaration — no field bindings written by hand
164/// Section::new("Publication").schema(Publication::form(Post::fields().publication()));
165/// ```
166///
167/// # How a field is classified
168///
169/// A field marked `#[form(embed)]` is another **embedded value**, delegated to
170/// its own `EmbeddedForm`. Every other field is a **scalar**: one column, read
171/// and written through `FormScalar` (`String`, a `TypedValue` type, or an
172/// `Option` of one). A scalar of another type fails to compile at the field,
173/// naming the trait. An empty scalar is its declared `#[form(blank = ..)]`,
174/// else its `FormScalar::blank()`; with neither, the parse refuses its key.
175///
176/// # Which variant an enum reads
177///
178/// A named discriminant always wins, and an undeclared one is refused;
179/// otherwise the first variant, in declaration order, with a **payload of its
180/// own** submitted — a `#[shared(..)]` column belongs to several variants and
181/// never selects one; otherwise the first variant.
182///
183/// # Per-field attributes
184///
185/// - `#[form(embed)]` — a nested `EmbeddedForm` value.
186/// - `#[form(label = "Canonical URL")]` — the control's label (default: the field name,
187/// humanized).
188/// - `#[form(multiline = 3)]` — a `<textarea>` of 3 rows.
189/// - `#[form(blank = ..)]` — what an empty submission reads as, overriding the leaf type's own
190/// answer.
191///
192/// Anything else in `#[form(..)]` is a compile error, as are `label`, `multiline`, and `blank`
193/// on an embedded value.
194pub use tablo_macros::EmbeddedForm;
195/// Derive `Options` for a unit-variant enum: the `(value, label)` list a
196/// choice field, a select filter and a column share.
197///
198/// ```rust
199/// # use tablo_core::Options;
200/// #[derive(Debug, Clone, Copy, PartialEq, Eq, tablo_core::Options)]
201/// pub enum Status {
202/// Draft,
203/// #[option(label = "Live")]
204/// Published,
205/// }
206///
207/// assert_eq!(Status::Published.value(), "published");
208/// assert_eq!(Status::Published.label(), "Live");
209/// assert_eq!(Status::from_value("draft"), Some(Status::Draft));
210/// ```
211///
212/// Each variant stores its `snake_case` name and reads as that name in
213/// sentence case. `#[option(value = "..")]` and `#[option(label = "..")]`
214/// override either. A generic enum, a variant with fields, two variants
215/// storing one value, and an unknown key are compile errors.
216pub use tablo_macros::Options;
217/// Derive `RecordForm` for the typed value a resource's form writes.
218///
219/// One field per model column the form writes, named and typed like the
220/// model's field. A scalar (`String`, a `TypedValue` type, or an `Option` of
221/// one) binds the key its control posts; a `#[form(embed)]` field binds every
222/// key of an `EmbeddedForm` value and is written whole.
223///
224/// ```rust
225/// # #[derive(Debug, Clone, toasty::Model)]
226/// # pub struct User {
227/// # #[key] #[auto] id: uuid::Uuid,
228/// # name: String,
229/// # role: String,
230/// # age: i64,
231/// # }
232/// #[derive(tablo_core::RecordForm)]
233/// #[form(model = User)]
234/// pub struct UserForm {
235/// pub name: String,
236/// #[form(blank = "member")]
237/// pub role: String,
238/// #[form(blank = 0)]
239/// pub age: i64,
240/// }
241/// ```
242///
243/// The derive also emits `UserFormField`, one variant per field, which
244/// `Posted` keys on and `RecordForm::fields` answers with each variant's keys.
245/// It emits `UserFormControls`, one control per field chosen from the field —
246/// a `bool` is a toggle, `#[form(options = T)]` a choice over `T`'s options,
247/// `#[form(choice)]` a bare choice, `#[form(file)]` a file field,
248/// `#[form(embed)]` the embedded value's schema, and any other field a text
249/// field — with `controls()` handing them over and `RecordForm::schema`
250/// arranging one per field in declaration order. `RecordForm::table` lists a
251/// sortable column per text field, searchable over a `String` or
252/// `Option<String>`, an options field by its option's label, and a toggle as
253/// yes or no. A resource's `ResourceDef` defaults its form and table to them;
254/// `ResourceDef::form` and `ResourceDef::table` arrange or extend them instead.
255///
256/// # Attributes
257///
258/// - `#[form(model = User)]` on the struct: the model the form writes.
259/// - `#[form(blank = <expr>)]` on a scalar: the value an empty submission reads as, overriding
260/// the default (`String` answers `""` and `Option<T>` answers `None` through the type's own
261/// blank, and `bool` answers `false` through the derive's default).
262/// - `#[form(options = Status)]`: a choice over `Status::options()`.
263/// - `#[form(choice)]`: a bare choice, whose options or relationship the resource's `form` may
264/// add.
265/// - `#[form(file)]` on a `String`: a file field.
266/// - `#[form(embed)]` on an `EmbeddedForm` value.
267///
268/// A generic struct, a tuple struct, an empty struct, a `Deferred<_>` field,
269/// `blank` on an `Option` or an embedded value, and an unknown key are compile
270/// errors. So are a field the model lacks, a type the model's field does not
271/// have, and a scalar that is not a `FormScalar`.
272pub use tablo_macros::RecordForm;
273pub use tenancy::{Membership, Tenancy, Tenant, require_tenant, tenant_id};
274pub use upload::Uploader;