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