tablo-core 0.2.0

The core toolkit types for Tablo, a server-rendered admin toolkit on Topcoat and Toasty.
Documentation
//! Tablo: a server-rendered admin toolkit on [Topcoat](topcoat) and [Toasty](toasty).
//!
//! A [`Panel`] serves one [`Resource`] per Toasty model. A resource names the model and the
//! [`RecordForm`](derive@RecordForm) struct its create and edit submissions parse into, and
//! declares the rest as one [`ResourceDef`] value: its [`Policy`], which denies by default, and
//! any [`Table`] or [`Schema`] that arranges or extends what the record form derives. The app
//! mounts the panel on its own Topcoat router with [`RouterBuilderPanelExt::panel`], which builds
//! each def once, binds it to the database schema and checks it first; one router mounts any
//! number of panels at distinct prefixes.
//!
//! ```rust,no_run
//! # use tablo_core::{Allow, Panel, Resource, ResourceDef, RouterBuilderPanelExt};
//! # use toasty::Db;
//! # use topcoat::router::{Router, RouterBuilderDiscoverExt};
//! # fn main() -> topcoat::Result<()> {
//! # #[derive(Debug, Clone, toasty::Model)]
//! # pub struct Book {
//! #     #[key]
//! #     #[auto]
//! #     pub id: uuid::Uuid,
//! #     pub title: String,
//! # }
//! #[derive(tablo_core::RecordForm)]
//! #[form(model = Book)]
//! pub struct BookForm {
//!     pub title: String,
//! }
//!
//! pub struct BookResource;
//!
//! impl Resource for BookResource {
//!     type Model = Book;
//!     type Form = BookForm;
//!
//!     fn declare() -> ResourceDef<Self> {
//!         ResourceDef::new().policy(Allow)
//!     }
//! }
//!
//! # let db: Db = todo!();
//! let router = Router::builder()
//!     .discover()
//!     .app_context(db)
//!     .panel(Panel::new("admin").resource::<BookResource>())?
//!     .build();
//! # let _ = router;
//! # Ok(())
//! # }
//! ```
//!
//! The [user guide](https://y-l.fr/tablo/nightly/guide/) walks through a complete panel and each
//! part of it. [`ResourceDef`] lists everything a resource declares, [`Resource`] the hooks it
//! overrides, and [`Panel`] every builder call.

// The derives emit `tablo_core::` paths; this lets them expand inside this
// crate's own tests too.
extern crate self as tablo_core;

#[doc(hidden)]
pub mod __macro {
    pub use toasty::{
        Executor, Result as DbResult,
        schema::{Embed, Model},
        stmt,
        stmt::Path,
    };
    pub use toasty_core::schema::app::VariantId;
    pub use topcoat::context::Cx;

    pub use crate::{
        Lens,
        form::{FieldError, FormField, FormScalar, RecordForm, assert_form_scalar, parse_scalar},
        schema::{
            ChoiceField, CustomField, EmbeddedForm, Field, FileField, IntoSchema, Options, Schema,
            TextField,
            embedded::{
                Embedded, EmbeddedBuilder, embedded_keys, parse_leaf, take_leaf, take_value,
            },
            form_key,
        },
        table::{BooleanColumn, Table, TextColumn},
    };
}
// The toolkit surface: Panel, Resource, Table, Schema, Notification,
// Tenancy, CSRF, and the `Db` glue.
pub mod auth;
pub mod csrf;
pub mod db;
mod declaration;
mod error;
pub mod form;
mod lens;
mod naming;
pub mod navigation;
pub mod notification;
mod page;
pub mod panel;
pub mod policy;
mod query_term;
pub mod resource;
pub mod schema;
pub mod table;
pub mod tenancy;
#[cfg(test)]
mod test_support;
mod toasty_compat;
mod topcoat_compat;
pub mod upload;

