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;
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 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 the field with an app's [`Control`] instead, as [`Field::custom`] does:
90            /// how a record form's control takes a custom input. The field keeps its key and
91            /// label; the replaced control's own modifiers, such as `email`, `unique` or
92            /// `options`, are dropped with it.
93            pub fn custom(mut self, control: impl Control + 'static) -> CustomField<F> {
94                self.0.control = ControlKind::Custom(Arc::new(control));
95                self.0.checkbox = false;
96                CustomField(self.0, PhantomData)
97            }
98        }
99    };
100}
101
102/// Renders the control required, as a panel does for a record-form field with no blank answer.
103#[cfg(test)]
104macro_rules! required_for_tests {
105    ($($builder:ident),*) => {$(
106        impl<F> $builder<F> {
107            pub(crate) fn required(mut self) -> Self {
108                self.0.set_required(true);
109                self
110            }
111        }
112    )*};
113}
114
115#[cfg(test)]
116required_for_tests!(TextField, FileField);
117
118/// A text field: [`Field::text`], or a record form's text control (`F` is the form).
119pub struct TextField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
120
121/// A choice field: [`Field::choice`], or a record form's choice control (`F` is the form).
122pub struct ChoiceField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
123
124/// A file field: [`Field::file`], or a record form's file control (`F` is the form).
125pub struct FileField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
126
127/// A repeater: [`Field::repeater`], or a record form's `#[form(repeat)]` control (`F` is the
128/// form).
129pub struct RepeaterField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
130
131/// A field an app's [`Control`] renders: [`Field::custom`] and [`Field::toggle`], or a record
132/// form's toggle (`F` is the form).
133pub struct CustomField<F = ()>(pub(super) Field, PhantomData<fn() -> F>);
134
135impl<F> CustomField<F> {
136    /// Whether the control is the built-in checkbox, which a condition follows through `checked`.
137    pub(crate) fn is_checkbox(&self) -> bool {
138        self.0.is_checkbox()
139    }
140}
141
142common_modifiers!(TextField);
143common_modifiers!(ChoiceField);
144common_modifiers!(FileField);
145common_modifiers!(CustomField);
146base_modifiers!(RepeaterField);
147
148/// `choice`, on every builder but the choice's own.
149macro_rules! to_choice {
150    ($($builder:ident),*) => {$(
151        impl<F> $builder<F> {
152            /// Renders the field as a choice instead, as [`Field::choice`] does, ready for
153            /// [`options`](ChoiceField::options) or [`relationship`](ChoiceField::relationship).
154            /// Unlike `#[form(relationship = ..)]`, it leaves the record form's derived table and
155            /// detail page as they are. The replaced control's own modifiers are dropped with it.
156            pub fn choice(mut self) -> ChoiceField<F> {
157                self.0.control = ControlKind::Choice(ChoiceControl::default());
158                ChoiceField(self.0, PhantomData)
159            }
160        }
161    )*};
162}
163
164to_choice!(TextField, FileField, CustomField);
165
166impl<F> TextField<F> {
167    fn text(&mut self) -> &mut TextControl {
168        match &mut self.0.control {
169            ControlKind::Text(text) => text,
170            _ => unreachable!("a TextField holds a text control"),
171        }
172    }
173
174    /// Validate the value as an email address, and render `type="email"`.
175    pub fn email(mut self) -> Self {
176        self.text().email = true;
177        self
178    }
179
180    /// Render `type="password"`: the browser masks the value, and a refused form renders the
181    /// control empty rather than echoing the password back into the page.
182    pub fn password(mut self) -> Self {
183        self.text().password = true;
184        self
185    }
186
187    /// Probes a unique index before the write.
188    pub fn unique(mut self) -> Self {
189        self.text().unique = true;
190        self
191    }
192
193    /// The control's placeholder text.
194    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
195        self.text().placeholder = Some(placeholder.into());
196        self
197    }
198
199    /// Render a `<textarea>` of `rows` lines rather than a one-line input,
200    /// for a column holding prose.
201    pub fn multiline(mut self, rows: u32) -> Self {
202        self.text().rows = Some(rows);
203        self
204    }
205}
206
207impl<F> ChoiceField<F> {
208    fn choice_mut(&mut self) -> &mut ChoiceControl {
209        match &mut self.0.control {
210            ControlKind::Choice(choice) => choice,
211            _ => unreachable!("a ChoiceField holds a choice control"),
212        }
213    }
214
215    /// Static options: values that are their own label
216    /// (`["draft", "published"]`), `(value, label)` pairs, or a
217    /// [`#[derive(Options)]`](crate::Options) enum's
218    /// [`options()`](crate::schema::Options::options).
219    pub fn options(mut self, options: impl IntoOptions) -> Self {
220        self.choice_mut().options = options.into_options();
221        self
222    }
223
224    /// Takes several options: a checkbox per option, posting the field's key once per checked
225    /// box. A record form's many-to-many field (`Vec<K>` with `#[form(relationship = R)]`) is one;
226    /// [`parse_list`](crate::form::parse_list) reads one in an action's input.
227    ///
228    /// It offers at most [`MAX_RELATIONSHIP_OPTIONS`](crate::schema::MAX_RELATIONSHIP_OPTIONS)
229    /// records: past them it lists none but the chosen ones, and refuses a submission choosing
230    /// another. A record form's edit re-checks only the keys the record does not link yet.
231    /// `searchable` filters the listed options as the user types. A condition cannot watch it,
232    /// and it cannot depend on another field.
233    pub fn multiple(mut self) -> Self {
234        self.choice_mut().multiple = true;
235        self
236    }
237
238    /// Filters options as the user types, fetching from the relationship past the option cap.
239    pub fn searchable(mut self) -> Self {
240        self.choice_mut().searchable = true;
241        self
242    }
243
244    /// Loads options from a related source's tenant-scoped query, each valued by its primary key
245    /// and labelled by the source's [`label`](OptionSource::label), degrading to type-to-search
246    /// past the option cap.
247    ///
248    /// A resource's options carry its [`record_title`](crate::ResourceDef::record_title), the
249    /// title its detail page shows. [`relationship_labelled`](Self::relationship_labelled)
250    /// labels one field's options otherwise.
251    pub fn relationship<R>(mut self) -> Self
252    where
253        R: OptionSource + 'static,
254    {
255        self.choice_mut().relationship = Some(choice::Relationship::new::<R>(R::label));
256        self
257    }
258
259    /// Offers only the related rows whose column `column` equals the value `parent` posts: the
260    /// cities of the chosen country. The browser fetches the options again when `parent` changes,
261    /// and a submission must name a row of the posted parent value. A blank parent offers none.
262    ///
263    /// `column` belongs to the [`relationship`](Self::relationship)'s source model; mounting
264    /// refuses another model, a choice without a relationship, and a `parent` the schema does not
265    /// place.
266    pub fn depends_on<M, T>(
267        mut self,
268        parent: &impl Watched<F>,
269        column: impl Into<toasty::stmt::Path<M, T>>,
270    ) -> Self
271    where
272        M: Send + Sync + 'static,
273        T: FormScalar + toasty::stmt::IntoExpr<T> + Send + Sync + 'static,
274    {
275        let column: toasty::stmt::Path<M, T> = column.into();
276        self.choice_mut().parent = Some(choice::Parent {
277            watched: parent.key().to_string(),
278            model: std::any::TypeId::of::<M>(),
279            scope: Arc::new(move |value: &str| {
280                T::parse_form(value)
281                    .ok()
282                    .map(|value| column.clone().eq(value))
283            }),
284        });
285        self
286    }
287
288    /// Loads options as [`relationship`](Self::relationship) does, labelling each by `label`
289    /// instead of the source's label: to tell apart records that share a title. The option's
290    /// record loads without its relations.
291    pub fn relationship_labelled<R>(
292        mut self,
293        label: impl Fn(&R::Model) -> String + Send + Sync + 'static,
294    ) -> Self
295    where
296        R: OptionSource + 'static,
297    {
298        self.choice_mut().relationship = Some(choice::Relationship::new::<R>(
299            move |_cx: &topcoat::context::Cx, record: &R::Model| label(record),
300        ));
301        self
302    }
303}
304
305impl<F> RepeaterField<F> {
306    /// The add button's label, over "Add item".
307    pub fn add_label(mut self, label: impl Into<String>) -> Self {
308        match &mut self.0.control {
309            ControlKind::Repeater(repeater) => repeater.add_label = Some(label.into()),
310            _ => unreachable!("a RepeaterField holds a repeater control"),
311        }
312        self
313    }
314}
315
316/// A choice's static options as `(value, label)` pairs, in display order.
317pub trait IntoOptions {
318    fn into_options(self) -> Vec<(String, String)>;
319}
320
321impl IntoOptions for Vec<String> {
322    fn into_options(self) -> Vec<(String, String)> {
323        self.into_iter()
324            .map(|value| (value.clone(), value))
325            .collect()
326    }
327}
328
329impl IntoOptions for Vec<(String, String)> {
330    fn into_options(self) -> Vec<(String, String)> {
331        self
332    }
333}
334
335impl<const N: usize> IntoOptions for [&str; N] {
336    fn into_options(self) -> Vec<(String, String)> {
337        self.into_iter()
338            .map(|value| (value.to_string(), value.to_string()))
339            .collect()
340    }
341}
342
343/// The crate's tests read a builder's field as the schema will hold it.
344#[cfg(test)]
345mod test_deref {
346    use super::{ChoiceField, CustomField, Field, FileField, RepeaterField, TextField};
347
348    macro_rules! deref_field {
349        ($($builder:ident),*) => {$(
350            impl<F> std::ops::Deref for $builder<F> {
351                type Target = Field;
352                fn deref(&self) -> &Field {
353                    &self.0
354                }
355            }
356        )*};
357    }
358
359    deref_field!(
360        TextField,
361        ChoiceField,
362        FileField,
363        CustomField,
364        RepeaterField
365    );
366}