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}