Skip to main content

tablo_macros/
lib.rs

1//! The `RecordForm`, `EmbeddedForm` and `Options` derives, re-exported by
2//! `tablo-core`.
3
4mod embedded;
5mod fields;
6mod options;
7mod record_form;
8
9use proc_macro::TokenStream;
10use syn::DeriveInput;
11
12/// Derives `EmbeddedForm` for an embedded struct or enum.
13///
14/// Builds the schema node and converts the value through that node's keys.
15///
16/// ```rust,no_run
17/// # #[derive(Debug, Clone, toasty::Model)]
18/// # struct Post {
19/// #     #[key] #[auto] id: uuid::Uuid,
20/// #     publication: Publication,
21/// # }
22/// # use tablo_core::Section;
23/// #[derive(Debug, Clone, toasty::Embed, tablo_core::EmbeddedForm)]
24/// pub enum Publication {
25///     #[column(variant = 1)]
26///     Scheduled {
27///         #[shared(timestamp)]
28///         #[form(label = "Publication timestamp")]
29///         scheduled_at: String,
30///         scheduled_for: String,
31///     },
32///     #[column(variant = 2)]
33///     Published {
34///         #[shared(timestamp)]
35///         published_at: String,
36///         canonical_url: String,
37///     },
38/// }
39///
40/// // form declaration — no field bindings written by hand
41/// Section::new("Publication").schema(Publication::form(Post::fields().publication()));
42/// ```
43///
44/// # How a field is classified
45///
46/// A field marked `#[form(embed)]` is another **embedded value**, delegated to
47/// its own `EmbeddedForm`. Every other field is a **scalar**: one column, read
48/// and written through `FormScalar` (`String`, a `TypedValue` type, or an
49/// `Option` of one). A scalar of another type fails to compile at the field,
50/// naming the trait. An empty scalar is its declared `#[form(blank = ..)]`,
51/// else its `FormScalar::blank()`; with neither, the parse refuses its key.
52///
53/// # Which variant an enum reads
54///
55/// A named discriminant always wins, and an undeclared one is refused;
56/// otherwise the first variant, in declaration order, with a **payload of its
57/// own** submitted — a `#[shared(..)]` column belongs to several variants and
58/// never selects one; otherwise the first variant.
59///
60/// # Per-field attributes
61///
62/// - `#[form(embed)]` — a nested `EmbeddedForm` value.
63/// - `#[form(label = "Canonical URL")]` — the control's label (default: the field name, humanized).
64/// - `#[form(multiline = 3)]` — a `<textarea>` of 3 rows.
65/// - `#[form(blank = ..)]` — what an empty submission reads as, overriding the leaf type's own
66///   answer.
67///
68/// Anything else in `#[form(..)]` is a compile error, as are `label`, `multiline`, and `blank` on
69/// an embedded value.
70#[proc_macro_derive(EmbeddedForm, attributes(form))]
71pub fn embedded_form(input: TokenStream) -> TokenStream {
72    let input = syn::parse_macro_input!(input as DeriveInput);
73    embedded::expand(input)
74}
75
76/// Derive `RecordForm` for the typed value a resource's form writes.
77///
78/// One field per model column the form writes, named and typed like the
79/// model's field. A scalar (`String`, a `TypedValue` type, or an `Option` of
80/// one) binds the key its control posts; a `#[form(embed)]` field binds every
81/// key of an `EmbeddedForm` value and is written whole.
82///
83/// ```rust
84/// # #[derive(Debug, Clone, toasty::Model)]
85/// # pub struct User {
86/// #     #[key] #[auto] id: uuid::Uuid,
87/// #     name: String,
88/// #     role: String,
89/// #     age: i64,
90/// # }
91/// #[derive(tablo_core::RecordForm)]
92/// #[form(model = User)]
93/// pub struct UserForm {
94///     pub name: String,
95///     #[form(blank = "member")]
96///     pub role: String,
97///     #[form(blank = 0)]
98///     pub age: i64,
99/// }
100/// ```
101///
102/// The derive also emits `UserFormField`, one variant per field, which
103/// `Posted` keys on and `RecordForm::fields` answers with each variant's keys.
104/// It emits `UserFormControls`, one control per field chosen from the field —
105/// a `bool` is a toggle, `#[form(options = T)]` a choice over `T`'s options,
106/// `#[form(choice)]` a bare choice, `#[form(file)]` a file field,
107/// `#[form(embed)]` the embedded value's schema, and any other field a text
108/// field — with `controls()` handing them over and `RecordForm::schema`
109/// arranging one per field in declaration order. `RecordForm::table` lists a
110/// sortable column per text field, searchable over a `String` or
111/// `Option<String>`, an options field by its option's label, and a toggle as
112/// yes or no. A resource's `ResourceDef` defaults its form and table to them;
113/// `ResourceDef::form` and `ResourceDef::table` arrange or extend them instead.
114///
115/// # Attributes
116///
117/// - `#[form(model = User)]` on the struct: the model the form writes.
118/// - `#[form(blank = <expr>)]` on a scalar: the value an empty submission reads as, overriding the
119///   default (`String` answers `""` and `Option<T>` answers `None` through the type's own blank,
120///   and `bool` answers `false` through the derive's default).
121/// - `#[form(options = Status)]`: a choice over `Status::options()`.
122/// - `#[form(choice)]`: a bare choice, whose options or relationship the resource's `form` may add.
123/// - `#[form(file)]` on a `String`: a file field.
124/// - `#[form(embed)]` on an `EmbeddedForm` value.
125///
126/// A generic struct, a tuple struct, an empty struct, a `Deferred<_>` field,
127/// `blank` on an `Option` or an embedded value, and an unknown key are compile
128/// errors. So are a field the model lacks, a type the model's field does not
129/// have, and a scalar that is not a `FormScalar`.
130#[proc_macro_derive(RecordForm, attributes(form))]
131pub fn record_form(input: TokenStream) -> TokenStream {
132    let input = syn::parse_macro_input!(input as DeriveInput);
133    record_form::expand_tokens(input).into()
134}
135
136/// Derive `Options` for a unit-variant enum: the `(value, label)` list a
137/// choice field, a select filter and a column share.
138///
139/// ```rust
140/// # use tablo_core::Options;
141/// #[derive(Debug, Clone, Copy, PartialEq, Eq, tablo_core::Options)]
142/// pub enum Status {
143///     Draft,
144///     #[option(label = "Live")]
145///     Published,
146/// }
147///
148/// assert_eq!(Status::Published.value(), "published");
149/// assert_eq!(Status::Published.label(), "Live");
150/// assert_eq!(Status::from_value("draft"), Some(Status::Draft));
151/// ```
152///
153/// Each variant stores its `snake_case` name and reads as that name in
154/// sentence case. `#[option(value = "..")]` and `#[option(label = "..")]`
155/// override either. A generic enum, a variant with fields, two variants
156/// storing one value, and an unknown key are compile errors.
157#[proc_macro_derive(Options, attributes(option))]
158pub fn options(input: TokenStream) -> TokenStream {
159    let input = syn::parse_macro_input!(input as DeriveInput);
160    options::expand_tokens(input).into()
161}
162
163/// The path the generated code names `tablo-core` by: the `tablo` facade, which
164/// re-exports it at its root, else `::tablo_core`, either under the consumer's
165/// rename. The facade comes first: it is what an app depends on, and it
166/// reaches every path the generated code names.
167fn tablo_core_path(span: &syn::Ident, derive: &str) -> syn::Result<proc_macro2::TokenStream> {
168    let found = proc_macro_crate::crate_name("tablo")
169        .map(|found| (found, "tablo"))
170        .or_else(|_| proc_macro_crate::crate_name("tablo-core").map(|found| (found, "tablo_core")));
171    match found {
172        Ok((found, own_name)) => {
173            let name = match found {
174                proc_macro_crate::FoundCrate::Itself => own_name.to_string(),
175                proc_macro_crate::FoundCrate::Name(n) => n,
176            };
177            let ident = syn::Ident::new(&name.replace('-', "_"), proc_macro2::Span::call_site());
178            Ok(quote::quote! { ::#ident })
179        }
180        Err(_) => Err(syn::Error::new_spanned(
181            span,
182            format!("`tablo` (or `tablo-core`) must be a dependency to #[derive({derive})]"),
183        )),
184    }
185}