Skip to main content

tablo_core/schema/
fields.rs

1//! One [`Field`] type whose control is text, choice, file, or an app's own [`Control`], built from
2//! a path binding a column or an embedded leaf.
3
4mod builders;
5mod check;
6mod choice;
7mod chrome;
8pub(crate) mod custom;
9pub(crate) mod file;
10mod placement;
11mod read_only;
12mod render;
13#[cfg(feature = "rich-text")]
14mod rich_text;
15mod text;
16
17use std::sync::Arc;
18
19pub use builders::{
20    ChoiceField, CustomField, FileField, IntoFormValue, IntoOptions, RepeaterField, TextField,
21};
22pub(crate) use check::Items;
23pub(crate) use choice::{ChoiceControl, Relationship, option_view};
24pub(crate) use chrome::{FieldChrome, error_kind, render_field};
25pub(crate) use custom::{Control, ControlInput};
26pub use custom::{Switch, Toggle};
27pub(crate) use file::{FileControl, MAX_FORM_BYTES, is_linkable, is_rooted, stored_upload};
28pub(crate) use placement::Placement;
29pub(crate) use read_only::{read_only, value_cell};
30#[cfg(feature = "rich-text")]
31use rich_text::RichTextControl;
32#[cfg(feature = "rich-text")]
33pub use rich_text::RichTextField;
34pub(crate) use text::{Kind, Pattern, TextControl, password_reveal};
35use toasty::stmt::{List, Path};
36use topcoat::context::Cx;
37
38use super::{condition::Condition, lenses::Binding, repeater::RepeaterControl};
39use crate::{
40    Text,
41    schema::FormInput,
42    values::{FieldError, FormScalar, FormValue},
43};
44
45/// One form field binding a lens to the control editing it, offering only that control's modifiers
46/// so a modifier on the wrong control does not compile.
47///
48/// ```rust
49/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
50/// # struct User {
51/// #     #[key] #[auto] id: uuid::Uuid,
52/// #     name: String,
53/// #     email: String,
54/// #     age: i64,
55/// # }
56/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
57/// # struct Post {
58/// #     #[key] #[auto] id: uuid::Uuid,
59/// #     body: String,
60/// #     status: String,
61/// #     author_id: uuid::Uuid,
62/// #     featured: bool,
63/// # }
64/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
65/// # struct Doc { #[key] #[auto] id: uuid::Uuid, path: String }
66/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
67/// # struct Author { #[key] #[auto] id: uuid::Uuid, name: String }
68/// # #[derive(Debug, Clone, Copy, PartialEq, Eq, tablo_core::Options)]
69/// # enum Status { Draft, Published }
70/// # struct AuthorResource;
71/// # impl tablo_core::extend::OptionSource for AuthorResource {
72/// #     type Model = Author;
73/// #     fn scoped_query(_cx: &topcoat::context::Cx)
74/// #         -> topcoat::Result<toasty::stmt::Query<toasty::stmt::List<Author>>>
75/// #     {
76/// #         Ok(toasty::stmt::Query::all())
77/// #     }
78/// #     fn label(_cx: &topcoat::context::Cx, author: &Author) -> String {
79/// #         author.name.clone()
80/// #     }
81/// # }
82/// # use tablo_core::{Field, Options, lens};
83/// Field::text(lens!(User.name)).placeholder("Ada Lovelace"); // TextField
84/// Field::text(lens!(User.email)).email().unique();
85/// Field::text(lens!(User.age)); // typed: an `i64` column
86/// Field::text(lens!(Post.body)).multiline(6);
87/// Field::choice(lens!(Post.status)).options(Status::options()); // ChoiceField
88/// Field::choice(lens!(Post.author_id)).relationship::<AuthorResource>();
89/// Field::file(lens!(Doc.path)); // FileField
90/// Field::toggle(lens!(Post.featured)); // CustomField
91/// ```
92///
93/// Options belong to a choice: a text field has no `options` method.
94pub struct Field {
95    binding: Binding,
96    /// The declared label, over the binding's.
97    label: Option<Text>,
98    /// Whether the control renders as required: the panel sets it from the record form, and an
99    /// embedded value's derive from its own fields.
100    required: bool,
101    control: ControlKind,
102    /// The condition showing the field, set by `visible_when`.
103    condition: Option<Condition>,
104    /// Whether the control is a checkbox, which a condition follows through `checked`.
105    checkbox: bool,
106    /// Whether the checkbox must be checked, set by `accepted`.
107    accepted: bool,
108    /// The help text under the control, set by `help`.
109    help: Option<Text>,
110    /// When the control renders disabled and the server ignores what it posts.
111    disabled: Disabled,
112    /// The create form's initial value, as the control posts it, set by `default`.
113    default: Option<FormValue>,
114}
115
116/// When a [`Field`] renders disabled, so the browser posts nothing for it and the server drops
117/// a key posted anyway.
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub(crate) enum Disabled {
120    #[default]
121    Never,
122    Always,
123    /// On the edit form only: the create form takes the value, which no edit then changes.
124    OnEdit,
125}
126
127/// The control a [`Field`] renders, with what only that control declares.
128pub(crate) enum ControlKind {
129    /// A one-line `<input>`, or a `<textarea>` when `rows` is set.
130    Text(TextControl),
131    /// A `<textarea>` of HTML, which the browser edits through a toolbar.
132    #[cfg(feature = "rich-text")]
133    RichText(RichTextControl),
134    /// A `<select>` over static options or a relationship's rows.
135    Choice(ChoiceControl),
136    /// A file input storing the uploaded file's path.
137    File(FileControl),
138    /// An app's control, or the built-in [`Toggle`].
139    Custom(Arc<dyn Control>),
140    /// A row of an item's controls per value of a `#[document]` list.
141    Repeater(RepeaterControl),
142}
143
144impl std::fmt::Debug for Field {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        let control = match &self.control {
147            ControlKind::Text(_) => "text",
148            #[cfg(feature = "rich-text")]
149            ControlKind::RichText(_) => "rich text",
150            ControlKind::Choice(_) => "choice",
151            ControlKind::File(_) => "file",
152            ControlKind::Custom(_) => "custom",
153            ControlKind::Repeater(_) => "repeater",
154        };
155        f.debug_struct("Field")
156            .field("name", &self.name())
157            .field("label", &self.label())
158            .field("required", &self.required)
159            .field("control", &control)
160            .field("condition", &self.condition)
161            .field("help", &self.help)
162            .field("disabled", &self.disabled)
163            .field("default", &self.default)
164            .finish()
165    }
166}
167
168impl Field {
169    fn bound(binding: Binding, control: ControlKind) -> Self {
170        Self {
171            binding,
172            label: None,
173            required: false,
174            control,
175            condition: None,
176            checkbox: false,
177            accepted: false,
178            help: None,
179            disabled: Disabled::Never,
180            default: None,
181        }
182    }
183
184    /// Binds any [`FormScalar`] column, storing the type's own spelling and probing uniqueness
185    /// through the lens.
186    pub fn text<M, T>(lens: impl Into<Path<M, T>>) -> TextField
187    where
188        M: toasty::schema::Model,
189        T: FormScalar + toasty::stmt::IntoExpr<T> + 'static,
190    {
191        let path: Path<M, T> = lens.into();
192        let binding = Binding::of(&path);
193        let control =
194            TextControl::new::<M, T>(path, binding.unique(), binding.unique_with().to_vec());
195        TextField::new(Self::bound(binding, ControlKind::Text(control)))
196    }
197
198    /// The text field `#[derive(EmbeddedForm)]` renders for a leaf.
199    #[doc(hidden)]
200    pub fn embedded_leaf<M, T>(path: Path<M, T>) -> TextField
201    where
202        M: toasty::schema::Model,
203        T: FormScalar,
204    {
205        TextField::new(Self::bound(
206            Binding::of(&path),
207            ControlKind::Text(TextControl::leaf::<T>()),
208        ))
209    }
210
211    /// A choice field over any column, with options from [`options`](ChoiceField::options) or
212    /// [`relationship`](ChoiceField::relationship).
213    pub fn choice<M, T>(lens: impl Into<Path<M, T>>) -> ChoiceField
214    where
215        M: toasty::schema::Model,
216    {
217        ChoiceField::new(Self::bound(
218            Binding::of::<M, T>(&lens.into()),
219            ControlKind::Choice(ChoiceControl::default()),
220        ))
221    }
222
223    /// A file field over a `String` column holding the uploaded path, rendering no `value`
224    /// attribute.
225    pub fn file<M>(lens: impl Into<Path<M, String>>) -> FileField
226    where
227        M: toasty::schema::Model,
228    {
229        FileField::new(Self::bound(
230            Binding::of::<M, String>(&lens.into()),
231            ControlKind::File(FileControl::default()),
232        ))
233    }
234
235    /// A checkbox over a `bool` column that submits `false` when unchecked.
236    pub fn toggle<M>(lens: impl Into<Path<M, bool>>) -> CustomField
237    where
238        M: toasty::schema::Model,
239    {
240        let mut field = Self::custom(lens, Toggle);
241        field.0.checkbox = true;
242        field
243    }
244
245    /// A field over any [`FormScalar`] column, rendered by an app's [`Control`].
246    pub fn custom<M, T>(lens: impl Into<Path<M, T>>, control: impl Control + 'static) -> CustomField
247    where
248        M: toasty::schema::Model,
249        T: FormScalar,
250    {
251        CustomField::new(Self::bound(
252            Binding::of::<M, T>(&lens.into()),
253            ControlKind::Custom(Arc::new(control)),
254        ))
255    }
256
257    /// A row of `T`'s controls per item of a `#[document]` list of `T`, which the browser adds,
258    /// removes and moves: a record form's `#[form(repeat)]` control.
259    ///
260    /// Each row posts its item's keys under `{key}.{row}.`. A resource's form and an action's
261    /// input read them into `key`'s rows, and a page handling its own post reads it with
262    /// [`Schema::read`](crate::Schema::read); the items then parse with
263    /// [`parse_items`](crate::schema::parse_items). [`RepeaterField`] has the rules.
264    pub fn repeater<M, T>(lens: impl Into<Path<M, List<T>>>) -> RepeaterField
265    where
266        M: toasty::schema::Model,
267        T: FormInput,
268    {
269        RepeaterField::new(Self::bound(
270            Binding::of::<M, List<T>>(&lens.into()),
271            ControlKind::Repeater(RepeaterControl::new::<T>()),
272        ))
273    }
274
275    /// A text field posting `name`, a key no column binds: a [`FormInput`](crate::FormInput)
276    /// field. `T` picks the input type, as a column's type does for [`Field::text`].
277    pub fn text_input<T: FormScalar>(name: impl Into<String>) -> TextField {
278        TextField::new(Self::bound(
279            Self::named(name.into()),
280            ControlKind::Text(TextControl::leaf::<T>()),
281        ))
282    }
283
284    /// A choice field posting `name`, a key no column binds, with options from
285    /// [`options`](ChoiceField::options).
286    pub fn choice_input(name: impl Into<String>) -> ChoiceField {
287        ChoiceField::new(Self::bound(
288            Self::named(name.into()),
289            ControlKind::Choice(ChoiceControl::default()),
290        ))
291    }
292
293    /// A checkbox posting `name`, a key no column binds, that submits `false` when unchecked.
294    pub fn toggle_input(name: impl Into<String>) -> CustomField {
295        let mut field = Self::bound(
296            Self::named(name.into()),
297            ControlKind::Custom(Arc::new(Toggle)),
298        );
299        field.checkbox = true;
300        CustomField::new(field)
301    }
302
303    /// A binding for `name`, labelled as a column of that name would be.
304    fn named(name: String) -> Binding {
305        let label = Text::field(&name);
306        Binding::named(name, label)
307    }
308
309    /// The variant control of an embedded enum, never required since an empty submit reads the
310    /// variant from the payload.
311    pub(crate) fn discriminant(binding: Binding, variants: Vec<(String, Text)>) -> Self {
312        let choice = ChoiceControl {
313            discriminant: true,
314            options: variants,
315            ..ChoiceControl::default()
316        };
317        Self::bound(binding, ControlKind::Choice(choice))
318    }
319
320    pub(crate) fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
321        self.binding.misdeclared()
322    }
323
324    /// The key the field posts: the storage column its path names.
325    pub fn name(&self) -> &str {
326        self.binding.name()
327    }
328
329    /// The declared label, else the binding's.
330    pub(crate) fn label(&self) -> Text {
331        self.label.clone().unwrap_or_else(|| self.binding.label())
332    }
333
334    /// Whether the field is a choice declaring neither options nor a relationship.
335    pub(crate) fn offers_nothing(&self) -> bool {
336        self.as_choice().is_some_and(ChoiceControl::offers_nothing)
337    }
338
339    /// Whether the field is a masked text control, whose value never renders back.
340    pub(crate) fn is_password(&self) -> bool {
341        matches!(&self.control, ControlKind::Text(text) if text.password)
342    }
343
344    pub(crate) fn as_repeater(&self) -> Option<&RepeaterControl> {
345        match &self.control {
346            ControlKind::Repeater(repeater) => Some(repeater),
347            _ => None,
348        }
349    }
350
351    /// Posts under `prefix`: a repeater row's control, keeping its label.
352    pub(crate) fn prefix(&mut self, prefix: &str) {
353        let label = self.label();
354        self.binding = Binding::named(format!("{prefix}{}", self.name()), label);
355        self.prefix_comparisons(prefix);
356    }
357
358    pub(crate) fn as_choice(&self) -> Option<&ChoiceControl> {
359        match &self.control {
360            ControlKind::Choice(choice) => Some(choice),
361            _ => None,
362        }
363    }
364
365    pub(crate) fn is_file(&self) -> bool {
366        matches!(self.control, ControlKind::File(_))
367    }
368
369    pub(crate) fn is_unique(&self) -> bool {
370        matches!(&self.control, ControlKind::Text(text) if text.unique)
371    }
372
373    /// The fields each unique index covering a unique text field holds besides it.
374    pub(crate) fn unique_with(&self) -> Vec<&[String]> {
375        match &self.control {
376            ControlKind::Text(text) if text.unique => text.unique_with(),
377            _ => Vec::new(),
378        }
379    }
380
381    /// The condition showing the field, if any.
382    pub(crate) fn condition(&self) -> Option<&Condition> {
383        self.condition.as_ref()
384    }
385
386    /// Shows the field only while `condition` holds, as `visible_when` does.
387    pub(crate) fn set_condition(&mut self, condition: Condition) {
388        self.condition = Some(condition);
389    }
390
391    /// Whether the control is a checkbox.
392    pub(crate) fn is_checkbox(&self) -> bool {
393        self.checkbox
394    }
395
396    /// Whether the control can post `value`: a checkbox posts `true` or `false`, and a choice over
397    /// static options one of their values. `None` when the control does not say: a text field, a
398    /// relationship, or an app's own control.
399    pub(crate) fn can_post(&self, value: &str) -> Option<bool> {
400        if self.checkbox {
401            return Some(value == "true" || value == "false");
402        }
403        match &self.control {
404            // Its key holds a list, which no condition value spells.
405            ControlKind::Choice(choice) if choice.is_multiple() => Some(false),
406            ControlKind::Choice(choice) if !choice.is_relationship() => {
407                Some(value.is_empty() || choice.label_of(value).is_some())
408            }
409            _ => None,
410        }
411    }
412
413    /// Whether the control could never post `value`: a typed text field's type does not parse
414    /// it, a choice over static options or a checkbox does not offer it, or it is a list where
415    /// the control posts one value, or one value where it posts a list.
416    pub(crate) fn refuses(&self, value: &FormValue) -> bool {
417        match (&self.control, value) {
418            (ControlKind::Choice(choice), FormValue::List(items)) if choice.is_multiple() => {
419                !choice.is_relationship()
420                    && items.iter().any(|item| choice.label_of(item).is_none())
421            }
422            (ControlKind::Choice(choice), _) if choice.is_multiple() => true,
423            (ControlKind::Text(text), FormValue::Text(value)) => {
424                !value.trim().is_empty() && !text.parses(value)
425            }
426            (_, FormValue::Text(value)) => self.can_post(value) == Some(false),
427            _ => true,
428        }
429    }
430
431    /// Whether the control renders as required.
432    pub(crate) fn is_required(&self) -> bool {
433        self.required
434    }
435
436    /// Whether the control renders disabled, on the edit form when `editing`.
437    pub(crate) fn is_disabled(&self, editing: bool) -> bool {
438        match self.disabled {
439            Disabled::Never => false,
440            Disabled::Always => true,
441            Disabled::OnEdit => editing,
442        }
443    }
444
445    /// Whether the control renders disabled where `placement` puts it.
446    pub(crate) fn is_disabled_at(&self, placement: &Placement<'_>) -> bool {
447        placement.locked || self.is_disabled(placement.editing)
448    }
449
450    /// Whether the control renders disabled on the create form as well as the edit form.
451    pub(crate) fn is_always_disabled(&self) -> bool {
452        self.disabled == Disabled::Always
453    }
454
455    /// The create form's initial value, as the control posts it.
456    pub(crate) fn default_value(&self) -> Option<&FormValue> {
457        self.default.as_ref()
458    }
459
460    /// The validation state and help text the control renders with.
461    fn chrome(
462        &self,
463        cx: &Cx,
464        id: String,
465        error: Option<&FieldError>,
466        fallback: Option<String>,
467    ) -> FieldChrome {
468        let mut chrome = FieldChrome::new(cx, id, error, &self.label(), fallback);
469        chrome.help = self.help.clone();
470        chrome
471    }
472
473    /// Renders the control required when its record-form field has no blank answer.
474    pub(crate) fn set_required(&mut self, required: bool) {
475        self.required = required;
476    }
477
478    /// Whether the bound column stores NULL, which a unique index admits any number of times.
479    pub(crate) fn is_nullable(&self) -> bool {
480        self.binding.nullable()
481    }
482
483    /// Whether two submissions spell the same stored value, as the unique check compares them.
484    pub(crate) fn same_value(&self, a: &str, b: &str) -> bool {
485        match &self.control {
486            ControlKind::Text(text) => text.same_value(a, b),
487            _ => a.trim() == b.trim(),
488        }
489    }
490
491    /// The key of the field whose value narrows a dependent choice's options.
492    pub(crate) fn parent_key(&self) -> Option<&str> {
493        self.as_choice().and_then(ChoiceControl::parent_key)
494    }
495
496    /// Whether a submitted choice matches the options its parent's value `parent` offers; a
497    /// multiple choice checks only the keys its stored value `stored` does not hold.
498    pub(crate) async fn validate_exists(
499        &self,
500        cx: &Cx,
501        value: &FormValue,
502        parent: Option<&str>,
503        stored: Option<&FormValue>,
504    ) -> Vec<crate::Text> {
505        match &self.control {
506            ControlKind::Choice(choice) => {
507                choice
508                    .validate_exists(cx, &self.label(), value, parent, stored)
509                    .await
510            }
511            _ => Vec::new(),
512        }
513    }
514
515    /// Re-checks a relationship choice in the write's transaction; a multiple choice re-checks
516    /// only the keys its stored value `stored` does not hold.
517    pub(crate) async fn recheck(
518        &self,
519        cx: &Cx,
520        value: &FormValue,
521        parent: Option<&str>,
522        stored: Option<&FormValue>,
523        ex: &mut dyn toasty::Executor,
524    ) -> Vec<crate::Text> {
525        match &self.control {
526            ControlKind::Choice(choice) => {
527                choice
528                    .recheck(cx, &self.label(), value, parent, stored, ex)
529                    .await
530            }
531            _ => Vec::new(),
532        }
533    }
534
535    /// A multiple choice's `value` with the stored keys it drops that the user cannot view put
536    /// back, or `None` when it drops none.
537    pub(crate) async fn keep_unseen(
538        &self,
539        cx: &Cx,
540        value: &FormValue,
541        stored: &FormValue,
542        ex: &mut dyn toasty::Executor,
543    ) -> Option<FormValue> {
544        match &self.control {
545            ControlKind::Choice(choice) => choice.keep_unseen(cx, value, stored, ex).await,
546            _ => None,
547        }
548    }
549}
550
551#[cfg(test)]
552mod test_support {
553    pub(super) use crate::test_support::DummyUser;
554}
555
556#[cfg(test)]
557mod tests;