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