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;
4
5use topcoat::{Result, context::Cx, view::*};
6
7use super::{
8    Schema,
9    embedded::Embedded,
10    fields::Field,
11    layouts::{Grid, Group, Section},
12    lenses::FieldResolver,
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    Section(Box<Section>),
21    Group(Box<Group>),
22    Grid(Box<Grid>),
23    Embedded(Box<Embedded>),
24    /// An embedded value its schema has not bound yet: [`Schema::bind`] builds it through the app
25    /// schema and splices it in place.
26    Unbound(Unbound),
27}
28
29/// Builds an embedded value's node through the app schema it binds to.
30pub(crate) struct Unbound {
31    pub(crate) build: Box<dyn Fn(&FieldResolver) -> Schema + Send + Sync>,
32    /// The value's type, which the declaration error names.
33    pub(crate) value: &'static str,
34}
35
36impl std::fmt::Debug for Unbound {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.debug_tuple("Unbound").field(&self.value).finish()
39    }
40}
41
42/// Holds where a schema render reads field values and errors from for a form or a read-only view.
43pub struct Source<'a> {
44    values: &'a HashMap<String, String>,
45    errors: Option<&'a FieldErrors>,
46}
47
48impl<'a> Source<'a> {
49    /// Renders controls hydrated with `values` and inline `errors`.
50    pub fn form(values: &'a HashMap<String, String>, errors: &'a FieldErrors) -> Self {
51        Self {
52            values,
53            errors: Some(errors),
54        }
55    }
56
57    /// Renders a record's `values` read-only and renders a missing key as `(missing)`.
58    pub fn view(values: &'a HashMap<String, String>) -> Self {
59        Self {
60            values,
61            errors: None,
62        }
63    }
64
65    pub(crate) fn mode(&self) -> Mode {
66        if self.errors.is_some() {
67            Mode::Form
68        } else {
69            Mode::View
70        }
71    }
72
73    pub(crate) fn value(&self, name: &str) -> Option<&str> {
74        self.values.get(name).map(String::as_str)
75    }
76
77    /// Returns the message the field `field` renders under its control.
78    pub(crate) fn error_for(&self, field: &Field) -> Option<String> {
79        self.errors
80            .and_then(|errors| errors.first(field.name()))
81            .map(|error| error.message(field.label_str()))
82    }
83}
84
85/// Distinguishes a form render from a read-only view.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub(crate) enum Mode {
88    Form,
89    View,
90}
91
92impl Node {
93    /// Renders this node from `source`, reading field slots from `fields`.
94    pub(crate) async fn render<'a>(
95        &self,
96        cx: &'a Cx,
97        fields: &[Field],
98        source: &Source<'_>,
99    ) -> Result<BoxView<'a>> {
100        match self {
101            Node::Field(index) => {
102                let field = &fields[*index];
103                let error = source.error_for(field);
104                Box::pin(field.render(
105                    cx,
106                    source.value(field.name()),
107                    error.as_deref(),
108                    source.mode(),
109                ))
110                .await
111            }
112            Node::Section(s) => Box::pin(s.render(cx, fields, source)).await,
113            Node::Group(g) => Box::pin(g.render(cx, fields, source)).await,
114            Node::Grid(g) => Box::pin(g.render(cx, fields, source)).await,
115            Node::Embedded(e) => Box::pin(e.render(cx, fields, source)).await,
116            // `Schema::render` refuses an unbound schema before any node renders.
117            Node::Unbound(_) => Ok(().boxed()),
118        }
119    }
120
121    /// Returns the nodes a layout container holds, or `None` for a field slot and an embedded
122    /// value.
123    pub(crate) fn children(&self) -> Option<&[Node]> {
124        match self {
125            Node::Section(s) => Some(&s.children.nodes),
126            Node::Group(g) => Some(&g.children.nodes),
127            Node::Grid(g) => Some(&g.children.nodes),
128            Node::Field(_) | Node::Embedded(_) | Node::Unbound(_) => None,
129        }
130    }
131
132    fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
133        match self {
134            Node::Section(s) => Some(&mut s.children.nodes),
135            Node::Group(g) => Some(&mut g.children.nodes),
136            Node::Grid(g) => Some(&mut g.children.nodes),
137            Node::Field(_) | Node::Embedded(_) | Node::Unbound(_) => None,
138        }
139    }
140
141    /// Shifts every field slot under this node by `by`.
142    pub(crate) fn offset(&mut self, by: usize) {
143        match self {
144            Node::Field(index) => *index += by,
145            Node::Embedded(e) => e.offset(by),
146            _ => {
147                for child in self.children_mut().into_iter().flatten() {
148                    child.offset(by);
149                }
150            }
151        }
152    }
153}
154
155/// Builds every unbound node under `nodes` through `resolver` in place, appending its fields to
156/// `fields`.
157pub(crate) fn bind_nodes(nodes: &mut Vec<Node>, fields: &mut Vec<Field>, resolver: &FieldResolver) {
158    let mut bound = Vec::with_capacity(nodes.len());
159    for node in std::mem::take(nodes) {
160        let mut node = match node {
161            Node::Unbound(unbound) => {
162                let Schema {
163                    nodes: mut built,
164                    fields: built_fields,
165                } = (unbound.build)(resolver);
166                let offset = fields.len();
167                for node in &mut built {
168                    node.offset(offset);
169                }
170                fields.extend(built_fields);
171                bound.extend(built);
172                continue;
173            }
174            node => node,
175        };
176        if let Some(children) = node.children_mut() {
177            bind_nodes(children, fields, resolver);
178        }
179        bound.push(node);
180    }
181    *nodes = bound;
182}
183
184/// Collects the value types of the unbound nodes under `nodes`.
185pub(crate) fn unbound_values(nodes: &[Node], out: &mut Vec<&'static str>) {
186    for node in nodes {
187        match node {
188            Node::Unbound(unbound) => out.push(unbound.value),
189            node => unbound_values(node.children().unwrap_or_default(), out),
190        }
191    }
192}
193
194/// Renders `nodes` in order as one view.
195pub(crate) async fn render_nodes<'a>(
196    cx: &'a Cx,
197    nodes: &[Node],
198    fields: &[Field],
199    source: &Source<'_>,
200) -> Result<BoxView<'a>> {
201    let mut views = Vec::with_capacity(nodes.len());
202    for node in nodes {
203        views.push(node.render(cx, fields, source).await?);
204    }
205    Ok(view! {
206        cx =>
207        for v in views {
208            (v)
209        }
210    }
211    .boxed())
212}
213
214/// Converts a field, a layout block, a tuple of either, or a schema into a [`Schema`].
215pub trait IntoSchema {
216    fn into_schema(self) -> Schema;
217}
218
219impl IntoSchema for Schema {
220    fn into_schema(self) -> Schema {
221        self
222    }
223}
224
225impl IntoSchema for Field {
226    fn into_schema(self) -> Schema {
227        Schema {
228            nodes: vec![Node::Field(0)],
229            fields: vec![self],
230        }
231    }
232}
233
234/// Generates the single-node [`IntoSchema`] impl for every layout container.
235macro_rules! container_nodes {
236    ($($ty:ident),+ $(,)?) => {
237        $(
238            impl IntoSchema for $ty {
239                fn into_schema(mut self) -> Schema {
240                    let fields = std::mem::take(&mut self.children.fields);
241                    Schema {
242                        nodes: vec![Node::$ty(Box::new(self))],
243                        fields,
244                    }
245                }
246            }
247        )+
248    };
249}
250
251container_nodes!(Section, Group, Grid);
252
253/// Generates the tuple impls of [`IntoSchema`] from one list per arity.
254macro_rules! into_schema_tuples {
255    ($($T:ident => $v:ident),+ $(,)?) => {
256        impl<$($T),+> IntoSchema for ($($T,)+)
257        where
258            $($T: IntoSchema,)+
259        {
260            fn into_schema(self) -> Schema {
261                let ($($v,)+) = self;
262                let mut schema = Schema::empty();
263                $( schema.append($v.into_schema()); )+
264                schema
265            }
266        }
267    };
268}
269
270into_schema_tuples!(A => a, B => b);
271into_schema_tuples!(A => a, B => b, C => c);
272into_schema_tuples!(A => a, B => b, C => c, D => d);
273into_schema_tuples!(A => a, B => b, C => c, D => d, E => e);
274into_schema_tuples!(A => a, B => b, C => c, D => d, E => e, F => f);
275into_schema_tuples!(A => a, B => b, C => c, D => d, E => e, F => f, G => g);
276into_schema_tuples!(
277    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h
278);
279
280#[cfg(test)]
281mod tests;