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)]
204pub enum FieldErrorKind {
205    /// Posted empty, and the field has no blank answer.
206    Required,
207    /// Posted a value the field's type does not accept.
208    Invalid,
209}
210
211/// One key a submission was refused under, with the sentence the form renders
212/// beneath it.
213///
214/// The entry type of [`FieldErrors`]. [`Self::required`] is what
215/// [`RecordForm::parse`] answers for an unanswered key, whose control supplies
216/// the rendered wording.
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct FieldError {
219    /// The form key the error renders under.
220    pub key: String,
221    /// Why the key failed.
222    pub kind: FieldErrorKind,
223    /// The sentence the form shows.
224    pub message: String,
225}
226
227impl FieldError {
228    /// The submission left `key` unanswered — an empty field with no blank
229    /// answer, or a required group whose inputs were all empty; `message` is
230    /// the wording its control renders in the slot.
231    pub fn unanswered(key: impl Into<String>, message: impl Into<String>) -> Self {
232        Self {
233            key: key.into(),
234            kind: FieldErrorKind::Required,
235            message: message.into(),
236        }
237    }
238
239    /// `key` was posted empty and has no blank answer, worded as its own key
240    /// names it.
241    pub fn required(key: impl Into<String>) -> Self {
242        let key = key.into();
243        let message = format!("{key} is required");
244        Self::unanswered(key, message)
245    }
246
247    /// `key` carried a value its type refuses; `message` says why.
248    pub fn invalid(key: impl Into<String>, message: impl Into<String>) -> Self {
249        Self {
250            key: key.into(),
251            kind: FieldErrorKind::Invalid,
252            message: message.into(),
253        }
254    }
255}
256
257/// Read one scalar from a completed submission.
258///
259/// Trims the value; an empty one takes `blank` when the record form declares
260/// one, else the type's own [`FormScalar::blank`], else refuses as
261/// [`FieldErrorKind::Required`].
262pub fn parse_scalar<T: FormScalar>(
263    key: &str,
264    values: &HashMap<String, String>,
265    blank: Option<T>,
266) -> std::result::Result<T, FieldError> {
267    let raw = values.get(key).map(|value| value.trim()).unwrap_or("");
268    if raw.is_empty() {
269        return blank
270            .or_else(T::blank)
271            .ok_or_else(|| FieldError::required(key));
272    }
273    T::parse_form(raw).map_err(|message| FieldError::invalid(key, message))
274}
275
276/// One record-form field and the form keys it binds.
277#[derive(Debug, Clone)]
278pub struct FormField<K> {
279    /// The field, as the form's field enum names it.
280    pub field: K,
281    /// The Rust field name, for build-time refusals.
282    pub name: &'static str,
283    /// The keys the field binds: one for a scalar, every key of an embedded
284    /// value (an enum's discriminant first).
285    pub keys: Vec<String>,
286    /// Whether an empty submission has an answer. The build check refuses the
287    /// field only where one of its keys can be posted empty with nothing to
288    /// resolve it; a scalar's answer is its declared `#[form(blank = ..)]` or
289    /// its type's own, and an embedded value answers when every leaf does
290    /// (`EmbeddedForm::answers_blank`).
291    pub answers_blank: bool,
292}
293
294/// The typed value a resource's form submission parses into.
295///
296/// Derive it with [`tablo_core::RecordForm`](crate::RecordForm); a
297/// hand-written impl is what the derive expands to.
298pub trait RecordForm: Sized + Send + 'static {
299    /// The model the form writes.
300    type Model: Model + toasty::stmt::IntoExpr<Self::Model> + Send + Sync + 'static;
301
302    /// One variant per form field: the key [`Posted`] uses. Its bound form keys
303    /// are [`Self::fields`]'.
304    type Field: Copy + Eq + Hash + Debug + Send + Sync + 'static;
305
306    /// Every field, in declaration order, with the keys it binds.
307    fn fields() -> Vec<FormField<Self::Field>>;
308
309    /// The form's default schema: one control per field, in declaration
310    /// order. A [`ResourceDef`](crate::ResourceDef) without a [`form`](crate::ResourceDef::form)
311    /// renders it.
312    ///
313    /// The derive chooses each control from the field (see
314    /// [`RecordForm`](derive@crate::RecordForm)); the default here declares
315    /// none, which is what [`NoForm`] wants.
316    fn schema() -> Schema {
317        Schema::empty()
318    }
319
320    /// The form's default table: one column per field a column can show, in declaration order.
321    /// A [`ResourceDef`](crate::ResourceDef) without a [`table`](crate::ResourceDef::table) lists
322    /// it.
323    ///
324    /// The derive lists each text field in a sortable column, searchable over a `String` or
325    /// `Option<String>`, an options field by its option's label, and a toggle as yes or no (see
326    /// [`RecordForm`](derive@crate::RecordForm)). The default here lists none, so a resource
327    /// whose form lists nothing, such as one naming [`NoForm`], declares its own.
328    fn table() -> Table<Self::Model> {
329        Table::new(())
330    }
331
332    /// The stored record as the form spells it.
333    fn hydrate(cx: &Cx, record: &Self::Model) -> HashMap<String, String>;
334
335    /// Parse a completed, normalized submission.
336    ///
337    /// # Errors
338    ///
339    /// Every key that failed, each once.
340    fn parse(
341        cx: &Cx,
342        values: &HashMap<String, String>,
343    ) -> std::result::Result<Self, Vec<FieldError>>;
344
345    /// The create builder with every field set.
346    fn into_create(self) -> <Self::Model as Model>::Create;
347
348    /// The instance update builder with one assignment per named field, or
349    /// `None` when no field is named.
350    ///
351    /// `None` is what keeps a submission that names no field from building an
352    /// empty update, which toasty refuses with an assertion.
353    fn into_update<'a>(
354        self,
355        record: &'a mut Self::Model,
356        named: &HashSet<Self::Field>,
357    ) -> Option<<Self::Model as Model>::Update<'a>>;
358
359    /// Execute a builder [`Self::into_update`] returned.
360    ///
361    /// Toasty's instance update builder implements no trait that carries
362    /// `exec`, so the generic write reaches it through the form.
363    fn exec_update<'a>(
364        update: <Self::Model as Model>::Update<'a>,
365        ex: &'a mut dyn Executor,
366    ) -> impl Future<Output = toasty::Result<()>> + Send + 'a;
367
368    /// Whether the resource naming this form has create and edit pages.
369    ///
370    /// [`Panel::resource`](crate::Panel::resource) registers the create, edit,
371    /// and options routes only when this holds. [`NoForm`] sets it to `false`.
372    const HAS_FORM: bool = true;
373}
374
375/// The record form of a resource with no create or edit page.
376///
377/// A list-only resource names it as [`Resource::Form`](crate::Resource::Form):
378/// `type Form = NoForm<Self::Model>;`. [`fields`](RecordForm::fields) and
379/// [`hydrate`](RecordForm::hydrate) are empty and
380/// [`into_update`](RecordForm::into_update) answers `None`. No route parses or
381/// writes it, so [`parse`](RecordForm::parse) and
382/// [`into_create`](RecordForm::into_create) panic naming the model, and the
383/// future [`exec_update`](RecordForm::exec_update) returns panics when polled.
384pub struct NoForm<M>(std::marker::PhantomData<fn() -> M>);
385
386impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> NoForm<M> {
387    fn unreachable() -> ! {
388        panic!("`NoForm<{}>` has no form", std::any::type_name::<M>())
389    }
390}
391
392impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> RecordForm for NoForm<M> {
393    type Model = M;
394    type Field = std::convert::Infallible;
395
396    const HAS_FORM: bool = false;
397
398    fn fields() -> Vec<FormField<Self::Field>> {
399        Vec::new()
400    }
401
402    fn hydrate(_cx: &Cx, _record: &M) -> HashMap<String, String> {
403        HashMap::new()
404    }
405
406    fn parse(
407        _cx: &Cx,
408        _values: &HashMap<String, String>,
409    ) -> std::result::Result<Self, Vec<FieldError>> {
410        Self::unreachable()
411    }
412
413    fn into_create(self) -> M::Create {
414        Self::unreachable()
415    }
416
417    fn into_update<'a>(
418        self,
419        _record: &'a mut M,
420        _named: &HashSet<Self::Field>,
421    ) -> Option<M::Update<'a>> {
422        None
423    }
424
425    fn exec_update<'a>(
426        update: M::Update<'a>,
427        ex: &'a mut dyn Executor,
428    ) -> impl Future<Output = toasty::Result<()>> + Send + 'a {
429        // `into_update` answers `None`, so no builder reaches here. The
430        // builder is not known to be `Send`, so the future must not hold it.
431        drop((update, ex));
432        async { Self::unreachable() }
433    }
434}
435
436/// An edit submission: the parsed form, and the fields the submission named.
437///
438/// Derefs to the form, so a record fn reads `posted.author_id` — the posted
439/// value, or the stored one when the submission did not name the field.
440pub struct Posted<F: RecordForm> {
441    form: F,
442    named: HashSet<F::Field>,
443}
444
445impl<F: RecordForm> Posted<F> {
446    /// A posted form that names `named`.
447    pub fn new(form: F, named: impl IntoIterator<Item = F::Field>) -> Self {
448        Self {
449            form,
450            named: named.into_iter().collect(),
451        }
452    }
453
454    /// Whether the submission named `field`.
455    pub fn named(&self, field: F::Field) -> bool {
456        self.named.contains(&field)
457    }
458
459    /// The update builder assigning every named field, or `None` when the
460    /// submission named none. See [`RecordForm::into_update`].
461    pub fn into_update(self, record: &mut F::Model) -> Option<<F::Model as Model>::Update<'_>> {
462        self.form.into_update(record, &self.named)
463    }
464}
465
466impl<F: RecordForm> std::ops::Deref for Posted<F> {
467    type Target = F;
468
469    fn deref(&self) -> &F {
470        &self.form
471    }
472}
473
474/// A refused submission, keyed by the form key each error renders under.
475///
476/// One type for every source: the schema's own rules
477/// ([`Schema::validate`](crate::schema::Schema::validate)), a rejected upload, a
478/// failed uniqueness probe, and an app's
479/// [`validate_record`](crate::Resource::validate_record). The submit handler
480/// merges them without translating, and the form render reads each field's own
481/// key from the result.
482///
483/// A key the rendered form owns: a control's own key, or a
484/// [`Repeater`](crate::Repeater) group's label. A key no slot owns has nowhere
485/// to render, and the submit handler refuses it as a declaration error rather
486/// than writing past it.
487#[derive(Debug, Default)]
488pub struct FieldErrors {
489    errors: Vec<FieldError>,
490}
491
492impl FieldErrors {
493    /// No errors.
494    pub fn new() -> Self {
495        Self::default()
496    }
497
498    /// Refuse `key` with `message`.
499    pub fn add(&mut self, key: impl Into<String>, message: impl Into<String>) {
500        self.errors.push(FieldError::invalid(key, message));
501    }
502
503    /// Refuse `key` as unanswered, with the message its control renders.
504    pub fn add_required(&mut self, key: impl Into<String>, message: impl Into<String>) {
505        self.errors.push(FieldError::unanswered(key, message));
506    }
507
508    /// Refuse `error`'s key with `error`, keeping the error's own kind.
509    pub fn push(&mut self, error: FieldError) {
510        self.errors.push(error);
511    }
512
513    /// Whether any error renders under `key`.
514    pub fn contains_key(&self, key: &str) -> bool {
515        self.errors.iter().any(|error| error.key == key)
516    }
517
518    /// The error `key` renders: the first one added, like the render's own
519    /// first-message slot.
520    pub fn first(&self, key: &str) -> Option<&FieldError> {
521        self.errors.iter().find(|error| error.key == key)
522    }
523
524    /// Whether nothing was refused.
525    pub fn is_empty(&self) -> bool {
526        self.errors.is_empty()
527    }
528
529    /// Every error, in the order it was added.
530    pub fn iter(&self) -> impl Iterator<Item = &FieldError> {
531        self.errors.iter()
532    }
533
534    /// Append `other`'s errors after these.
535    pub fn extend(&mut self, other: Self) {
536        self.errors.extend(other.errors);
537    }
538
539    /// Take `other`'s errors, dropping this collection's own errors under every
540    /// key `other` names.
541    ///
542    /// A source that owns a key answers for it: a rejected upload replaces the
543    /// "required" the emptied control would otherwise report.
544    pub fn replace(&mut self, other: Self) {
545        let owned: HashSet<&str> = other
546            .errors
547            .iter()
548            .map(|error| error.key.as_str())
549            .collect();
550        self.errors
551            .retain(|kept| !owned.contains(kept.key.as_str()));
552        self.errors.extend(other.errors);
553    }
554}
555
556/// Which of `M`'s root fields the create builder fills before any setter runs:
557/// `#[auto]` fields, which the database fills, and `#[default(..)]` ones.
558///
559/// Read off `M::Create::default()`, because toasty keeps a `#[default]` in its
560/// generated code only, never in the app schema.
561pub(crate) fn prefilled_fields<M: Model>() -> Vec<bool> {
562    let insert = <M::Create as Default>::default().into_insert();
563    let toasty_core::stmt::Expr::Stmt(statement) = toasty_core::stmt::Expr::from(insert) else {
564        return Vec::new();
565    };
566    statement
567        .stmt
568        .as_insert()
569        .and_then(|insert| insert.source.body.as_values())
570        .and_then(|values| values.rows.last())
571        .and_then(|row| row.as_record())
572        .map(|record| {
573            record
574                .fields
575                .iter()
576                .map(|expr| !expr.is_value_null())
577                .collect()
578        })
579        .unwrap_or_default()
580}