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;