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
52mod values;
53
54use std::{collections::HashSet, fmt::Debug, hash::Hash};
55
56use toasty::{Executor, schema::Model};
57use topcoat::context::Cx;
58pub use values::{FormValue, FormValues};
59
60use crate::{
61    schema::{FieldResolver, OptionSource, Schema, TypedValue},
62    table::column::Includes,
63};
64
65/// A type one form key reads and writes: `String`, every [`TypedValue`] type,
66/// every enum deriving [`Options`](crate::Options), and an `Option` of any of them.
67///
68/// The value the parse sees is trimmed and non-empty; an empty submission is the
69/// record-form field's **blank answer** instead, and a field with none is required.
70///
71/// A text field binds a path of any `FormScalar` type
72/// ([`Field::text`](crate::Field::text)), and the form derives read and write
73/// every field that is not `#[form(embed)]` through it.
74#[diagnostic::on_unimplemented(
75    message = "`{Self}` is not a form scalar",
76    label = "a form field of this type has no text spelling",
77    note = "a form scalar is `String`, a `TypedValue` type, an `Options` enum, or an `Option` of \
78            one; implement `TypedValue` for an app type, derive `Options` for a unit enum, or \
79            mark an `EmbeddedForm` value `#[form(embed)]`"
80)]
81pub trait FormScalar: Sized {
82    /// The `type` attribute of the text control that edits it.
83    const INPUT_TYPE: &'static str = "text";
84
85    /// The control's `step` attribute, when its input type takes one.
86    const STEP: Option<&'static str> = None;
87
88    /// The control's `value` for a stored value's form spelling.
89    fn input_value(stored: &str) -> String {
90        stored.to_string()
91    }
92
93    /// Parse a trimmed, non-empty submission, or return the error message.
94    fn parse_form(value: &str) -> std::result::Result<Self, String>;
95
96    /// The form spelling of a stored value.
97    fn to_form(&self) -> String;
98
99    /// What a column cell and a group header read: the form spelling, or an
100    /// `Options` enum's label.
101    fn to_label(&self) -> String {
102        self.to_form()
103    }
104}
105
106impl FormScalar for String {
107    fn parse_form(value: &str) -> std::result::Result<Self, String> {
108        Ok(value.to_string())
109    }
110
111    fn to_form(&self) -> String {
112        self.clone()
113    }
114}
115
116impl<T: TypedValue> FormScalar for T {
117    const INPUT_TYPE: &'static str = T::INPUT_TYPE;
118    const STEP: Option<&'static str> = T::STEP;
119
120    fn input_value(stored: &str) -> String {
121        T::input_value(stored)
122    }
123
124    fn parse_form(value: &str) -> std::result::Result<Self, String> {
125        T::parse_input(value).ok_or_else(|| format!("`{value}` is not a valid {}", T::NOUN))
126    }
127
128    fn to_form(&self) -> String {
129        self.to_string()
130    }
131}
132
133/// A form scalar whose `Option` is one too: every [`TypedValue`] type, and every enum deriving
134/// [`Options`](crate::Options), which implements it.
135#[doc(hidden)]
136pub trait NullableScalar: FormScalar {}
137
138impl<T: TypedValue> NullableScalar for T {}
139
140impl<T: NullableScalar> FormScalar for Option<T> {
141    const INPUT_TYPE: &'static str = T::INPUT_TYPE;
142    const STEP: Option<&'static str> = T::STEP;
143
144    fn input_value(stored: &str) -> String {
145        T::input_value(stored)
146    }
147
148    fn parse_form(value: &str) -> std::result::Result<Self, String> {
149        T::parse_form(value).map(Some)
150    }
151
152    fn to_form(&self) -> String {
153        self.as_ref().map(T::to_form).unwrap_or_default()
154    }
155
156    fn to_label(&self) -> String {
157        self.as_ref().map(T::to_label).unwrap_or_default()
158    }
159}
160
161impl FormScalar for Option<String> {
162    fn parse_form(value: &str) -> std::result::Result<Self, String> {
163        Ok(Some(value.to_string()))
164    }
165
166    fn to_form(&self) -> String {
167        self.clone().unwrap_or_default()
168    }
169}
170
171/// Compiles only for a form scalar: the form derives call it, spanned on a
172/// field's type, so a field of another type fails there.
173#[doc(hidden)]
174pub fn assert_form_scalar<T: FormScalar>() {}
175
176/// Why a key was refused.
177#[derive(Debug, Clone, PartialEq, Eq)]
178#[non_exhaustive]
179pub enum FieldErrorKind {
180    /// Posted empty, and the field has no blank answer. The form renders
181    /// "{label} is required" under the control.
182    Required,
183    /// Posted a value the field refuses, with the sentence the form renders.
184    Invalid(String),
185}
186
187/// One key a submission was refused under: a form key, or a record form's
188/// field enum in [`Resource::validate_record`](crate::Resource::validate_record).
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct FieldError<K = String> {
191    /// What the error renders under.
192    pub key: K,
193    /// Why the key was refused.
194    pub kind: FieldErrorKind,
195}
196
197impl<K> FieldError<K> {
198    /// `key` was posted empty and has no blank answer.
199    pub fn required(key: impl Into<K>) -> Self {
200        Self {
201            key: key.into(),
202            kind: FieldErrorKind::Required,
203        }
204    }
205
206    /// `key` carried a value it refuses; `message` says why.
207    pub fn invalid(key: impl Into<K>, message: impl Into<String>) -> Self {
208        Self {
209            key: key.into(),
210            kind: FieldErrorKind::Invalid(message.into()),
211        }
212    }
213
214    /// The sentence the form renders under the control labelled `label`.
215    pub fn message(&self, label: &str) -> String {
216        match &self.kind {
217            FieldErrorKind::Required => format!("{label} is required"),
218            FieldErrorKind::Invalid(message) => message.clone(),
219        }
220    }
221}
222
223/// The refusal of a key holding another shape than its parse reads: a list or rows where it reads
224/// text, or text where it reads a list. Only a schema whose controls disagree with its parse
225/// holds one.
226pub(crate) fn unreadable(key: &str) -> FieldError {
227    FieldError::invalid(key, "The value could not be read")
228}
229
230/// Read one scalar from a completed submission.
231///
232/// Trims the value; an empty one is `blank`, the field's blank answer, or
233/// refuses as [`FieldErrorKind::Required`] when it has none.
234pub fn parse_scalar<T: FormScalar>(
235    key: &str,
236    values: &FormValues,
237    blank: Option<T>,
238) -> std::result::Result<T, FieldError> {
239    let raw = match values.get(key) {
240        None => "",
241        Some(FormValue::Text(text)) => text.trim(),
242        Some(_) => return Err(unreadable(key)),
243    };
244    if raw.is_empty() {
245        return blank.ok_or_else(|| FieldError::required(key));
246    }
247    T::parse_form(raw).map_err(|message| FieldError::invalid(key, message))
248}
249
250/// Read a password from a completed submission: required as [`parse_scalar`] reads a `String`,
251/// but kept as typed, with its outer spaces, since a login compares the password untrimmed.
252pub fn parse_password(
253    key: &str,
254    values: &FormValues,
255    blank: Option<String>,
256) -> std::result::Result<String, FieldError> {
257    parse_scalar::<String>(key, values, blank)?;
258    Ok(values.text(key).unwrap_or_default().to_string())
259}
260
261/// The values a multiple choice holds, as its one form key carries them: what a record form
262/// hydrates a many-to-many field with, and what a hand-written one writes.
263pub fn write_list<'a, T: FormScalar + 'a>(values: impl IntoIterator<Item = &'a T>) -> FormValue {
264    FormValue::List(values.into_iter().map(T::to_form).collect())
265}
266
267/// Read the values a multiple choice posted, none when it posted nothing: what a record form's
268/// parse reads a many-to-many field with.
269///
270/// # Errors
271///
272/// A value `T` refuses, or a key holding no list, under `key`.
273pub fn parse_list<T: FormScalar>(
274    key: &str,
275    values: &FormValues,
276) -> std::result::Result<Vec<T>, FieldError> {
277    let items: &[String] = match values.get(key) {
278        None => &[],
279        Some(FormValue::List(items)) => items,
280        Some(_) => return Err(unreadable(key)),
281    };
282    items
283        .iter()
284        .map(|item| T::parse_form(item).map_err(|message| FieldError::invalid(key, message)))
285        .collect()
286}
287
288/// The form key of the many-to-many field `path`, a `#[has_many(via = ..)]` list of `R`'s
289/// records, holding their keys of type `K`: the derive calls it, so a field whose type is not
290/// `Vec<K>` of the related model's key, or whose source is not that model's, fails to compile.
291#[doc(hidden)]
292pub fn links_key<M, R, K>(
293    path: impl Into<toasty::stmt::Path<M, toasty::stmt::List<R::Model>>>,
294) -> String
295where
296    M: Model,
297    R: OptionSource,
298    R::Model: Model<PrimaryKey = K>,
299    K: FormScalar,
300{
301    crate::schema::form_key(path.into())
302}
303
304/// `includes` and the include loading the many-to-many field `path`, which
305/// [`RecordForm::hydrate`] reads.
306#[doc(hidden)]
307pub fn links_include<M, R>(
308    includes: Includes<M>,
309    path: impl Into<toasty::stmt::Path<M, toasty::stmt::List<R::Model>>>,
310) -> Includes<M>
311where
312    M: Model,
313    R: OptionSource,
314{
315    includes.with(path.into())
316}
317
318/// The keys of the records a many-to-many field holds, as its form key carries them, or `None`
319/// when the record was loaded without them.
320#[doc(hidden)]
321pub fn write_links<T>(records: &toasty::Deferred<Vec<T>>) -> Option<FormValue>
322where
323    T: Model + toasty::stmt::IntoExpr<T>,
324{
325    (!records.is_unloaded()).then(|| {
326        FormValue::List(
327            records
328                .get()
329                .iter()
330                .map(crate::toasty_compat::pk::pk_text)
331                .collect(),
332        )
333    })
334}
335
336/// One record-form field and the form keys it binds.
337#[derive(Debug, Clone)]
338pub struct FormField<K> {
339    /// The field, as the form's field enum names it.
340    pub field: K,
341    /// The Rust field name, for build-time refusals.
342    pub name: &'static str,
343    /// The keys the field binds: one for a scalar, every key of an embedded
344    /// value (an enum's discriminant first). An error on the field renders
345    /// under the first.
346    pub keys: Vec<String>,
347    /// The keys with no blank answer, which an empty submission refuses: the
348    /// panel renders their controls required.
349    pub required: Vec<String>,
350}
351
352/// The typed value a resource's form submission parses into.
353///
354/// Derive it from a model with [`tablo_core::ModelForm`](crate::ModelForm) or from a struct with
355/// [`tablo_core::RecordForm`](crate::RecordForm); a hand-written impl is what the derives expand
356/// to.
357pub trait RecordForm: Sized + Send + 'static {
358    /// The model the form writes.
359    type Model: Model + toasty::stmt::IntoExpr<Self::Model> + Send + Sync + 'static;
360
361    /// One variant per form field: the key [`Posted`] uses. Its bound form keys
362    /// are [`Self::fields`]'.
363    type Field: Copy + Eq + Hash + Debug + Send + Sync + 'static;
364
365    /// Every field, in declaration order, with the keys it binds through `resolver`'s app schema.
366    fn fields(resolver: &FieldResolver) -> Vec<FormField<Self::Field>>;
367
368    /// The control the form renders `field` with when its [`form`](crate::ResourceDef::form)
369    /// does not place it: what `controls()` hands over for the field, as a one-control schema.
370    ///
371    /// A resource's form renders every field it does not place after the ones it does, in
372    /// declaration order, so a resource that declares no form renders one control per field.
373    fn control(field: Self::Field) -> Schema<Self>;
374
375    /// The relations [`hydrate`](Self::hydrate) reads off the record: each many-to-many field's,
376    /// which the edit page and the edit's write load the record with.
377    fn includes() -> Includes<Self::Model> {
378        Includes::new()
379    }
380
381    /// The stored record as the form spells it.
382    fn hydrate(cx: &Cx, record: &Self::Model) -> FormValues;
383
384    /// The records each many-to-many field holds, as their keys' form spelling: what
385    /// [`write_create`](crate::resource::write_create) links a new record to, and what
386    /// [`write_update`](crate::resource::write_update) leaves a posted field's record linked to.
387    fn links(&self) -> Vec<(Self::Field, Vec<String>)> {
388        Vec::new()
389    }
390
391    /// Parse a completed submission: the form's only presence and type check.
392    ///
393    /// # Errors
394    ///
395    /// Every key that failed, each once, under one of [`Self::fields`]' keys.
396    fn parse(cx: &Cx, values: &FormValues) -> std::result::Result<Self, Vec<FieldError>>;
397
398    /// The create builder with every field set.
399    fn into_create(self) -> <Self::Model as Model>::Create;
400
401    /// The instance update builder with one assignment per named field, or
402    /// `None` when no field is named.
403    ///
404    /// `None` is what keeps a submission that names no field from building an
405    /// empty update, which toasty refuses with an assertion.
406    fn into_update<'a>(
407        self,
408        record: &'a mut Self::Model,
409        named: &HashSet<Self::Field>,
410    ) -> Option<<Self::Model as Model>::Update<'a>>;
411
412    /// Execute a builder [`Self::into_update`] returned.
413    ///
414    /// Toasty's instance update builder implements no trait that carries
415    /// `exec`, so the generic write reaches it through the form.
416    fn exec_update<'a>(
417        update: <Self::Model as Model>::Update<'a>,
418        ex: &'a mut dyn Executor,
419    ) -> impl Future<Output = toasty::Result<()>> + Send + 'a;
420
421    /// Whether the resource naming this form has create and edit pages.
422    ///
423    /// [`Panel::resource`](crate::Panel::resource) registers the create, edit,
424    /// and options routes only when this holds. [`NoForm`] sets it to `false`.
425    const HAS_FORM: bool = true;
426}
427
428/// The record form of a resource with no create or edit page.
429///
430/// A list-only resource names it as [`Resource::Form`](crate::Resource::Form):
431/// `type Form = NoForm<Self::Model>;`. [`fields`](RecordForm::fields) and
432/// [`hydrate`](RecordForm::hydrate) are empty and
433/// [`into_update`](RecordForm::into_update) answers `None`. No route parses or
434/// writes it, so [`parse`](RecordForm::parse) and
435/// [`into_create`](RecordForm::into_create) panic naming the model, and the
436/// future [`exec_update`](RecordForm::exec_update) returns panics when polled.
437pub struct NoForm<M>(std::marker::PhantomData<fn() -> M>);
438
439impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> NoForm<M> {
440    fn unreachable() -> ! {
441        panic!("`NoForm<{}>` has no form", std::any::type_name::<M>())
442    }
443}
444
445impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> RecordForm for NoForm<M> {
446    type Model = M;
447    type Field = std::convert::Infallible;
448
449    const HAS_FORM: bool = false;
450
451    fn fields(_resolver: &FieldResolver) -> Vec<FormField<Self::Field>> {
452        Vec::new()
453    }
454
455    fn control(field: Self::Field) -> Schema<Self> {
456        match field {}
457    }
458
459    fn hydrate(_cx: &Cx, _record: &M) -> FormValues {
460        FormValues::new()
461    }
462
463    fn parse(_cx: &Cx, _values: &FormValues) -> std::result::Result<Self, Vec<FieldError>> {
464        Self::unreachable()
465    }
466
467    fn into_create(self) -> M::Create {
468        Self::unreachable()
469    }
470
471    fn into_update<'a>(
472        self,
473        _record: &'a mut M,
474        _named: &HashSet<Self::Field>,
475    ) -> Option<M::Update<'a>> {
476        None
477    }
478
479    fn exec_update<'a>(
480        update: M::Update<'a>,
481        ex: &'a mut dyn Executor,
482    ) -> impl Future<Output = toasty::Result<()>> + Send + 'a {
483        // `into_update` answers `None`, so no builder reaches here. The
484        // builder is not known to be `Send`, so the future must not hold it.
485        drop((update, ex));
486        async { Self::unreachable() }
487    }
488}
489
490/// An edit submission: the parsed form, and the fields the submission named.
491///
492/// Derefs to the form, so a record fn reads `posted.author_id` — the posted
493/// value, or the stored one when the submission did not name the field.
494pub struct Posted<F: RecordForm> {
495    form: F,
496    named: HashSet<F::Field>,
497}
498
499impl<F: RecordForm> Posted<F> {
500    /// A posted form that names `named`.
501    pub fn new(form: F, named: impl IntoIterator<Item = F::Field>) -> Self {
502        Self {
503            form,
504            named: named.into_iter().collect(),
505        }
506    }
507
508    /// Whether the submission named `field`.
509    pub fn named(&self, field: F::Field) -> bool {
510        self.named.contains(&field)
511    }
512
513    /// The update builder assigning every named field, or `None` when the
514    /// submission named none. See [`RecordForm::into_update`].
515    pub fn into_update(self, record: &mut F::Model) -> Option<<F::Model as Model>::Update<'_>> {
516        self.form.into_update(record, &self.named)
517    }
518}
519
520impl<F: RecordForm> std::ops::Deref for Posted<F> {
521    type Target = F;
522
523    fn deref(&self) -> &F {
524        &self.form
525    }
526}
527
528/// A refused submission.
529///
530/// [`Resource::validate_record`](crate::Resource::validate_record) keys it by the record form's
531/// field enum (`FieldErrors<UserFormField>`), so an error always names a field the form renders;
532/// one on an embedded value renders under its first control. The panel keys every error by the
533/// form key it renders under (`FieldErrors`, keyed by `String`), and [`Source::form`] reads them
534/// back when a schema renders.
535///
536/// [`Source::form`]: crate::Source::form
537#[derive(Debug)]
538pub struct FieldErrors<K = String> {
539    errors: Vec<FieldError<K>>,
540}
541
542impl<K> Default for FieldErrors<K> {
543    fn default() -> Self {
544        Self { errors: Vec::new() }
545    }
546}
547
548impl<K> FieldErrors<K> {
549    /// No errors.
550    pub fn new() -> Self {
551        Self::default()
552    }
553
554    /// Refuse `key` with `message`.
555    pub fn add(&mut self, key: impl Into<K>, message: impl Into<String>) {
556        self.errors.push(FieldError::invalid(key, message));
557    }
558
559    /// Refuse `error`'s key with `error`.
560    pub fn push(&mut self, error: FieldError<K>) {
561        self.errors.push(error);
562    }
563
564    /// Whether nothing was refused.
565    pub fn is_empty(&self) -> bool {
566        self.errors.is_empty()
567    }
568
569    /// Every error, in the order it was added.
570    pub fn iter(&self) -> impl Iterator<Item = &FieldError<K>> {
571        self.errors.iter()
572    }
573
574    /// Append `other`'s errors after these.
575    pub fn extend(&mut self, other: Self) {
576        self.errors.extend(other.errors);
577    }
578}
579
580impl<K> IntoIterator for FieldErrors<K> {
581    type Item = FieldError<K>;
582    type IntoIter = std::vec::IntoIter<FieldError<K>>;
583
584    fn into_iter(self) -> Self::IntoIter {
585        self.errors.into_iter()
586    }
587}
588
589impl FieldErrors {
590    /// Whether any error renders under `key`.
591    pub fn contains_key(&self, key: &str) -> bool {
592        self.errors.iter().any(|error| error.key == key)
593    }
594
595    /// The error `key` renders: the first one added.
596    pub fn first(&self, key: &str) -> Option<&FieldError> {
597        self.errors.iter().find(|error| error.key == key)
598    }
599
600    /// Take `other`'s errors, dropping this collection's own errors under every
601    /// key `other` names.
602    ///
603    /// A source that owns a key answers for it: a rejected upload replaces the
604    /// error the emptied control would otherwise report.
605    pub(crate) fn replace(&mut self, other: Self) {
606        let owned: HashSet<&str> = other
607            .errors
608            .iter()
609            .map(|error| error.key.as_str())
610            .collect();
611        self.errors
612            .retain(|kept| !owned.contains(kept.key.as_str()));
613        self.errors.extend(other.errors);
614    }
615}
616
617#[cfg(test)]
618mod tests;