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, Repeater, Section};
27pub(crate) use lenses::{ResolvedLens, declare_with, lens_field, lens_field_unique, schema_of};
28pub use lenses::{declare, 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::{LeafPlace, Node, render_nodes, walk_absent_groups};
35pub use validation::TypedValue;
36pub(crate) use validation::required_error;
37
38use crate::form::FieldErrors;
39
40/// One control and the rule an empty submission meets.
41#[derive(Debug, Clone)]
42pub(crate) struct ControlCheck {
43    /// The key the control posts.
44    pub(crate) name: String,
45    /// Whether an empty submission fails the control's rules.
46    pub(crate) required: bool,
47    /// The message an empty submission produces, when it fails.
48    pub(crate) required_error: Option<String>,
49    /// Whether the control sits inside a `Repeater`.
50    pub(crate) in_repeater: bool,
51    /// Where the control sits in the form.
52    pub(crate) place: LeafPlace,
53}
54
55impl ControlCheck {
56    /// Reports whether an empty submission reaches this control's rule.
57    pub(crate) fn needs_answer(&self) -> bool {
58        match self.place {
59            LeafPlace::Rendered => true,
60            LeafPlace::Payload => self.in_repeater,
61            LeafPlace::Discriminant => false,
62        }
63    }
64}
65
66/// Composes fields and layout blocks and resolves every field once into one list.
67#[derive(Debug, Default)]
68pub struct Schema {
69    pub(crate) nodes: Vec<Node>,
70    pub(crate) fields: Vec<Field>,
71}
72
73impl Schema {
74    /// Reports whether this schema declares nothing to render.
75    pub fn is_empty(&self) -> bool {
76        self.nodes.is_empty()
77    }
78
79    /// Builds a `Schema` from any `IntoSchema` and reports duplicate field names as declaration
80    /// errors.
81    pub fn new(children: impl IntoSchema) -> Self {
82        children.into_schema()
83    }
84
85    pub fn empty() -> Self {
86        Self::default()
87    }
88
89    /// Every field, in declaration order.
90    pub fn fields(&self) -> impl Iterator<Item = &Field> {
91        self.fields.iter()
92    }
93
94    /// Renders the schema from `source` and fails with declaration errors instead of rendering.
95    ///
96    /// # Errors
97    ///
98    /// A misdeclared schema fails with its errors rather than render.
99    pub async fn render<'a>(&self, cx: &'a Cx, source: Source<'_>) -> Result<BoxView<'a>> {
100        let errors = self.declaration_errors();
101        if !errors.is_empty() {
102            return Err(crate::error::misdeclared(&errors));
103        }
104        render_nodes(cx, &self.nodes, &self.fields, &source).await
105    }
106
107    /// Appends `other`'s nodes and fields after this one's, re-slotting its field slots.
108    pub(crate) fn append(&mut self, other: Schema) {
109        let Schema { mut nodes, fields } = other;
110        let offset = self.fields.len();
111        for node in &mut nodes {
112            node.offset(offset);
113        }
114        self.fields.extend(fields);
115        self.nodes.extend(nodes);
116    }
117
118    /// The embedded node a derived value's schema holds.
119    pub(crate) fn embedded_root(&self) -> &embedded::Embedded {
120        match self.nodes.as_slice() {
121            [Node::Embedded(node)] => node,
122            _ => panic!("an embedded value's schema is its one embedded node"),
123        }
124    }
125
126    /// Rewrites submitted values into their fields' stored spelling and leaves empty submissions
127    /// empty for the presence rule to refuse.
128    pub fn normalize_values(&self, values: &mut HashMap<String, String>) {
129        for field in &self.fields {
130            let Some(submitted) = values.get_mut(field.name()) else {
131                continue;
132            };
133            if submitted.trim().is_empty() {
134                continue;
135            }
136            if let Ok(normalized) = field.normalize(submitted) {
137                *submitted = normalized;
138            }
139        }
140    }
141
142    /// Appends another schema's nodes after this one's.
143    pub fn extend(mut self, other: Schema) -> Schema {
144        self.append(other);
145        self
146    }
147
148    /// Collects every field with the rule an empty submission meets and where its control sits.
149    pub(crate) fn controls(&self) -> Vec<ControlCheck> {
150        fn mark(nodes: &[Node], inside: bool, in_repeater: &mut [bool], place: &mut [LeafPlace]) {
151            for node in nodes {
152                match node {
153                    Node::Repeater(r) => mark(&r.children.nodes, true, in_repeater, place),
154                    Node::Field(_) | Node::Embedded(_) => {
155                        node.visit_fields(&mut |index, leaf_place| {
156                            in_repeater[index] = inside;
157                            place[index] = leaf_place;
158                        });
159                    }
160                    _ => mark(
161                        node.children().unwrap_or_default(),
162                        inside,
163                        in_repeater,
164                        place,
165                    ),
166                }
167            }
168        }
169        let mut in_repeater = vec![false; self.fields.len()];
170        let mut place = vec![LeafPlace::Rendered; self.fields.len()];
171        mark(&self.nodes, false, &mut in_repeater, &mut place);
172        self.fields
173            .iter()
174            .zip(in_repeater)
175            .zip(place)
176            .map(|((field, in_repeater), place)| {
177                let errors = field.validate("");
178                ControlCheck {
179                    name: field.name().to_string(),
180                    required: !errors.is_empty(),
181                    required_error: errors
182                        .into_iter()
183                        .next()
184                        .map(|error| error.message)
185                        .or_else(|| Some(required_error(field.label_str()))),
186                    in_repeater,
187                    place,
188                }
189            })
190            .collect()
191    }
192
193    /// Lists keys in `values` that no declared input owns, sorted, so handlers reject
194    /// client-controlled writes.
195    pub fn unknown_keys(&self, values: &HashMap<String, String>) -> Vec<String> {
196        let known: HashSet<&str> = self.fields.iter().map(Field::name).collect();
197        let mut out: Vec<String> = values
198            .keys()
199            .filter(|k| !known.contains(k.as_str()))
200            .cloned()
201            .collect();
202        out.sort();
203        out
204    }
205
206    /// Reports what is wrong with this declaration: a field whose lens binds no single column, and
207    /// two fields sharing a name.
208    pub fn declaration_errors(&self) -> Vec<crate::DeclarationErrorKind> {
209        let mut errors = Vec::new();
210        let mut seen = HashSet::new();
211        for field in &self.fields {
212            match field.misdeclared() {
213                Some(error) => errors.push(error.clone()),
214                None if !seen.insert(field.name()) => {
215                    errors.push(crate::DeclarationErrorKind::DuplicateField {
216                        name: field.name().to_string(),
217                    });
218                }
219                None => {}
220            }
221        }
222        errors
223    }
224
225    /// Validates submitted values against declared inputs, treating absent keys as empty and
226    /// skipping fields in absent repeater groups and hidden variant groups.
227    pub fn validate(&self, values: &HashMap<String, String>) -> FieldErrors {
228        let mut errors = FieldErrors::new();
229        let mut skip: HashSet<String> = HashSet::new();
230        walk_absent_groups(
231            &self.nodes,
232            &self.fields,
233            values,
234            &mut skip,
235            &mut errors,
236            false,
237        );
238        for field in &self.fields {
239            if skip.contains(field.name()) {
240                continue;
241            }
242            let value = values.get(field.name()).map(String::as_str).unwrap_or("");
243            for error in field.validate(value) {
244                errors.push(error);
245            }
246        }
247        errors
248    }
249
250    /// Collects field names a submission leaves out of validation.
251    pub(crate) fn absent_fields(&self, values: &HashMap<String, String>) -> HashSet<String> {
252        let mut skip = HashSet::new();
253        let mut discarded = FieldErrors::new();
254        walk_absent_groups(
255            &self.nodes,
256            &self.fields,
257            values,
258            &mut skip,
259            &mut discarded,
260            false,
261        );
262        skip
263    }
264
265    /// Reports whether this submission renders an error under `key`, either a visible field or a
266    /// repeater label.
267    pub(crate) fn renders_error_key(&self, values: &HashMap<String, String>, key: &str) -> bool {
268        fn labels(nodes: &[Node], key: &str) -> bool {
269            nodes.iter().any(|node| match node {
270                Node::Repeater(repeater) => {
271                    repeater.label == key || labels(&repeater.children.nodes, key)
272                }
273                node => node
274                    .children()
275                    .is_some_and(|children| labels(children, key)),
276            })
277        }
278        let hidden = self.hidden_fields(values);
279        let renders_field = self
280            .fields
281            .iter()
282            .any(|field| field.name() == key && !hidden.contains(key));
283        renders_field || labels(&self.nodes, key)
284    }
285
286    /// Collects field names this submission hides.
287    fn hidden_fields(&self, values: &HashMap<String, String>) -> HashSet<String> {
288        fn walk(nodes: &[Node], values: &HashMap<String, String>, out: &mut Vec<usize>) {
289            for node in nodes {
290                match node {
291                    Node::Embedded(embedded) => embedded.hidden_fields(values, out),
292                    node => {
293                        if let Some(children) = node.children() {
294                            walk(children, values, out);
295                        }
296                    }
297                }
298            }
299        }
300        let mut indices = Vec::new();
301        walk(&self.nodes, values, &mut indices);
302        indices
303            .into_iter()
304            .map(|index| self.fields[index].name().to_string())
305            .collect()
306    }
307
308    /// Validates submissions and then probes each visible choice's option existence.
309    pub async fn validate_async(&self, cx: &Cx, values: &HashMap<String, String>) -> FieldErrors {
310        let mut errors = self.validate(values);
311        let absent = self.absent_fields(values);
312        for field in &self.fields {
313            let name = field.name();
314            if errors.contains_key(name) || absent.contains(name) {
315                continue;
316            }
317            let Some(value) = values.get(name) else {
318                continue;
319            };
320            for message in field.validate_exists(cx, value).await {
321                errors.add(name, message);
322            }
323        }
324        errors
325    }
326
327    /// Re-checks every submitted relationship key through the write's open transaction.
328    pub(crate) async fn recheck_relationships(
329        &self,
330        cx: &Cx,
331        values: &HashMap<String, String>,
332        ex: &mut dyn toasty::Executor,
333    ) -> FieldErrors {
334        let mut errors = FieldErrors::new();
335        let absent = self.absent_fields(values);
336        for field in &self.fields {
337            let name = field.name();
338            if absent.contains(name) {
339                continue;
340            }
341            let Some(value) = values.get(name) else {
342                continue;
343            };
344            for message in field.recheck(cx, value, &mut *ex).await {
345                errors.add(name, message);
346            }
347        }
348        errors
349    }
350}
351
352#[cfg(test)]
353mod tests;