pub use auth::{Auth, Authenticator, PanelUser, PasswordAuth, membership};
pub use declaration::{DeclarationError, DeclarationErrorKind, MountError, SegmentFault, Site};
pub use form::{
    FieldError, FieldErrorKind, FieldErrors, FormField, FormScalar, NoForm, Posted, RecordForm,
};
pub use lens::Lens;
pub use navigation::{NavTarget, NavigationItem};
pub use notification::{Notification, NotificationStatus};
pub use page::Page;
pub use panel::{Brand, Panel, RouterBuilderPanelExt, can_list, url};
pub use policy::{Ability, Allow, Deny, Policy, ReadOnly, when};
pub use resource::{
    Action, Committed, ForeignKey, Mutation, Relation, Resource, ResourceDef, can, scoped_query,
    scoped_view_query, write_create, write_update,
};
pub use schema::{
    ChoiceField, Control, ControlInput, CustomField, EmbeddedForm, Field, FileField, Grid, Group,
    IntoOptions, IntoSchema, Options, Repeater, Schema, Section, Source, TextField, Toggle,
    declare,
};
pub use table::{
    BooleanColumn, Column, ColumnWidth, ComputedColumn, Cursor, DateFilter, Filter, FilterInput,
    Includes, IntoColumns, IntoFilters, QueryFilter, SelectFilter, Sort, Table, TablePage,
    TableState, TernaryFilter, TextColumn, contains_expr,
};
/// Derives `EmbeddedForm` for an embedded struct or enum.
///
/// Builds the schema node and converts the value through that node's keys.
///
/// ```rust,no_run
/// # #[derive(Debug, Clone, toasty::Model)]
/// # struct Post {
/// #     #[key] #[auto] id: uuid::Uuid,
/// #     publication: Publication,
/// # }
/// # use tablo_core::Section;
/// #[derive(Debug, Clone, toasty::Embed, tablo_core::EmbeddedForm)]
/// pub enum Publication {
///     #[column(variant = 1)]
///     Scheduled {
///         #[shared(timestamp)]
///         #[form(label = "Publication timestamp")]
///         scheduled_at: String,
///         scheduled_for: String,
///     },
///     #[column(variant = 2)]
///     Published {
///         #[shared(timestamp)]
///         published_at: String,
///         canonical_url: String,
///     },
/// }
///
/// // form declaration — no field bindings written by hand
/// Section::new("Publication").schema(Publication::form(Post::fields().publication()));
/// ```
///
/// # How a field is classified
///
/// A field marked `#[form(embed)]` is another **embedded value**, delegated to
/// its own `EmbeddedForm`. Every other field is a **scalar**: one column, read
/// and written through `FormScalar` (`String`, a `TypedValue` type, or an
/// `Option` of one). A scalar of another type fails to compile at the field,
/// naming the trait. An empty scalar is its declared `#[form(blank = ..)]`,
/// else its `FormScalar::blank()`; with neither, the parse refuses its key.
///
/// # Which variant an enum reads
///
/// A named discriminant always wins, and an undeclared one is refused;
/// otherwise the first variant, in declaration order, with a **payload of its
/// own** submitted — a `#[shared(..)]` column belongs to several variants and
/// never selects one; otherwise the first variant.
///
/// # Per-field attributes
///
/// - `#[form(embed)]` — a nested `EmbeddedForm` value.
/// - `#[form(label = "Canonical URL")]` — the control's label (default: the field name,
///   humanized).
/// - `#[form(multiline = 3)]` — a `<textarea>` of 3 rows.
/// - `#[form(blank = ..)]` — what an empty submission reads as, overriding the leaf type's own
///   answer.
///
/// Anything else in `#[form(..)]` is a compile error, as are `label`, `multiline`, and `blank`
/// on an embedded value.
pub use tablo_macros::EmbeddedForm;
/// Derive `Options` for a unit-variant enum: the `(value, label)` list a
/// choice field, a select filter and a column share.
///
/// ```rust
/// # use tablo_core::Options;
/// #[derive(Debug, Clone, Copy, PartialEq, Eq, tablo_core::Options)]
/// pub enum Status {
///     Draft,
///     #[option(label = "Live")]
///     Published,
/// }
///
/// assert_eq!(Status::Published.value(), "published");
/// assert_eq!(Status::Published.label(), "Live");
/// assert_eq!(Status::from_value("draft"), Some(Status::Draft));
/// ```
///
/// Each variant stores its `snake_case` name and reads as that name in
/// sentence case. `#[option(value = "..")]` and `#[option(label = "..")]`
/// override either. A generic enum, a variant with fields, two variants
/// storing one value, and an unknown key are compile errors.
pub use tablo_macros::Options;
/// Derive `RecordForm` for the typed value a resource's form writes.
///
/// One field per model column the form writes, named and typed like the
/// model's field. A scalar (`String`, a `TypedValue` type, or an `Option` of
/// one) binds the key its control posts; a `#[form(embed)]` field binds every
/// key of an `EmbeddedForm` value and is written whole.
///
/// ```rust
/// # #[derive(Debug, Clone, toasty::Model)]
/// # pub struct User {
/// #     #[key] #[auto] id: uuid::Uuid,
/// #     name: String,
/// #     role: String,
/// #     age: i64,
/// # }
/// #[derive(tablo_core::RecordForm)]
/// #[form(model = User)]
/// pub struct UserForm {
///     pub name: String,
///     #[form(blank = "member")]
///     pub role: String,
///     #[form(blank = 0)]
///     pub age: i64,
/// }
/// ```
///
/// The derive also emits `UserFormField`, one variant per field, which
/// `Posted` keys on and `RecordForm::fields` answers with each variant's keys.
/// It emits `UserFormControls`, one control per field chosen from the field —
/// a `bool` is a toggle, `#[form(options = T)]` a choice over `T`'s options,
/// `#[form(choice)]` a bare choice, `#[form(file)]` a file field,
/// `#[form(embed)]` the embedded value's schema, and any other field a text
/// field — with `controls()` handing them over and `RecordForm::schema`
/// arranging one per field in declaration order. `RecordForm::table` lists a
/// sortable column per text field, searchable over a `String` or
/// `Option<String>`, an options field by its option's label, and a toggle as
/// yes or no. A resource's `ResourceDef` defaults its form and table to them;
/// `ResourceDef::form` and `ResourceDef::table` arrange or extend them instead.
///
/// # Attributes
///
/// - `#[form(model = User)]` on the struct: the model the form writes.
/// - `#[form(blank = <expr>)]` on a scalar: the value an empty submission reads as, overriding
///   the default (`String` answers `""` and `Option<T>` answers `None` through the type's own
///   blank, and `bool` answers `false` through the derive's default).
/// - `#[form(options = Status)]`: a choice over `Status::options()`.
/// - `#[form(choice)]`: a bare choice, whose options or relationship the resource's `form` may
///   add.
/// - `#[form(file)]` on a `String`: a file field.
/// - `#[form(embed)]` on an `EmbeddedForm` value.
///
/// A generic struct, a tuple struct, an empty struct, a `Deferred<_>` field,
/// `blank` on an `Option` or an embedded value, and an unknown key are compile
/// errors. So are a field the model lacks, a type the model's field does not
/// have, and a scalar that is not a `FormScalar`.
pub use tablo_macros::RecordForm;
pub use tenancy::{Membership, Tenancy, Tenant, require_tenant, tenant_id};
pub use upload::Uploader;