Skip to main content

tablo_core/schema/
mod.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//!
6//! `lens_field` reaches into `toasty_core` (upstream issue #114); retire the walk when Toasty
7//! exposes it (upstream #183).
8
9pub(crate) mod embedded;
10mod fields;
11mod layouts;
12mod lenses;
13mod options;
14mod relationship;
15mod tree;
16mod validation;
17
18use std::collections::{HashMap, HashSet};
19
20pub use embedded::EmbeddedForm;
21pub(crate) use fields::option_view;
22pub use fields::{
23    ChoiceField, Control, ControlInput, CustomField, Field, FileField, IntoOptions, TextField,
24    Toggle,
25};
26pub use layouts::{Grid, Group, Section};
27pub(crate) use lenses::{Binding, lens_field, lens_field_unique};
28pub use lenses::{FieldResolver, form_key};
29pub use options::Options;
30pub(crate) use relationship::OptionLoadError;
31pub use relationship::{MAX_RELATIONSHIP_OPTIONS, OptionSource};
32use topcoat::{Result, context::Cx, view::*};
33pub use tree::{IntoSchema, Source};
34pub(crate) use tree::{Node, render_nodes};
35use tree::{bind_nodes, unbound_values};
36pub use validation::TypedValue;
37
38use crate::form::FieldErrors;
39
40/// Composes fields and layout blocks and resolves every field once into one list.
41#[derive(Debug, Default)]
42pub struct Schema {
43    pub(crate) nodes: Vec<Node>,
44    pub(crate) fields: Vec<Field>,
45}
46
47impl Schema {
48    /// Reports whether this schema declares nothing to render.
49    pub fn is_empty(&self) -> bool {
50        self.nodes.is_empty()
51    }
52
53    /// Builds a `Schema` from any `IntoSchema` and reports duplicate field names as declaration
54    /// errors.
55    pub fn new(children: impl IntoSchema) -> Self {
56        children.into_schema()
57    }
58
59    pub fn empty() -> Self {
60        Self::default()
61    }
62
63    /// Every field, in the order it joins the schema: a layout's fields as it composes, in
64    /// declaration order, and an embedded value's fields when it binds.
65    pub fn fields(&self) -> impl Iterator<Item = &Field> {
66        self.fields.iter()
67    }
68
69    /// Binds the schema's embedded values and embedded paths to `db`'s app schema.
70    ///
71    /// A panel binds the schemas it mounts; bind one a custom page renders before rendering it.
72    /// A schema with no embedded value or path is bound from the start.
73    pub fn bind(mut self, db: &toasty::Db) -> Self {
74        self.bind_with(&FieldResolver::of_db(db));
75        self
76    }
77
78    /// Binds the schema through `resolver`: builds each unbound embedded value in place, then
79    /// binds every field's path.
80    pub(crate) fn bind_with(&mut self, resolver: &FieldResolver) {
81        bind_nodes(&mut self.nodes, &mut self.fields, resolver);
82        for field in &self.fields {
83            field.bind(resolver);
84        }
85    }
86
87    /// Renders each control required exactly when `required` names its key.
88    pub(crate) fn require(&mut self, required: &HashSet<&str>) {
89        for field in &mut self.fields {
90            let key = required.contains(field.name());
91            field.set_required(key);
92        }
93    }
94
95    /// Renders the schema from `source` and fails with declaration errors instead of rendering.
96    ///
97    /// # Errors
98    ///
99    /// A misdeclared schema fails with its errors rather than render.
100    pub async fn render<'a>(&self, cx: &'a Cx, source: Source<'_>) -> Result<BoxView<'a>> {
101        let errors = self.declaration_errors();
102        if !errors.is_empty() {
103            return Err(crate::error::misdeclared(&errors));
104        }
105        render_nodes(cx, &self.nodes, &self.fields, &source).await
106    }
107
108    /// Appends `other`'s nodes and fields after this one's, re-slotting its field slots.
109    pub(crate) fn append(&mut self, other: Schema) {
110        let Schema { mut nodes, fields } = other;
111        let offset = self.fields.len();
112        for node in &mut nodes {
113            node.offset(offset);
114        }
115        self.fields.extend(fields);
116        self.nodes.extend(nodes);
117    }
118
119    /// The embedded node a derived value's schema holds.
120    pub(crate) fn embedded_root(&self) -> &embedded::Embedded {
121        match self.nodes.as_slice() {
122            [Node::Embedded(node)] => node,
123            _ => panic!("an embedded value's schema is its one embedded node"),
124        }
125    }
126
127    /// Appends another schema's nodes after this one's.
128    pub fn extend(mut self, other: Schema) -> Schema {
129        self.append(other);
130        self
131    }
132
133    /// Lists keys in `values` that no declared input owns, sorted, so handlers reject
134    /// client-controlled writes.
135    pub(crate) fn unknown_keys(&self, values: &HashMap<String, String>) -> Vec<String> {
136        let known: HashSet<&str> = self.fields.iter().map(Field::name).collect();
137        let mut out: Vec<String> = values
138            .keys()
139            .filter(|k| !known.contains(k.as_str()))
140            .cloned()
141            .collect();
142        out.sort();
143        out
144    }
145
146    /// Reports what is wrong with this declaration: an embedded value or path never bound, a field
147    /// whose lens binds no single column, and two fields sharing a name.
148    pub fn declaration_errors(&self) -> Vec<crate::DeclarationErrorKind> {
149        let mut unbound = Vec::new();
150        unbound_values(&self.nodes, &mut unbound);
151        let mut errors: Vec<_> = unbound
152            .into_iter()
153            .map(|item| crate::DeclarationErrorKind::Unbound { item })
154            .collect();
155        let mut seen = HashSet::new();
156        for field in &self.fields {
157            match field.misdeclared() {
158                Some(error) => errors.push(error),
159                None if !seen.insert(field.name()) => {
160                    errors.push(crate::DeclarationErrorKind::DuplicateField {
161                        name: field.name().to_string(),
162                    });
163                }
164                None => {}
165            }
166        }
167        errors
168    }
169
170    /// Collects the keys of the fields this submission hides: the payload of every embedded
171    /// variant it does not choose.
172    pub(crate) fn hidden_fields(&self, values: &HashMap<String, String>) -> HashSet<String> {
173        fn walk(nodes: &[Node], values: &HashMap<String, String>, out: &mut Vec<usize>) {
174            for node in nodes {
175                match node {
176                    Node::Embedded(embedded) => embedded.hidden_fields(values, out),
177                    node => {
178                        if let Some(children) = node.children() {
179                            walk(children, values, out);
180                        }
181                    }
182                }
183            }
184        }
185        let mut indices = Vec::new();
186        walk(&self.nodes, values, &mut indices);
187        indices
188            .into_iter()
189            .map(|index| self.fields[index].name().to_string())
190            .collect()
191    }
192
193    /// Adds to `errors` what the controls' own rules refuse in a submission: an email field's
194    /// address, and a choice that is not one of its options. Skips hidden fields and keys
195    /// `errors` already refuses.
196    pub(crate) async fn check_controls(
197        &self,
198        cx: &Cx,
199        values: &HashMap<String, String>,
200        errors: &mut FieldErrors,
201    ) {
202        let hidden = self.hidden_fields(values);
203        for field in &self.fields {
204            let name = field.name();
205            if errors.contains_key(name) || hidden.contains(name) {
206                continue;
207            }
208            let Some(value) = values.get(name) else {
209                continue;
210            };
211            if let Some(error) = field.check(value) {
212                errors.push(error);
213                continue;
214            }
215            for message in field.validate_exists(cx, value).await {
216                errors.add(name, message);
217            }
218        }
219    }
220
221    /// What the controls' own rules refuse in `values`, alone.
222    #[cfg(test)]
223    pub(crate) async fn checked(&self, cx: &Cx, values: &HashMap<String, String>) -> FieldErrors {
224        let mut errors = FieldErrors::new();
225        self.check_controls(cx, values, &mut errors).await;
226        errors
227    }
228
229    /// Re-checks every submitted relationship key through the write's open transaction.
230    pub(crate) async fn recheck_relationships(
231        &self,
232        cx: &Cx,
233        values: &HashMap<String, String>,
234        ex: &mut dyn toasty::Executor,
235    ) -> FieldErrors {
236        let mut errors = FieldErrors::new();
237        let hidden = self.hidden_fields(values);
238        for field in &self.fields {
239            let name = field.name();
240            if hidden.contains(name) {
241                continue;
242            }
243            let Some(value) = values.get(name) else {
244                continue;
245            };
246            for message in field.recheck(cx, value, &mut *ex).await {
247                errors.add(name, message);
248            }
249        }
250        errors
251    }
252}
253
254#[cfg(test)]
255mod tests;