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}