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 super::{
5    super::{IntoSchema, OptionSource, Schema},
6    ChoiceControl, ControlKind, Field, TextControl, choice,
7};
8
9/// `label` and `name`, shared by every builder.
10macro_rules! common_modifiers {
11    ($builder:ident) => {
12        impl $builder {
13            pub fn label(mut self, label: impl Into<String>) -> Self {
14                self.0.label = Some(label.into());
15                self
16            }
17
18            pub fn name(&self) -> &str {
19                self.0.name()
20            }
21        }
22
23        impl From<$builder> for Field {
24            fn from(builder: $builder) -> Field {
25                builder.0
26            }
27        }
28
29        impl IntoSchema for $builder {
30            fn into_schema(self) -> Schema {
31                self.0.into_schema()
32            }
33        }
34    };
35}
36
37/// Renders the control required, as a panel does for a record-form field with no blank answer.
38#[cfg(test)]
39macro_rules! required_for_tests {
40    ($($builder:ident),*) => {$(
41        impl $builder {
42            pub(crate) fn required(mut self) -> Self {
43                self.0.set_required(true);
44                self
45            }
46        }
47    )*};
48}
49
50#[cfg(test)]
51required_for_tests!(TextField, FileField);
52
53/// A text field: [`Field::text`].
54pub struct TextField(pub(super) Field);
55
56/// A choice field: [`Field::choice`].
57pub struct ChoiceField(pub(super) Field);
58
59/// A file field: [`Field::file`].
60pub struct FileField(pub(super) Field);
61
62/// A field an app's [`Control`](super::Control) renders: [`Field::custom`]
63/// and [`Field::toggle`].
64pub struct CustomField(pub(super) Field);
65
66common_modifiers!(TextField);
67common_modifiers!(ChoiceField);
68common_modifiers!(FileField);
69common_modifiers!(CustomField);
70
71impl TextField {
72    fn text(&mut self) -> &mut TextControl {
73        match &mut self.0.control {
74            ControlKind::Text(text) => text,
75            _ => unreachable!("a TextField holds a text control"),
76        }
77    }
78
79    /// Validate the value as an email address, and render `type="email"`.
80    pub fn email(mut self) -> Self {
81        self.text().email = true;
82        self
83    }
84
85    /// Probes a unique index before the write.
86    pub fn unique(mut self) -> Self {
87        self.text().unique = true;
88        self
89    }
90
91    /// The control's placeholder text.
92    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
93        self.text().placeholder = Some(placeholder.into());
94        self
95    }
96
97    /// Render a `<textarea>` of `rows` lines rather than a one-line input,
98    /// for a column holding prose.
99    pub fn multiline(mut self, rows: u32) -> Self {
100        self.text().rows = Some(rows);
101        self
102    }
103}
104
105impl ChoiceField {
106    fn choice(&mut self) -> &mut ChoiceControl {
107        match &mut self.0.control {
108            ControlKind::Choice(choice) => choice,
109            _ => unreachable!("a ChoiceField holds a choice control"),
110        }
111    }
112
113    /// Static options: values that are their own label
114    /// (`["draft", "published"]`), `(value, label)` pairs, or a
115    /// [`#[derive(Options)]`](crate::Options) enum's
116    /// [`options()`](crate::schema::Options::options).
117    pub fn options(mut self, options: impl IntoOptions) -> Self {
118        self.choice().options = options.into_options();
119        self
120    }
121
122    /// Filters options as the user types, fetching from the relationship past the option cap.
123    pub fn searchable(mut self) -> Self {
124        self.choice().searchable = true;
125        self
126    }
127
128    /// Loads options from a related source's tenant-scoped query, each valued by its primary key
129    /// and labelled by `label`, degrading to type-to-search past the option cap.
130    pub fn relationship<R>(
131        mut self,
132        label: impl Fn(&R::Model) -> String + Send + Sync + 'static,
133    ) -> Self
134    where
135        R: OptionSource + 'static,
136    {
137        self.choice().relationship = Some(choice::Relationship::new::<R>(label));
138        self
139    }
140}
141
142/// A choice's static options as `(value, label)` pairs, in display order.
143pub trait IntoOptions {
144    fn into_options(self) -> Vec<(String, String)>;
145}
146
147impl IntoOptions for Vec<String> {
148    fn into_options(self) -> Vec<(String, String)> {
149        self.into_iter()
150            .map(|value| (value.clone(), value))
151            .collect()
152    }
153}
154
155impl IntoOptions for Vec<(String, String)> {
156    fn into_options(self) -> Vec<(String, String)> {
157        self
158    }
159}
160
161impl<const N: usize> IntoOptions for [&str; N] {
162    fn into_options(self) -> Vec<(String, String)> {
163        self.into_iter()
164            .map(|value| (value.to_string(), value.to_string()))
165            .collect()
166    }
167}
168
169/// The crate's tests read a builder's field as the schema will hold it.
170#[cfg(test)]
171mod test_deref {
172    use super::{ChoiceField, CustomField, Field, FileField, TextField};
173
174    macro_rules! deref_field {
175        ($($builder:ident),*) => {$(
176            impl std::ops::Deref for $builder {
177                type Target = Field;
178                fn deref(&self) -> &Field {
179                    &self.0
180                }
181            }
182        )*};
183    }
184
185    deref_field!(TextField, ChoiceField, FileField, CustomField);
186}