Skip to main content

tablo_core/
schema.rs

1//! Unified Schema primitive — fields and layout blocks that compose via `view!`.
2//!
3//! `Schema` holds layout blocks, embedded values, and [`Field`] slots and resolves every field once
4//! into one list for rendering and validation.
5
6mod condition;
7pub(crate) mod embedded;
8pub(crate) mod fields;
9mod layouts;
10mod lenses;
11mod options;
12mod read;
13pub(crate) mod relationship;
14pub(crate) mod repeater;
15pub(crate) mod tree;
16pub(crate) mod validation;
17
18use std::{collections::HashSet, marker::PhantomData};
19
20pub use condition::Watched;
21pub use embedded::EmbeddedForm;
22use fields::ChoiceControl;
23pub use fields::{
24    ChoiceField, CustomField, Field, FileField, IntoFormValue, IntoOptions, RepeaterField,
25    TextField, Toggle,
26};
27pub(crate) use fields::{option_view, read_only, stored_upload, value_cell};
28pub use layouts::{Grid, Group, Section};
29pub(crate) use lenses::Binding;
30pub use lenses::{FieldResolver, form_key};
31pub use options::Options;
32pub use relationship::MAX_RELATIONSHIP_OPTIONS;
33pub(crate) use relationship::{OptionLoadError, OptionSource};
34pub use repeater::{MAX_ROWS, RepeaterItem, parse_items, write_items};
35use topcoat::{Result, context::Cx, view::*};
36pub use tree::{IntoSchema, Source};
37pub(crate) use tree::{Node, Retype, render_nodes};
38use tree::{bind_nodes, unbound_values};
39pub(crate) use validation::TypedValue;
40
41use crate::form::{FieldErrors, FormValue, FormValues};
42
43/// Composes fields and layout blocks and resolves every field once into one list.
44///
45/// `F` is the record form whose controls the schema places. A resource's form is a
46/// `Schema<UserForm>`, which only `UserForm::controls()` fill, so a control from another form or a
47/// field built with [`Field::text`] fails to compile there. A page's or an action's schema is a
48/// plain `Schema`, which [`Field`]'s constructors fill.
49pub struct Schema<F = ()> {
50    pub(crate) nodes: Vec<Node>,
51    pub(crate) fields: Vec<Field>,
52    form: PhantomData<fn() -> F>,
53}
54
55impl<F> Default for Schema<F> {
56    fn default() -> Self {
57        Self::from_parts(Vec::new(), Vec::new())
58    }
59}
60
61impl<F> std::fmt::Debug for Schema<F> {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("Schema")
64            .field("nodes", &self.nodes)
65            .field("fields", &self.fields)
66            .finish()
67    }
68}
69
70impl Schema {
71    /// A schema with no field: a page's or an action's that renders nothing. A resource's form
72    /// that places nothing is `Schema::default()`, which renders every control.
73    pub fn empty() -> Self {
74        Self::default()
75    }
76}
77
78impl<F> Retype for Schema<F> {
79    type As<G> = Schema<G>;
80
81    fn retype<G>(self) -> Schema<G> {
82        Schema::from_parts(self.nodes, self.fields)
83    }
84}
85
86impl<F> Schema<F> {
87    pub(crate) fn from_parts(nodes: Vec<Node>, fields: Vec<Field>) -> Self {
88        Self {
89            nodes,
90            fields,
91            form: PhantomData,
92        }
93    }
94
95    /// Reports whether this schema declares nothing to render.
96    pub fn is_empty(&self) -> bool {
97        self.nodes.is_empty()
98    }
99
100    /// Builds a `Schema` from any `IntoSchema` and reports duplicate field names as declaration
101    /// errors.
102    pub fn new(children: impl IntoSchema<F>) -> Self {
103        children.into_schema()
104    }
105
106    /// Every field, in the order it joins the schema: a layout's fields as it composes, in
107    /// declaration order, and an embedded value's fields when it binds.
108    pub fn fields(&self) -> impl Iterator<Item = &Field> {
109        self.fields.iter()
110    }
111
112    /// Binds the schema's embedded values and embedded paths to `db`'s app schema.
113    ///
114    /// A panel binds the schemas it mounts; bind one a custom page renders before rendering it.
115    /// A schema with no embedded value or path is bound from the start.
116    pub fn bind(mut self, db: &toasty::Db) -> Self {
117        self.bind_with(&FieldResolver::of_db(db));
118        self
119    }
120
121    /// Binds the schema through `resolver`: builds each unbound embedded value in place, then
122    /// binds every field's path.
123    pub(crate) fn bind_with(&mut self, resolver: &FieldResolver) {
124        bind_nodes(&mut self.nodes, &mut self.fields, resolver);
125        for field in &self.fields {
126            field.bind(resolver);
127        }
128    }
129
130    /// Renders each control required exactly when `required` names its key.
131    pub(crate) fn require(&mut self, required: &HashSet<&str>) {
132        for field in &mut self.fields {
133            let key = required.contains(field.name());
134            field.set_required(key);
135        }
136    }
137
138    /// Renders the schema from `source` and fails with declaration errors instead of rendering.
139    ///
140    /// # Errors
141    ///
142    /// A misdeclared schema fails with its errors rather than render.
143    pub async fn render<'a>(&self, cx: &'a Cx, source: Source<'_>) -> Result<BoxView<'a>> {
144        let errors = self.declaration_errors();
145        if !errors.is_empty() {
146            return Err(crate::error::misdeclared(&errors));
147        }
148        let source = source.watching(self.watched());
149        render_nodes(cx, &self.nodes, &self.fields, &source).await
150    }
151
152    /// Appends `other`'s nodes and fields after this one's, re-slotting its field slots.
153    pub(crate) fn append(&mut self, other: Schema<F>) {
154        let Schema {
155            mut nodes, fields, ..
156        } = other;
157        let offset = self.fields.len();
158        for node in &mut nodes {
159            node.offset(offset);
160        }
161        self.fields.extend(fields);
162        self.nodes.extend(nodes);
163    }
164
165    /// The embedded node a derived value's schema holds.
166    pub(crate) fn embedded_root(&self) -> &embedded::Embedded {
167        match self.nodes.as_slice() {
168            [Node::Embedded(node)] => node,
169            _ => panic!("an embedded value's schema is its one embedded node"),
170        }
171    }
172
173    /// The create form's initial values: each field's `default`.
174    pub(crate) fn defaults(&self) -> FormValues {
175        self.fields
176            .iter()
177            .filter_map(|field| Some((field.name().to_string(), field.default_value()?.clone())))
178            .collect()
179    }
180
181    /// Drops what a disabled control posted, which the browser never sends: on the edit form when
182    /// `editing`, else on the create form, where the field takes its `default`. Run it before
183    /// [`condition_hidden`](Self::condition_hidden), which drops a hidden field's default again.
184    pub(crate) fn drop_disabled(&self, values: &mut FormValues, editing: bool) {
185        for field in self
186            .fields
187            .iter()
188            .filter(|field| field.is_disabled(editing))
189        {
190            let name = field.name();
191            values.remove(name);
192            if let Some(default) = field.default_value()
193                && !editing
194            {
195                values.insert(name, default.clone());
196            }
197        }
198    }
199
200    /// The keys of the controls disabled on the edit form when `editing`, else the create form.
201    pub(crate) fn disabled_keys(&self, editing: bool) -> impl Iterator<Item = &str> {
202        self.fields
203            .iter()
204            .filter(move |field| field.is_disabled(editing))
205            .map(Field::name)
206    }
207
208    /// Appends another schema's nodes after this one's.
209    pub fn extend(mut self, other: Schema<F>) -> Schema<F> {
210        self.append(other);
211        self
212    }
213
214    /// Returns the first key in `errors` that no control renders a message under: neither a
215    /// field's own key nor, for a repeater, a row's key under `{key}.`.
216    pub(crate) fn unplaced_error<'e>(&self, errors: &'e crate::FieldErrors) -> Option<&'e str> {
217        errors.iter().map(|error| error.key.as_str()).find(|key| {
218            !self
219                .fields
220                .iter()
221                .any(|field| match key.strip_prefix(field.name()) {
222                    Some(rest) => {
223                        rest.is_empty() || (rest.starts_with('.') && field.as_repeater().is_some())
224                    }
225                    None => false,
226                })
227        })
228    }
229
230    /// One [`EmptyChoice`](crate::DeclarationErrorKind::EmptyChoice) per choice with neither
231    /// options nor a relationship. Mounting checks a resource's form and its actions' inputs; a
232    /// page's schema may build its options from data that is empty for now, so rendering does
233    /// not.
234    pub(crate) fn empty_choices(&self) -> Vec<crate::DeclarationErrorKind> {
235        self.fields
236            .iter()
237            .filter(|field| field.offers_nothing())
238            .map(|field| crate::DeclarationErrorKind::EmptyChoice {
239                field: field.name().to_string(),
240            })
241            .collect()
242    }
243
244    /// What is wrong with the schema's dependent choices: a column of another model than the
245    /// relationship's source, or no relationship, and a parent the schema does not place, which
246    /// posts nothing to narrow the options by.
247    fn dependent_errors(&self) -> Vec<crate::DeclarationErrorKind> {
248        let mut errors = Vec::new();
249        for field in &self.fields {
250            let Some(choice) = field.as_choice() else {
251                continue;
252            };
253            if choice.misdeclared_parent() {
254                errors.push(crate::DeclarationErrorKind::MisdeclaredDependentChoice {
255                    field: field.name().to_string(),
256                });
257            }
258            let Some(parent) = choice.parent_key() else {
259                continue;
260            };
261            match self.fields.iter().find(|placed| placed.name() == parent) {
262                None => errors.push(crate::DeclarationErrorKind::UnplacedParentField {
263                    field: field.name().to_string(),
264                    parent: parent.to_string(),
265                }),
266                Some(parent)
267                    if choice.is_multiple()
268                        || parent.as_choice().is_some_and(ChoiceControl::is_multiple) =>
269                {
270                    errors.push(crate::DeclarationErrorKind::MultipleDependentChoice {
271                        field: field.name().to_string(),
272                    });
273                }
274                Some(_) => {}
275            }
276        }
277        errors
278    }
279
280    /// Reports what is wrong with this declaration: an embedded value or path never bound, a field
281    /// whose lens binds no single column, and two fields sharing a name.
282    pub fn declaration_errors(&self) -> Vec<crate::DeclarationErrorKind> {
283        let mut unbound = Vec::new();
284        unbound_values(&self.nodes, &mut unbound);
285        let mut errors: Vec<_> = unbound
286            .into_iter()
287            .map(|item| crate::DeclarationErrorKind::Unbound { item })
288            .collect();
289        errors.extend(self.condition_errors());
290        errors.extend(self.dependent_errors());
291        // A control disabled on create posts nothing there, so the create needs a value.
292        for field in &self.fields {
293            let blank = field.default_value().is_none_or(|value| match value {
294                FormValue::Text(text) => text.trim().is_empty(),
295                FormValue::List(items) => items.is_empty(),
296                FormValue::Rows(rows) => rows.is_empty(),
297            });
298            if field.is_always_disabled() && field.is_required() && blank {
299                errors.push(crate::DeclarationErrorKind::DisabledWithoutValue {
300                    field: field.name().to_string(),
301                });
302            }
303            if let Some(value) = field.default_value()
304                && field.refuses(value)
305            {
306                errors.push(crate::DeclarationErrorKind::UnpostedDefault {
307                    field: field.name().to_string(),
308                    value: match value {
309                        FormValue::Text(text) => text.clone(),
310                        FormValue::List(items) => format!("{items:?}"),
311                        FormValue::Rows(_) => "rows".to_string(),
312                    },
313                });
314            }
315            // A row's controls post under the repeater's key, which neither the disabled drop nor
316            // the defaults read.
317            if let Some(repeater) = field.as_repeater()
318                && let Some(leaf) = repeater
319                    .item_schema()
320                    .fields()
321                    .find(|leaf| leaf.is_disabled(true) || leaf.default_value().is_some())
322            {
323                errors.push(crate::DeclarationErrorKind::RepeaterLeafModifier {
324                    field: field.name().to_string(),
325                    leaf: leaf.name().to_string(),
326                });
327            }
328        }
329        let mut seen = HashSet::new();
330        for field in &self.fields {
331            match field.misdeclared() {
332                Some(error) => errors.push(error),
333                None if !seen.insert(field.name()) => {
334                    errors.push(crate::DeclarationErrorKind::DuplicateField {
335                        name: field.name().to_string(),
336                    });
337                }
338                None => {}
339            }
340        }
341        errors
342    }
343
344    /// Collects the keys of the fields this submission hides: the payload of every embedded
345    /// variant it does not choose, and every field a condition hides.
346    pub(crate) fn hidden_fields(&self, values: &FormValues) -> HashSet<String> {
347        fn walk(nodes: &[Node], values: &FormValues, out: &mut Vec<usize>) {
348            for node in nodes {
349                match node {
350                    Node::Embedded(embedded) => embedded.hidden_fields(values, out),
351                    node => {
352                        if let Some(children) = node.children() {
353                            walk(children, values, out);
354                        }
355                    }
356                }
357            }
358        }
359        let mut indices = Vec::new();
360        walk(&self.nodes, values, &mut indices);
361        indices
362            .into_iter()
363            .map(|index| self.fields[index].name().to_string())
364            .chain(self.condition_hidden(values))
365            .collect()
366    }
367
368    /// Adds to `errors` what the controls' own rules refuse in a submission: an email field's
369    /// address, and a choice that is not one of its options. Skips hidden fields and keys
370    /// `errors` already refuses.
371    pub(crate) async fn check_controls(
372        &self,
373        cx: &Cx,
374        values: &FormValues,
375        stored: &FormValues,
376        errors: &mut FieldErrors,
377    ) {
378        let hidden = self.hidden_fields(values);
379        for field in &self.fields {
380            let name = field.name();
381            if errors.contains_key(name) || hidden.contains(name) {
382                continue;
383            }
384            let Some(value) = values.get(name) else {
385                continue;
386            };
387            if let Some(repeater) = field.as_repeater() {
388                let rows = value.as_rows().unwrap_or_default();
389                repeater.check(cx, name, rows, errors).await;
390                continue;
391            }
392            if let Some(error) = field.check(value) {
393                errors.push(error);
394                continue;
395            }
396            let parent = field.parent_key().and_then(|key| values.text(key));
397            for message in field
398                .validate_exists(cx, value, parent, stored.get(name))
399                .await
400            {
401                errors.add(name, message);
402            }
403        }
404    }
405
406    /// What the controls' own rules refuse in `values`, alone.
407    #[cfg(test)]
408    pub(crate) async fn checked(&self, cx: &Cx, values: &FormValues) -> FieldErrors {
409        let mut errors = FieldErrors::new();
410        self.check_controls(cx, values, &FormValues::new(), &mut errors)
411            .await;
412        errors
413    }
414
415    /// Re-checks every submitted relationship key through the write's open transaction, but a
416    /// multiple choice's keys the record's stored value in `stored` holds already.
417    pub(crate) async fn recheck_relationships(
418        &self,
419        cx: &Cx,
420        values: &FormValues,
421        stored: &FormValues,
422        ex: &mut dyn toasty::Executor,
423    ) -> FieldErrors {
424        let mut errors = FieldErrors::new();
425        let hidden = self.hidden_fields(values);
426        for field in &self.fields {
427            let name = field.name();
428            if hidden.contains(name) {
429                continue;
430            }
431            let Some(value) = values.get(name) else {
432                continue;
433            };
434            let parent = field.parent_key().and_then(|key| values.text(key));
435            for message in field
436                .recheck(cx, value, parent, stored.get(name), &mut *ex)
437                .await
438            {
439                errors.add(name, message);
440            }
441        }
442        errors
443    }
444
445    /// Puts back in each multiple choice's list the keys of the record's stored list `stored` it
446    /// drops that the user cannot view, through the write's open transaction: the form never
447    /// offered them, so its submission leaves them linked.
448    pub(crate) async fn keep_unseen_links(
449        &self,
450        cx: &Cx,
451        values: &mut FormValues,
452        stored: &FormValues,
453        ex: &mut dyn toasty::Executor,
454    ) {
455        for field in &self.fields {
456            let name = field.name();
457            let (Some(value), Some(held)) = (values.get(name), stored.get(name)) else {
458                continue;
459            };
460            if let Some(kept) = field.keep_unseen(cx, value, held, &mut *ex).await {
461                values.insert(name, kept);
462            }
463        }
464    }
465}
466
467#[cfg(test)]
468mod tests;