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