Skip to main content

tablo_core/
form.rs

1//! Record forms: the typed value a form submission parses into, and the write
2//! that stores it.
3//!
4//! Declares one `RecordForm` struct with one field per written model column.
5//! The struct is the form's one source of truth: it parses every key, and a
6//! field's blank answer decides whether its control is required. Completes
7//! unposted keys from the stored record and writes only named fields plus model
8//! defaults.
9//!
10//! ```no_run
11//! #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
12//! struct User {
13//!     #[key]
14//!     #[auto]
15//!     id: uuid::Uuid,
16//!     name: String,
17//!     age: i64,
18//! }
19//!
20//! #[derive(tablo_core::RecordForm)]
21//! #[form(model = User)]
22//! struct UserForm {
23//!     name: String,
24//!     #[form(blank = 0)]
25//!     age: i64,
26//! }
27//!
28//! // A column type toasty stores but the form edge cannot spell.
29//! #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
30//! struct Tagged {
31//!     #[key]
32//!     #[auto]
33//!     id: uuid::Uuid,
34//!     tags: Vec<String>,
35//! }
36//! ```
37//!
38//! # Compile-time refusals
39//!
40//! The derive refuses, at compile time:
41//!
42//! - a field the model does not have;
43//! - a type the model's field does not have, such as `i32` for an `i64` column;
44//! - a bare choice, which would offer nothing: a foreign key names its source with
45//!   `#[form(relationship = ..)]`;
46//! - `relationship` and `file` on one field, which each pick the control;
47//! - a scalar type the form edge cannot spell, such as `Vec<String>`;
48//! - a many-to-many field whose keys are not its source's primary key;
49//! - `optional` on a type with no empty value;
50//! - `embed` on a type that is not an [`EmbeddedForm`](crate::EmbeddedForm).
51
52use std::{collections::HashSet, fmt::Debug, hash::Hash};
53
54use toasty::{Executor, schema::Model};
55use topcoat::context::Cx;
56
57#[doc(hidden)]
58pub use crate::schema::input::required_input;
59use crate::{
60    column::Includes,
61    schema::{OptionSource, Schema},
62};
63// A record form's values, scalars and errors sit below `schema`, which reads and renders them,
64// and a form input is a schema's parse; both are re-exported here, where a record form names
65// them.
66pub use crate::{
67    schema::input::FormInput,
68    values::{
69        FieldError, FieldErrors, FormField, FormScalar, FormValue, FormValues, NullableScalar,
70        assert_form_scalar, parse_list, parse_password, parse_scalar,
71    },
72};
73
74impl<F: RecordForm> crate::schema::EditsRecords for F {}
75
76/// The hidden control the edit form carries the record's [`ModelColumns::version`] in.
77///
78/// [`ModelColumns::version`]: crate::ModelColumns::version
79pub(crate) const VERSION_FIELD: &str = "record_version";
80
81/// The name of the create form's second submit button, which saves and lands on a fresh create
82/// form rather than the list.
83pub(crate) const ANOTHER_FIELD: &str = "create_another";
84
85/// The form key of the many-to-many field `path`, a `#[has_many(via = ..)]` list of `R`'s
86/// records, holding their keys of type `K`: the derive calls it, so a field whose type is not
87/// `Vec<K>` of the related model's key, or whose source is not that model's, fails to compile.
88#[doc(hidden)]
89pub fn links_key<M, R, K>(
90    path: impl Into<toasty::stmt::Path<M, toasty::stmt::List<R::Model>>>,
91) -> String
92where
93    M: Model,
94    R: OptionSource,
95    R::Model: Model<PrimaryKey = K>,
96    K: FormScalar,
97{
98    crate::schema::form_key(path.into())
99}
100
101/// `includes` and the include loading the many-to-many field `path`, which
102/// [`RecordForm::hydrate`] reads.
103#[doc(hidden)]
104pub fn links_include<M, R>(
105    includes: Includes<M>,
106    path: impl Into<toasty::stmt::Path<M, toasty::stmt::List<R::Model>>>,
107) -> Includes<M>
108where
109    M: Model,
110    R: OptionSource,
111{
112    includes.with(path.into())
113}
114
115/// The keys of the records a many-to-many field holds, as its form key carries them, or `None`
116/// when the record was loaded without them.
117#[doc(hidden)]
118pub fn write_links<T>(records: &toasty::Deferred<Vec<T>>) -> Option<FormValue>
119where
120    T: Model + toasty::stmt::IntoExpr<T>,
121{
122    (!records.is_unloaded()).then(|| {
123        FormValue::List(
124            records
125                .get()
126                .iter()
127                .map(crate::toasty_compat::pk::pk_text)
128                .collect(),
129        )
130    })
131}
132
133/// The typed value a resource's form submission parses into.
134///
135/// Derive it from a model with [`tablo_core::ModelForm`](crate::ModelForm) or from a struct with
136/// [`tablo_core::RecordForm`](crate::RecordForm); a hand-written impl is what the derives expand
137/// to.
138pub trait RecordForm: Sized + Send + 'static {
139    /// The model the form writes.
140    type Model: Model + toasty::stmt::IntoExpr<Self::Model> + Send + Sync + 'static;
141
142    /// One variant per form field: the key [`Posted`] uses. Its bound form keys
143    /// are [`Self::fields`]'.
144    type Field: Copy + Eq + Hash + Debug + Send + Sync + 'static;
145
146    /// Every field, in declaration order, with the keys it binds.
147    fn fields() -> Vec<FormField<Self::Field>>;
148
149    /// The control the form renders `field` with when its [`form`](crate::ResourceDef::form)
150    /// does not place it: what `controls()` hands over for the field, as a one-control schema.
151    ///
152    /// A resource's form renders every field it does not place after the ones it does, in
153    /// declaration order, so a resource that declares no form renders one control per field.
154    fn control(field: Self::Field) -> Schema<Self>;
155
156    /// The relations [`hydrate`](Self::hydrate) reads off the record: each many-to-many field's,
157    /// which the edit page and the edit's write load the record with.
158    fn includes() -> Includes<Self::Model> {
159        Includes::new()
160    }
161
162    /// The stored record as the form spells it.
163    fn hydrate(cx: &Cx, record: &Self::Model) -> FormValues;
164
165    /// The records each many-to-many field holds, as their keys' form spelling: what
166    /// [`write_create`](crate::resource::write_create) links a new record to, and what
167    /// [`write_update`](crate::resource::write_update) leaves a posted field's record linked to.
168    fn links(&self) -> Vec<(Self::Field, Vec<String>)> {
169        Vec::new()
170    }
171
172    /// Parse a completed submission: the form's only presence and type check.
173    ///
174    /// # Errors
175    ///
176    /// Every key that failed, each once, under one of [`Self::fields`]' keys.
177    fn parse(cx: &Cx, values: &FormValues) -> std::result::Result<Self, Vec<FieldError>>;
178
179    /// The create builder with every field set.
180    fn into_create(self) -> <Self::Model as Model>::Create;
181
182    /// The instance update builder with one assignment per named field, or
183    /// `None` when no field is named.
184    ///
185    /// `None` is what keeps a submission that names no field from building an
186    /// empty update, which toasty refuses with an assertion.
187    fn into_update<'a>(
188        self,
189        record: &'a mut Self::Model,
190        named: &HashSet<Self::Field>,
191    ) -> Option<<Self::Model as Model>::Update<'a>>;
192
193    /// Execute a builder [`Self::into_update`] returned.
194    ///
195    /// Toasty's instance update builder implements no trait that carries
196    /// `exec`, so the generic write reaches it through the form.
197    fn exec_update<'a>(
198        update: <Self::Model as Model>::Update<'a>,
199        ex: &'a mut dyn Executor,
200    ) -> impl Future<Output = toasty::Result<()>> + Send + 'a;
201
202    /// Whether the resource naming this form has create and edit pages.
203    ///
204    /// [`Panel::resource`](crate::Panel::resource) registers the create, edit,
205    /// and options routes only when this holds. [`NoForm`] sets it to `false`.
206    const HAS_FORM: bool = true;
207}
208
209/// The record form of a resource with no create or edit page.
210///
211/// A list-only resource names it as [`Resource::Form`](crate::Resource::Form):
212/// `type Form = NoForm<Self::Model>;`. [`fields`](RecordForm::fields) and
213/// [`hydrate`](RecordForm::hydrate) are empty and
214/// [`into_update`](RecordForm::into_update) answers `None`. No route parses or
215/// writes it, so [`parse`](RecordForm::parse) and
216/// [`into_create`](RecordForm::into_create) panic naming the model, and the
217/// future [`exec_update`](RecordForm::exec_update) returns panics when polled.
218pub struct NoForm<M>(std::marker::PhantomData<fn() -> M>);
219
220impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> NoForm<M> {
221    fn unreachable() -> ! {
222        panic!("`NoForm<{}>` has no form", std::any::type_name::<M>())
223    }
224}
225
226impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> RecordForm for NoForm<M> {
227    type Model = M;
228    type Field = std::convert::Infallible;
229
230    const HAS_FORM: bool = false;
231
232    fn fields() -> Vec<FormField<Self::Field>> {
233        Vec::new()
234    }
235
236    fn control(field: Self::Field) -> Schema<Self> {
237        match field {}
238    }
239
240    fn hydrate(_cx: &Cx, _record: &M) -> FormValues {
241        FormValues::new()
242    }
243
244    fn parse(_cx: &Cx, _values: &FormValues) -> std::result::Result<Self, Vec<FieldError>> {
245        Self::unreachable()
246    }
247
248    fn into_create(self) -> M::Create {
249        Self::unreachable()
250    }
251
252    fn into_update<'a>(
253        self,
254        _record: &'a mut M,
255        _named: &HashSet<Self::Field>,
256    ) -> Option<M::Update<'a>> {
257        None
258    }
259
260    fn exec_update<'a>(
261        update: M::Update<'a>,
262        ex: &'a mut dyn Executor,
263    ) -> impl Future<Output = toasty::Result<()>> + Send + 'a {
264        // `into_update` answers `None`, so no builder reaches here. The
265        // builder is not known to be `Send`, so the future must not hold it.
266        drop((update, ex));
267        async { Self::unreachable() }
268    }
269}
270
271/// An edit submission: the parsed form, and the fields the submission named.
272///
273/// Derefs to the form, so a record fn reads `posted.author_id` — the posted
274/// value, or the stored one when the submission did not name the field.
275pub struct Posted<F: RecordForm> {
276    form: F,
277    named: HashSet<F::Field>,
278}
279
280impl<F: RecordForm> Posted<F> {
281    /// A posted form that names `named`.
282    pub fn new(form: F, named: impl IntoIterator<Item = F::Field>) -> Self {
283        Self {
284            form,
285            named: named.into_iter().collect(),
286        }
287    }
288
289    /// Whether the submission named `field`.
290    pub fn named(&self, field: F::Field) -> bool {
291        self.named.contains(&field)
292    }
293
294    /// The update builder assigning every named field, or `None` when the
295    /// submission named none. See [`RecordForm::into_update`].
296    pub fn into_update(self, record: &mut F::Model) -> Option<<F::Model as Model>::Update<'_>> {
297        self.form.into_update(record, &self.named)
298    }
299}
300
301impl<F: RecordForm> std::ops::Deref for Posted<F> {
302    type Target = F;
303
304    fn deref(&self) -> &F {
305        &self.form
306    }
307}