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 condition;
7pub(crate) mod embedded;
8pub(crate) mod fields;
9mod layouts;
10mod lenses;
11mod options;
12pub(crate) mod relationship;
13pub(crate) mod repeater;
14pub(crate) mod tree;
15pub(crate) mod validation;
16
17use std::{
18    collections::{HashMap, HashSet},
19    marker::PhantomData,
20};
21
22pub use condition::Watched;
23pub use embedded::EmbeddedForm;
24use fields::ChoiceControl;
25pub use fields::{
26    ChoiceField, CustomField, Field, FileField, IntoOptions, RepeaterField, TextField, Toggle,
27};
28pub(crate) use fields::{option_view, read_only, stored_upload, value_cell};
29pub use layouts::{Grid, Group, Section};
30pub(crate) use lenses::Binding;
31pub use lenses::{FieldResolver, form_key};
32pub use options::Options;
33pub use relationship::MAX_RELATIONSHIP_OPTIONS;
34pub(crate) use relationship::{OptionLoadError, OptionSource};
35pub use repeater::{MAX_ROWS, RepeaterItem, parse_items, write_items};
36use topcoat::{Result, context::Cx, view::*};
37pub use tree::{IntoSchema, Source};
38pub(crate) use tree::{Node, Retype, render_nodes};
39use tree::{bind_nodes, unbound_values};
40pub(crate) use validation::TypedValue;
41
42use crate::form::FieldErrors;
43
44/// Composes fields and layout blocks and resolves every field once into one list.
45///
46/// `F` is the record form whose controls the schema places. A resource's form is a
47/// `Schema<UserForm>`, which only `UserForm::controls()` fill, so a control from another form or a
48/// field built with [`Field::text`] fails to compile there. A page's or an action's schema is a
49/// plain `Schema`, which [`Field`]'s constructors fill.
50///
51/// ```compile_fail
52/// # #[derive(Debug, Clone, toasty::Model)]
53/// # struct User { #[key] #[auto] id: uuid::Uuid, name: String, email: String }
54/// # #[derive(tablo_core::RecordForm)]
55/// # #[form(model = User)]
56/// # struct UserForm { name: String }
57/// use tablo_core::{Field, Schema};
58///
59/// let form: Schema<UserForm> = Schema::new((
60///     UserForm::controls().name,
61///     Field::text(User::fields().email()),
62/// ));
63/// ```
64pub struct Schema<F = ()> {
65    pub(crate) nodes: Vec<Node>,
66    pub(crate) fields: Vec<Field>,
67    form: PhantomData<fn() -> F>,
68}
69
70impl<F> Default for Schema<F> {
71    fn default() -> Self {
72        Self::from_parts(Vec::new(), Vec::new())
73    }
74}
75
76impl<F> std::fmt::Debug for Schema<F> {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.debug_struct("Schema")
79            .field("nodes", &self.nodes)
80            .field("fields", &self.fields)
81            .finish()
82    }
83}
84
85impl Schema {
86    /// A schema with no field: a page's or an action's that renders nothing. A resource's form
87    /// that places nothing is `Schema::default()`, which renders every control.
88    pub fn empty() -> Self {
89        Self::default()
90    }
91}
92
93impl<F> Retype for Schema<F> {
94    type As<G> = Schema<G>;
95
96    fn retype<G>(self) -> Schema<G> {
97        Schema::from_parts(self.nodes, self.fields)
98    }
99}
100
101impl<F> Schema<F> {
102    pub(crate) fn from_parts(nodes: Vec<Node>, fields: Vec<Field>) -> Self {
103        Self {
104            nodes,
105            fields,
106            form: PhantomData,
107        }
108    }
109
110    /// Reports whether this schema declares nothing to render.
111    pub fn is_empty(&self) -> bool {
112        self.nodes.is_empty()
113    }
114
115    /// Builds a `Schema` from any `IntoSchema` and reports duplicate field names as declaration
116    /// errors.
117    pub fn new(children: impl IntoSchema<F>) -> Self {
118        children.into_schema()
119    }
120
121    /// Every field, in the order it joins the schema: a layout's fields as it composes, in
122    /// declaration order, and an embedded value's fields when it binds.
123    pub fn fields(&self) -> impl Iterator<Item = &Field> {
124        self.fields.iter()
125    }
126
127    /// Binds the schema's embedded values and embedded paths to `db`'s app schema.
128    ///
129    /// A panel binds the schemas it mounts; bind one a custom page renders before rendering it.
130    /// A schema with no embedded value or path is bound from the start.
131    pub fn bind(mut self, db: &toasty::Db) -> Self {
132        self.bind_with(&FieldResolver::of_db(db));
133        self
134    }
135
136    /// Binds the schema through `resolver`: builds each unbound embedded value in place, then
137    /// binds every field's path.
138    pub(crate) fn bind_with(&mut self, resolver: &FieldResolver) {
139        bind_nodes(&mut self.nodes, &mut self.fields, resolver);
140        for field in &self.fields {
141            field.bind(resolver);
142        }
143    }
144
145    /// Renders each control required exactly when `required` names its key.
146    pub(crate) fn require(&mut self, required: &HashSet<&str>) {
147        for field in &mut self.fields {
148            let key = required.contains(field.name());
149            field.set_required(key);
150        }
151    }
152
153    /// Renders the schema from `source` and fails with declaration errors instead of rendering.
154    ///
155    /// # Errors
156    ///
157    /// A misdeclared schema fails with its errors rather than render.
158    pub async fn render<'a>(&self, cx: &'a Cx, source: Source<'_>) -> Result<BoxView<'a>> {
159        let errors = self.declaration_errors();
160        if !errors.is_empty() {
161            return Err(crate::error::misdeclared(&errors));
162        }
163        let source = source.watching(self.watched());
164        render_nodes(cx, &self.nodes, &self.fields, &source).await
165    }
166
167    /// Appends `other`'s nodes and fields after this one's, re-slotting its field slots.
168    pub(crate) fn append(&mut self, other: Schema<F>) {
169        let Schema {
170            mut nodes, fields, ..
171        } = other;
172        let offset = self.fields.len();
173        for node in &mut nodes {
174            node.offset(offset);
175        }
176        self.fields.extend(fields);
177        self.nodes.extend(nodes);
178    }
179
180    /// The embedded node a derived value's schema holds.
181    pub(crate) fn embedded_root(&self) -> &embedded::Embedded {
182        match self.nodes.as_slice() {
183            [Node::Embedded(node)] => node,
184            _ => panic!("an embedded value's schema is its one embedded node"),
185        }
186    }
187
188    /// Folds each repeater's posted rows into its own key, which
189    /// [`parse_items`] reads.
190    ///
191    /// A resource's form and an action's input fold their submissions; a page handling its own
192    /// post folds it before reading it, or the rows' keys stay apart.
193    ///
194    /// # Errors
195    ///
196    /// A repeater's key lists something other than its rows, a row posting nothing, or more than
197    /// [`MAX_ROWS`] rows: no browser posts these, so a handler answers 400.
198    pub fn fold_repeaters(
199        &self,
200        values: &mut HashMap<String, String>,
201    ) -> std::result::Result<(), String> {
202        for field in &self.fields {
203            if let Some(repeater) = field.as_repeater() {
204                repeater.fold(field.name(), values)?;
205            }
206        }
207        Ok(())
208    }
209
210    /// Folds the values each multiple choice posted, listed in `lists`, into its one key: a list
211    /// without blanks or repeats, which [`parse_list`](crate::form::parse_list) reads. A choice
212    /// that posted nothing folds nothing, so an edit keeps the stored list.
213    pub(crate) fn fold_choices(
214        &self,
215        values: &mut HashMap<String, String>,
216        lists: &HashMap<String, Vec<String>>,
217    ) {
218        let multiple = self
219            .fields
220            .iter()
221            .filter(|field| field.as_choice().is_some_and(ChoiceControl::is_multiple));
222        for field in multiple {
223            let Some(posted) = lists.get(field.name()) else {
224                continue;
225            };
226            let mut keys: Vec<String> = Vec::new();
227            for value in posted.iter().map(|value| value.trim()) {
228                if !value.is_empty() && !keys.iter().any(|key| key == value) {
229                    keys.push(value.to_string());
230                }
231            }
232            values.insert(field.name().to_string(), crate::form::encode_list(&keys));
233        }
234    }
235
236    /// A posted input as its parse reads it: the `reserved` keys the POST carries for itself
237    /// dropped, each repeater's rows and each multiple choice's values folded into their one key
238    /// (read with `parse_items` and `parse_list`), and a condition-hidden field's key removed,
239    /// since the browser posts nothing for it.
240    ///
241    /// # Errors
242    ///
243    /// 400 for a malformed repeater and for a key no field declares.
244    pub(crate) fn read_input(
245        &self,
246        values: &HashMap<String, String>,
247        lists: &HashMap<String, Vec<String>>,
248        reserved: &[&str],
249    ) -> Result<HashMap<String, String>> {
250        use topcoat::router::error::bad_request;
251
252        let mut input = values.clone();
253        input.retain(|key, _| !reserved.contains(&key.as_str()));
254        self.fold_repeaters(&mut input).map_err(bad_request)?;
255        self.fold_choices(&mut input, lists);
256        let unknown = self.unknown_keys(&input);
257        if !unknown.is_empty() {
258            return Err(bad_request(format!("unknown field(s): {}", unknown.join(", "))).into());
259        }
260        for key in self.condition_hidden(&input) {
261            input.remove(&key);
262        }
263        Ok(input)
264    }
265
266    /// Appends another schema's nodes after this one's.
267    pub fn extend(mut self, other: Schema<F>) -> Schema<F> {
268        self.append(other);
269        self
270    }
271
272    /// Lists keys in `values` that no declared input owns, sorted, so handlers reject
273    /// client-controlled writes.
274    pub(crate) fn unknown_keys(&self, values: &HashMap<String, String>) -> Vec<String> {
275        let known: HashSet<&str> = self.fields.iter().map(Field::name).collect();
276        let mut out: Vec<String> = values
277            .keys()
278            .filter(|k| !known.contains(k.as_str()))
279            .cloned()
280            .collect();
281        out.sort();
282        out
283    }
284
285    /// One [`EmptyChoice`](crate::DeclarationErrorKind::EmptyChoice) per choice with neither
286    /// options nor a relationship. Mounting checks a resource's form and its actions' inputs; a
287    /// page's schema may build its options from data that is empty for now, so rendering does
288    /// not.
289    pub(crate) fn empty_choices(&self) -> Vec<crate::DeclarationErrorKind> {
290        self.fields
291            .iter()
292            .filter(|field| field.offers_nothing())
293            .map(|field| crate::DeclarationErrorKind::EmptyChoice {
294                field: field.name().to_string(),
295            })
296            .collect()
297    }
298
299    /// What is wrong with the schema's dependent choices: a column of another model than the
300    /// relationship's source, or no relationship, and a parent the schema does not place, which
301    /// posts nothing to narrow the options by.
302    fn dependent_errors(&self) -> Vec<crate::DeclarationErrorKind> {
303        let mut errors = Vec::new();
304        for field in &self.fields {
305            let Some(choice) = field.as_choice() else {
306                continue;
307            };
308            if choice.misdeclared_parent() {
309                errors.push(crate::DeclarationErrorKind::MisdeclaredDependentChoice {
310                    field: field.name().to_string(),
311                });
312            }
313            let Some(parent) = choice.parent_key() else {
314                continue;
315            };
316            match self.fields.iter().find(|placed| placed.name() == parent) {
317                None => errors.push(crate::DeclarationErrorKind::UnplacedParentField {
318                    field: field.name().to_string(),
319                    parent: parent.to_string(),
320                }),
321                Some(parent)
322                    if choice.is_multiple()
323                        || parent.as_choice().is_some_and(ChoiceControl::is_multiple) =>
324                {
325                    errors.push(crate::DeclarationErrorKind::MultipleDependentChoice {
326                        field: field.name().to_string(),
327                    });
328                }
329                Some(_) => {}
330            }
331        }
332        errors
333    }
334
335    /// Reports what is wrong with this declaration: an embedded value or path never bound, a field
336    /// whose lens binds no single column, and two fields sharing a name.
337    pub fn declaration_errors(&self) -> Vec<crate::DeclarationErrorKind> {
338        let mut unbound = Vec::new();
339        unbound_values(&self.nodes, &mut unbound);
340        let mut errors: Vec<_> = unbound
341            .into_iter()
342            .map(|item| crate::DeclarationErrorKind::Unbound { item })
343            .collect();
344        errors.extend(self.condition_errors());
345        errors.extend(self.dependent_errors());
346        let mut seen = HashSet::new();
347        for field in &self.fields {
348            match field.misdeclared() {
349                Some(error) => errors.push(error),
350                None if !seen.insert(field.name()) => {
351                    errors.push(crate::DeclarationErrorKind::DuplicateField {
352                        name: field.name().to_string(),
353                    });
354                }
355                None => {}
356            }
357        }
358        errors
359    }
360
361    /// Collects the keys of the fields this submission hides: the payload of every embedded
362    /// variant it does not choose, and every field a condition hides.
363    pub(crate) fn hidden_fields(&self, values: &HashMap<String, String>) -> HashSet<String> {
364        fn walk(nodes: &[Node], values: &HashMap<String, String>, out: &mut Vec<usize>) {
365            for node in nodes {
366                match node {
367                    Node::Embedded(embedded) => embedded.hidden_fields(values, out),
368                    node => {
369                        if let Some(children) = node.children() {
370                            walk(children, values, out);
371                        }
372                    }
373                }
374            }
375        }
376        let mut indices = Vec::new();
377        walk(&self.nodes, values, &mut indices);
378        indices
379            .into_iter()
380            .map(|index| self.fields[index].name().to_string())
381            .chain(self.condition_hidden(values))
382            .collect()
383    }
384
385    /// Adds to `errors` what the controls' own rules refuse in a submission: an email field's
386    /// address, and a choice that is not one of its options. Skips hidden fields and keys
387    /// `errors` already refuses.
388    pub(crate) async fn check_controls(
389        &self,
390        cx: &Cx,
391        values: &HashMap<String, String>,
392        stored: &HashMap<String, String>,
393        errors: &mut FieldErrors,
394    ) {
395        let hidden = self.hidden_fields(values);
396        for field in &self.fields {
397            let name = field.name();
398            if errors.contains_key(name) || hidden.contains(name) {
399                continue;
400            }
401            let Some(value) = values.get(name) else {
402                continue;
403            };
404            if let Some(repeater) = field.as_repeater() {
405                repeater.check(cx, name, value, errors).await;
406                continue;
407            }
408            if let Some(error) = field.check(value) {
409                errors.push(error);
410                continue;
411            }
412            let parent = field.parent_key().and_then(|key| values.get(key));
413            for message in field
414                .validate_exists(
415                    cx,
416                    value,
417                    parent.map(String::as_str),
418                    stored.get(name).map(String::as_str),
419                )
420                .await
421            {
422                errors.add(name, message);
423            }
424        }
425    }
426
427    /// What the controls' own rules refuse in `values`, alone.
428    #[cfg(test)]
429    pub(crate) async fn checked(&self, cx: &Cx, values: &HashMap<String, String>) -> FieldErrors {
430        let mut errors = FieldErrors::new();
431        self.check_controls(cx, values, &HashMap::new(), &mut errors)
432            .await;
433        errors
434    }
435
436    /// Re-checks every submitted relationship key through the write's open transaction, but a
437    /// multiple choice's keys the record's stored value in `stored` holds already.
438    pub(crate) async fn recheck_relationships(
439        &self,
440        cx: &Cx,
441        values: &HashMap<String, String>,
442        stored: &HashMap<String, String>,
443        ex: &mut dyn toasty::Executor,
444    ) -> FieldErrors {
445        let mut errors = FieldErrors::new();
446        let hidden = self.hidden_fields(values);
447        for field in &self.fields {
448            let name = field.name();
449            if hidden.contains(name) {
450                continue;
451            }
452            let Some(value) = values.get(name) else {
453                continue;
454            };
455            let parent = field.parent_key().and_then(|key| values.get(key));
456            for message in field
457                .recheck(
458                    cx,
459                    value,
460                    parent.map(String::as_str),
461                    stored.get(name).map(String::as_str),
462                    &mut *ex,
463                )
464                .await
465            {
466                errors.add(name, message);
467            }
468        }
469        errors
470    }
471
472    /// Puts back in each multiple choice's list the keys of the record's stored list `stored` it
473    /// drops that the user cannot view, through the write's open transaction: the form never
474    /// offered them, so its submission leaves them linked.
475    pub(crate) async fn keep_unseen_links(
476        &self,
477        cx: &Cx,
478        values: &mut HashMap<String, String>,
479        stored: &HashMap<String, String>,
480        ex: &mut dyn toasty::Executor,
481    ) {
482        for field in &self.fields {
483            let name = field.name();
484            let (Some(value), Some(held)) = (values.get(name), stored.get(name)) else {
485                continue;
486            };
487            if let Some(kept) = field.keep_unseen(cx, value, held, &mut *ex).await {
488                values.insert(name.to_string(), kept);
489            }
490        }
491    }
492}
493
494#[cfg(test)]
495mod tests;