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}