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