Skip to main content

tablo_core/schema/fields/
builders.rs

1//! The typed builders the [`Field`] constructors return, each offering only its control's modifiers
2//! so a modifier on the wrong control does not compile.
3
4use std::{marker::PhantomData, sync::Arc};
5
6use super::{
7    super::{
8        IntoSchema, OptionSource, Retype, Schema,
9        condition::{Condition, Watched},
10    },
11    ChoiceControl, Control, ControlKind, Field, TextControl, choice,
12};
13use crate::form::{FormScalar, FormValue};
14
15/// `label`, `name` and `visible_when`, shared by every builder.
16macro_rules! base_modifiers {
17    ($builder:ident) => {
18        impl $builder {
19            pub(super) fn new(field: Field) -> Self {
20                Self(field, PhantomData)
21            }
22        }
23
24        impl<F> $builder<F> {
25            pub fn label(mut self, label: impl Into<String>) -> Self {
26                self.0.label = Some(label.into());
27                self
28            }
29
30            pub fn name(&self) -> &str {
31                self.0.name()
32            }
33
34            /// Shows the field only while `watched` posts one of `values`: an option's value, or
35            /// `true` for a checked toggle. A hidden field posts nothing, so an edit keeps its
36            /// stored value and a create takes its blank answer; a field with no blank answer
37            /// cannot be conditional.
38            ///
39            /// ```rust
40            /// # #[derive(Debug, Clone, toasty::Model)]
41            /// # struct Customer {
42            /// #     #[key] #[auto] id: uuid::Uuid,
43            /// #     kind: String,
44            /// #     vat_number: Option<String>,
45            /// # }
46            /// # use tablo_core::{Field, Schema};
47            /// let kind = Field::choice(Customer::fields().kind()).options(["person", "company"]);
48            /// let vat = Field::text(Customer::fields().vat_number()).visible_when(&kind, ["company"]);
49            /// Schema::new((kind, vat));
50            /// ```
51            pub fn visible_when(
52                mut self,
53                watched: &impl Watched<F>,
54                values: impl IntoIterator<Item = impl Into<String>>,
55            ) -> Self {
56                self.0.condition = Some(Condition::new(watched, values));
57                self
58            }
59        }
60
61        impl<F> From<$builder<F>> for Field {
62            fn from(builder: $builder<F>) -> Field {
63                builder.0
64            }
65        }
66
67        impl<F> IntoSchema<F> for $builder<F> {
68            fn into_schema(self) -> Schema<F> {
69                self.0.into_schema().retype()
70            }
71        }
72
73        impl<F> Retype for $builder<F> {
74            type As<G> = $builder<G>;
75
76            fn retype<G>(self) -> $builder<G> {
77                $builder(self.0, PhantomData)
78            }
79        }
80    };
81}
82
83/// The base modifiers, `help`, `disabled` and `custom`, on every builder of one value's control.
84macro_rules! common_modifiers {
85    ($builder:ident) => {
86        base_modifiers!($builder);
87
88        impl<F> $builder<F> {
89            /// Renders `text` under the control, which the control names as its description.
90            pub fn help(mut self, text: impl Into<String>) -> Self {
91                self.0.help = Some(text.into());
92                self
93            }
94
95            /// Renders the control disabled on the create and edit forms. The server drops a
96            /// value posted for it anyway, so an edit keeps the stored value and a create takes
97            /// the field's `default`, else its blank answer; a field with neither cannot be
98            /// created, and its panel refuses to mount.
99            pub fn disabled(mut self) -> Self {
100                self.0.disabled = super::Disabled::Always;
101                self
102            }
103
104            /// Renders the control disabled on the edit form only: the create form sets the
105            /// value, and no edit changes it.
106            pub fn disabled_on_edit(mut self) -> Self {
107                self.0.disabled = super::Disabled::OnEdit;
108                self
109            }
110
111            /// Renders the field with an app's [`Control`] instead, as [`Field::custom`] does:
112            /// how a record form's control takes a custom input. The field keeps its key and
113            /// label; the replaced control's own modifiers, such as `email`, `unique` or
114            /// `options`, are dropped with it.
115            pub fn custom(mut self, control: impl Control + 'static) -> CustomField<F> {
116                self.0.control = ControlKind::Custom(Arc::new(control));
117                self.0.checkbox = false;
118                CustomField(self.0, PhantomData)
119            }
120        }
121    };
122}
123
124/// Renders the control required, as a panel does for a record-form field with no blank answer.
125#[cfg(test)]
126macro_rules! required_for_tests {
127    ($($builder:ident),*) => {$(
128        impl<F> $builder<F> {
129            pub(crate) fn required(mut self) -> Self {
130                self.0.set_required(true);
131                self
132            }
133        }
134    )*};
135}
136
137#[cfg(test)]
138required_for_tests!(TextField, FileField);
139
140/// `default`, on every builder whose control a value fills.
141macro_rules! default_modifier {
142    ($($builder:ident),*) => {$(
143        impl<F> $builder<F> {
144            /// Fills the create form's control with `value`: any [`FormScalar`] or a `&str` in
145            /// the form spelling, or, for a [`multiple`](ChoiceField::multiple) choice, an array
146            /// or `Vec` of them. On a [`disabled`](Self::disabled) field it is the value a create
147            /// stores. A schema refuses a default the control never posts: one a typed field
148            /// does not parse, one not among a choice's options, a list on a control posting one
149            /// value, and one value on a multiple choice.
150            ///
151            /// ```rust
152            /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
153            /// # struct Post { #[key] #[auto] id: uuid::Uuid, status: String, views: i64 }
154            /// # use tablo_core::Field;
155            /// Field::choice(Post::fields().status())
156            ///     .options(["draft", "published"])
157            ///     .default("draft");
158            /// Field::text(Post::fields().views()).default(0_i64).disabled();
159            /// Field::choice_input("channels")
160            ///     .options(["email", "sms"])
161            ///     .multiple()
162            ///     .default(["email"]);
163            /// ```
164            pub fn default(mut self, value: impl IntoFormValue) -> Self {
165                self.0.default = Some(value.into_form_value());
166                self
167            }
168        }
169    )*};
170}
171
172default_modifier!(TextField, ChoiceField, CustomField);
173
174/// A value a field's `default` takes: any [`FormScalar`] or a `&str` or `&String` in the form
175/// spelling, or an array or `Vec` of them for a multiple choice.
176pub trait IntoFormValue {
177    /// The value as its control posts it.
178    fn into_form_value(self) -> FormValue;
179}
180
181impl<T: FormScalar> IntoFormValue for T {
182    fn into_form_value(self) -> FormValue {
183        FormValue::Text(self.to_form())
184    }
185}
186
187impl IntoFormValue for &str {
188    fn into_form_value(self) -> FormValue {
189        FormValue::Text(self.to_string())
190    }
191}
192
193impl IntoFormValue for &String {
194    fn into_form_value(self) -> FormValue {
195        FormValue::Text(self.clone())
196    }
197}
198
199impl<T: FormScalar> IntoFormValue for Vec<T> {
200    fn into_form_value(self) -> FormValue {
201        FormValue::List(self.iter().map(FormScalar::to_form).collect())
202    }
203}
204
205impl IntoFormValue for Vec<&str> {
206    fn into_form_value(self) -> FormValue {
207        FormValue::List(self.into_iter().map(str::to_string).collect())
208    }
209}
210
211impl<T: FormScalar, const N: usize> IntoFormValue for [T; N] {
212    fn into_form_value(self) -> FormValue {
213        Vec::from(self).into_form_value()
214    }
215}
216
217impl<const N: usize> IntoFormValue for [&str; N] {
218    fn into_form_value(self) -> FormValue {
219        Vec::from(self).into_form_value()
220    }
221}
222
223/// A text field: [`Field::text`], or a record form's text control (`F` is the form).
224pub struct TextField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
225
226/// A choice field: [`Field::choice`], or a record form's choice control (`F` is the form).
227pub struct ChoiceField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
228
229/// A file field: [`Field::file`], or a record form's file control (`F` is the form).
230pub struct FileField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
231
232/// A repeater: [`Field::repeater`], or a record form's `#[form(repeat)]` control (`F` is the
233/// form).
234pub struct RepeaterField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
235
236/// A field an app's [`Control`] renders: [`Field::custom`] and [`Field::toggle`], or a record
237/// form's toggle (`F` is the form).
238pub struct CustomField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
239
240impl<F> CustomField<F> {
241    /// Whether the control is the built-in checkbox, which a condition follows through `checked`.
242    pub(crate) fn is_checkbox(&self) -> bool {
243        self.0.is_checkbox()
244    }
245}
246
247common_modifiers!(TextField);
248common_modifiers!(ChoiceField);
249common_modifiers!(FileField);
250common_modifiers!(CustomField);
251base_modifiers!(RepeaterField);
252
253/// `choice`, on every builder but the choice's own.
254macro_rules! to_choice {
255    ($($builder:ident),*) => {$(
256        impl<F> $builder<F> {
257            /// Renders the field as a choice instead, as [`Field::choice`] does, ready for
258            /// [`options`](ChoiceField::options) or [`relationship`](ChoiceField::relationship).
259            /// The replaced control's own modifiers are dropped with it.
260            pub fn choice(mut self) -> ChoiceField<F> {
261                self.0.control = ControlKind::Choice(ChoiceControl::default());
262                ChoiceField(self.0, PhantomData)
263            }
264        }
265    )*};
266}
267
268to_choice!(TextField, FileField, CustomField);
269
270impl<F> TextField<F> {
271    fn text(&mut self) -> &mut TextControl {
272        match &mut self.0.control {
273            ControlKind::Text(text) => text,
274            _ => unreachable!("a TextField holds a text control"),
275        }
276    }
277
278    /// Validate the value as an email address, and render `type="email"`.
279    pub fn email(mut self) -> Self {
280        self.text().email = true;
281        self
282    }
283
284    /// Render `type="password"`: the browser masks the value, and a refused form renders the
285    /// control empty rather than echoing the password back into the page.
286    pub fn password(mut self) -> Self {
287        self.text().password = true;
288        self
289    }
290
291    /// Probes a unique index before the write.
292    pub fn unique(mut self) -> Self {
293        self.text().unique = true;
294        self
295    }
296
297    /// The control's placeholder text.
298    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
299        self.text().placeholder = Some(placeholder.into());
300        self
301    }
302
303    /// Render a `<textarea>` of `rows` lines rather than a one-line input,
304    /// for a column holding prose.
305    pub fn multiline(mut self, rows: u32) -> Self {
306        self.text().rows = Some(rows);
307        self
308    }
309}
310
311impl<F> ChoiceField<F> {
312    fn choice_mut(&mut self) -> &mut ChoiceControl {
313        match &mut self.0.control {
314            ControlKind::Choice(choice) => choice,
315            _ => unreachable!("a ChoiceField holds a choice control"),
316        }
317    }
318
319    /// Static options: values that are their own label
320    /// (`["draft", "published"]`), `(value, label)` pairs, or a
321    /// [`#[derive(Options)]`](crate::Options) enum's
322    /// [`options()`](crate::schema::Options::options).
323    pub fn options(mut self, options: impl IntoOptions) -> Self {
324        self.choice_mut().options = options.into_options();
325        self
326    }
327
328    /// Takes several options: a checkbox per option, posting the field's key once per checked
329    /// box. A record form's many-to-many field (`Vec<K>` with `#[form(relationship = R)]`) is one;
330    /// [`parse_list`](crate::form::parse_list) reads one in an action's input.
331    ///
332    /// It offers at most [`MAX_RELATIONSHIP_OPTIONS`](crate::schema::MAX_RELATIONSHIP_OPTIONS)
333    /// records: past them it lists none but the chosen ones, and refuses a submission choosing
334    /// another. A record form's edit re-checks only the keys the record does not link yet.
335    /// `searchable` filters the listed options as the user types. A condition cannot watch it,
336    /// it cannot depend on another field, and its `default` is a list.
337    pub fn multiple(mut self) -> Self {
338        self.choice_mut().multiple = true;
339        self
340    }
341
342    /// Filters options as the user types, fetching from the relationship past the option cap.
343    pub fn searchable(mut self) -> Self {
344        self.choice_mut().searchable = true;
345        self
346    }
347
348    /// Loads options from a related source's tenant-scoped query, each valued by its primary key
349    /// and labelled by the source's [`label`](OptionSource::label), degrading to type-to-search
350    /// past the option cap.
351    ///
352    /// A resource's options carry its [`record_title`](crate::ResourceDef::record_title), the
353    /// title its detail page shows. [`relationship_labelled`](Self::relationship_labelled)
354    /// labels one field's options otherwise.
355    pub fn relationship<R>(mut self) -> Self
356    where
357        R: OptionSource + 'static,
358    {
359        self.choice_mut().relationship = Some(choice::Relationship::new::<R>(R::label));
360        self
361    }
362
363    /// Offers only the related rows whose column `column` equals the value `parent` posts: the
364    /// cities of the chosen country. The browser fetches the options again when `parent` changes,
365    /// and a submission must name a row of the posted parent value. A blank parent offers none.
366    ///
367    /// `column` belongs to the [`relationship`](Self::relationship)'s source model; mounting
368    /// refuses another model, a choice without a relationship, and a `parent` the schema does not
369    /// place.
370    pub fn depends_on<M, T>(
371        mut self,
372        parent: &impl Watched<F>,
373        column: impl Into<toasty::stmt::Path<M, T>>,
374    ) -> Self
375    where
376        M: Send + Sync + 'static,
377        T: FormScalar + toasty::stmt::IntoExpr<T> + Send + Sync + 'static,
378    {
379        let column: toasty::stmt::Path<M, T> = column.into();
380        self.choice_mut().parent = Some(choice::Parent {
381            watched: parent.key().to_string(),
382            model: std::any::TypeId::of::<M>(),
383            scope: Arc::new(move |value: &str| {
384                T::parse_form(value)
385                    .ok()
386                    .map(|value| column.clone().eq(value))
387            }),
388        });
389        self
390    }
391
392    /// Loads options as [`relationship`](Self::relationship) does, labelling each by `label`
393    /// instead of the source's label: to tell apart records that share a title. The option's
394    /// record loads without its relations.
395    pub fn relationship_labelled<R>(
396        mut self,
397        label: impl Fn(&R::Model) -> String + Send + Sync + 'static,
398    ) -> Self
399    where
400        R: OptionSource + 'static,
401    {
402        self.choice_mut().relationship = Some(choice::Relationship::new::<R>(
403            move |_cx: &topcoat::context::Cx, record: &R::Model| label(record),
404        ));
405        self
406    }
407}
408
409impl<F> RepeaterField<F> {
410    /// The add button's label, over "Add item".
411    pub fn add_label(mut self, label: impl Into<String>) -> Self {
412        match &mut self.0.control {
413            ControlKind::Repeater(repeater) => repeater.add_label = Some(label.into()),
414            _ => unreachable!("a RepeaterField holds a repeater control"),
415        }
416        self
417    }
418}
419
420/// A choice's static options as `(value, label)` pairs, in display order.
421pub trait IntoOptions {
422    fn into_options(self) -> Vec<(String, String)>;
423}
424
425impl IntoOptions for Vec<String> {
426    fn into_options(self) -> Vec<(String, String)> {
427        self.into_iter()
428            .map(|value| (value.clone(), value))
429            .collect()
430    }
431}
432
433impl IntoOptions for Vec<(String, String)> {
434    fn into_options(self) -> Vec<(String, String)> {
435        self
436    }
437}
438
439impl<const N: usize> IntoOptions for [&str; N] {
440    fn into_options(self) -> Vec<(String, String)> {
441        self.into_iter()
442            .map(|value| (value.to_string(), value.to_string()))
443            .collect()
444    }
445}
446
447/// The crate's tests read a builder's field as the schema will hold it.
448#[cfg(test)]
449mod test_deref {
450    use super::{ChoiceField, CustomField, Field, FileField, RepeaterField, TextField};
451
452    macro_rules! deref_field {
453        ($($builder:ident),*) => {$(
454            impl<F> std::ops::Deref for $builder<F> {
455                type Target = Field;
456                fn deref(&self) -> &Field {
457                    &self.0
458                }
459            }
460        )*};
461    }
462
463    deref_field!(
464        TextField,
465        ChoiceField,
466        FileField,
467        CustomField,
468        RepeaterField
469    );
470}