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