Skip to main content

tablo_core/schema/
tree.rs

1//! Composes field slots, layout containers, and embedded values into one tree.
2
3use std::collections::{HashMap, HashSet};
4
5use topcoat::{Result, context::Cx, view::*};
6
7use super::{
8    Schema,
9    embedded::Embedded,
10    fields::Field,
11    layouts::{Grid, Group, Repeater, Section},
12    validation::required_error,
13};
14use crate::form::FieldErrors;
15
16#[derive(Debug)]
17pub(crate) enum Node {
18    /// A field, by its index in the root schema's field list.
19    Field(usize),
20    Repeater(Box<Repeater>),
21    Section(Box<Section>),
22    Group(Box<Group>),
23    Grid(Box<Grid>),
24    Embedded(Box<Embedded>),
25}
26
27/// Reports where a field's control sits in the form.
28#[derive(Clone, Copy, Debug)]
29pub(crate) enum LeafPlace {
30    Rendered,
31    Payload,
32    Discriminant,
33}
34
35/// Holds where a schema render reads field values and errors from for a form or a read-only view.
36pub struct Source<'a> {
37    values: &'a HashMap<String, String>,
38    errors: Option<&'a FieldErrors>,
39}
40
41impl<'a> Source<'a> {
42    /// Renders controls hydrated with `values` and inline `errors`.
43    pub fn form(values: &'a HashMap<String, String>, errors: &'a FieldErrors) -> Self {
44        Self {
45            values,
46            errors: Some(errors),
47        }
48    }
49
50    /// Renders a record's `values` read-only and renders a missing key as `(missing)`.
51    pub fn view(values: &'a HashMap<String, String>) -> Self {
52        Self {
53            values,
54            errors: None,
55        }
56    }
57
58    pub(crate) fn mode(&self) -> Mode {
59        if self.errors.is_some() {
60            Mode::Form
61        } else {
62            Mode::View
63        }
64    }
65
66    pub(crate) fn value(&self, name: &str) -> Option<&str> {
67        self.values.get(name).map(String::as_str)
68    }
69
70    /// Returns the error `name` renders.
71    pub(crate) fn errors_for(&self, name: &str) -> Option<&str> {
72        self.errors
73            .and_then(|errors| errors.first(name))
74            .map(|error| error.message.as_str())
75    }
76}
77
78/// Distinguishes a form render from a read-only view.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub(crate) enum Mode {
81    Form,
82    View,
83}
84
85impl Node {
86    /// Renders this node from `source`, reading field slots from `fields`.
87    pub(crate) async fn render<'a>(
88        &self,
89        cx: &'a Cx,
90        fields: &[Field],
91        source: &Source<'_>,
92    ) -> Result<BoxView<'a>> {
93        match self {
94            Node::Field(index) => {
95                let field = &fields[*index];
96                Box::pin(field.render(
97                    cx,
98                    source.value(field.name()),
99                    source.errors_for(field.name()),
100                    source.mode(),
101                ))
102                .await
103            }
104            Node::Repeater(r) => Box::pin(r.render(cx, fields, source)).await,
105            Node::Section(s) => Box::pin(s.render(cx, fields, source)).await,
106            Node::Group(g) => Box::pin(g.render(cx, fields, source)).await,
107            Node::Grid(g) => Box::pin(g.render(cx, fields, source)).await,
108            Node::Embedded(e) => Box::pin(e.render(cx, fields, source)).await,
109        }
110    }
111
112    /// Returns the nodes a layout container holds, or `None` for a field slot and an embedded
113    /// value.
114    pub(crate) fn children(&self) -> Option<&[Node]> {
115        match self {
116            Node::Repeater(r) => Some(&r.children.nodes),
117            Node::Section(s) => Some(&s.children.nodes),
118            Node::Group(g) => Some(&g.children.nodes),
119            Node::Grid(g) => Some(&g.children.nodes),
120            Node::Field(_) | Node::Embedded(_) => None,
121        }
122    }
123
124    fn children_mut(&mut self) -> Option<&mut [Node]> {
125        match self {
126            Node::Repeater(r) => Some(&mut r.children.nodes),
127            Node::Section(s) => Some(&mut s.children.nodes),
128            Node::Group(g) => Some(&mut g.children.nodes),
129            Node::Grid(g) => Some(&mut g.children.nodes),
130            Node::Field(_) | Node::Embedded(_) => None,
131        }
132    }
133
134    /// Shifts every field slot under this node by `by`.
135    pub(crate) fn offset(&mut self, by: usize) {
136        match self {
137            Node::Field(index) => *index += by,
138            Node::Embedded(e) => e.offset(by),
139            _ => {
140                for child in self.children_mut().unwrap_or_default() {
141                    child.offset(by);
142                }
143            }
144        }
145    }
146
147    /// Visits every field slot under this node with where its control sits in the form.
148    pub(crate) fn visit_fields(&self, f: &mut impl FnMut(usize, LeafPlace)) {
149        match self {
150            Node::Field(index) => f(*index, LeafPlace::Rendered),
151            Node::Embedded(e) => e.visit_fields(LeafPlace::Rendered, f),
152            _ => {
153                for child in self.children().unwrap_or_default() {
154                    child.visit_fields(f);
155                }
156            }
157        }
158    }
159}
160
161/// Renders `nodes` in order as one view.
162pub(crate) async fn render_nodes<'a>(
163    cx: &'a Cx,
164    nodes: &[Node],
165    fields: &[Field],
166    source: &Source<'_>,
167) -> Result<BoxView<'a>> {
168    let mut views = Vec::with_capacity(nodes.len());
169    for node in nodes {
170        views.push(node.render(cx, fields, source).await?);
171    }
172    Ok(view! {
173        cx =>
174        for v in views {
175            (v)
176        }
177    }
178    .boxed())
179}
180
181/// Classifies the tree's groups against `values`, skipping fields in all-empty repeater groups and
182/// hidden variant groups and reporting a required absent repeater under its label.
183pub(crate) fn walk_absent_groups(
184    nodes: &[Node],
185    fields: &[Field],
186    values: &HashMap<String, String>,
187    skip: &mut HashSet<String>,
188    errors: &mut FieldErrors,
189    inside_absent: bool,
190) {
191    let name = |index: usize| fields[index].name().to_string();
192    for node in nodes {
193        match node {
194            Node::Embedded(e) => {
195                let mut hidden = Vec::new();
196                e.hidden_fields(values, &mut hidden);
197                skip.extend(hidden.into_iter().map(name));
198            }
199            Node::Repeater(r) => {
200                let mut inner = Vec::new();
201                node.visit_fields(&mut |index, _| inner.push(name(index)));
202                // `all` on an empty list is true: an inputless group is absent.
203                let all_empty = inner
204                    .iter()
205                    .all(|n| values.get(n).map(|v| v.trim().is_empty()).unwrap_or(true));
206                if all_empty {
207                    skip.extend(inner);
208                    if r.required && !inside_absent && !errors.contains_key(&r.label) {
209                        errors.add_required(&r.label, required_error(&r.label));
210                    }
211                }
212                walk_absent_groups(
213                    &r.children.nodes,
214                    fields,
215                    values,
216                    skip,
217                    errors,
218                    inside_absent || all_empty,
219                );
220            }
221            _ => {
222                if let Some(children) = node.children() {
223                    walk_absent_groups(children, fields, values, skip, errors, inside_absent);
224                }
225            }
226        }
227    }
228}
229
230/// Converts a field, a layout block, a tuple of either, or a schema into a [`Schema`].
231pub trait IntoSchema {
232    fn into_schema(self) -> Schema;
233}
234
235impl IntoSchema for Schema {
236    fn into_schema(self) -> Schema {
237        self
238    }
239}
240
241impl IntoSchema for Field {
242    fn into_schema(self) -> Schema {
243        Schema {
244            nodes: vec![Node::Field(0)],
245            fields: vec![self],
246        }
247    }
248}
249
250/// Generates the single-node [`IntoSchema`] impl for every layout container.
251macro_rules! container_nodes {
252    ($($ty:ident),+ $(,)?) => {
253        $(
254            impl IntoSchema for $ty {
255                fn into_schema(mut self) -> Schema {
256                    let fields = std::mem::take(&mut self.children.fields);
257                    Schema {
258                        nodes: vec![Node::$ty(Box::new(self))],
259                        fields,
260                    }
261                }
262            }
263        )+
264    };
265}
266
267container_nodes!(Section, Group, Grid, Repeater);
268
269/// Generates the tuple impls of [`IntoSchema`] from one list per arity.
270macro_rules! into_schema_tuples {
271    ($($T:ident => $v:ident),+ $(,)?) => {
272        impl<$($T),+> IntoSchema for ($($T,)+)
273        where
274            $($T: IntoSchema,)+
275        {
276            fn into_schema(self) -> Schema {
277                let ($($v,)+) = self;
278                let mut schema = Schema::empty();
279                $( schema.append($v.into_schema()); )+
280                schema
281            }
282        }
283    };
284}
285
286into_schema_tuples!(A => a, B => b);
287into_schema_tuples!(A => a, B => b, C => c);
288into_schema_tuples!(A => a, B => b, C => c, D => d);
289into_schema_tuples!(A => a, B => b, C => c, D => d, E => e);
290into_schema_tuples!(A => a, B => b, C => c, D => d, E => e, F => f);
291into_schema_tuples!(A => a, B => b, C => c, D => d, E => e, F => f, G => g);
292into_schema_tuples!(
293    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h
294);
295
296#[cfg(test)]
297mod tests;