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