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}