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