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    condition::{Condition, watch},
10    embedded::Embedded,
11    fields::{Field, Placement},
12    layouts::{Grid, Group, Section},
13    lenses::FieldResolver,
14};
15use crate::form::FieldErrors;
16
17#[derive(Debug)]
18pub(crate) enum Node {
19    /// A field, by its index in the root schema's field list.
20    Field(usize),
21    Section(Box<Section>),
22    Group(Box<Group>),
23    Grid(Box<Grid>),
24    Embedded(Box<Embedded>),
25    /// An embedded value its schema has not bound yet: [`Schema::bind`] builds it through the app
26    /// schema and splices it in place.
27    Unbound(Unbound),
28}
29
30/// Builds an embedded value's node through the app schema it binds to.
31pub(crate) struct Unbound {
32    pub(crate) build: Box<dyn Fn(&FieldResolver) -> Schema + Send + Sync>,
33    /// The value's type, which the declaration error names.
34    pub(crate) value: &'static str,
35}
36
37impl std::fmt::Debug for Unbound {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_tuple("Unbound").field(&self.value).finish()
40    }
41}
42
43/// Holds the values and errors a schema's controls render with.
44pub struct Source<'a> {
45    values: &'a HashMap<String, String>,
46    errors: &'a FieldErrors,
47    /// The conditions each watched field's key drives.
48    watched: HashMap<String, Vec<Condition>>,
49    /// The prefix of the form's DOM ids and of its signals' keys; empty for a form alone on its
50    /// page.
51    scope: String,
52    /// Where a searchable choice fetches its options, when not the resource's own route.
53    options: Option<String>,
54}
55
56impl<'a> Source<'a> {
57    /// Renders controls hydrated with `values` and inline `errors`.
58    pub fn form(values: &'a HashMap<String, String>, errors: &'a FieldErrors) -> Self {
59        Self {
60            values,
61            errors,
62            watched: HashMap::new(),
63            scope: String::new(),
64            options: None,
65        }
66    }
67
68    /// Renders a form sharing its page with another: its DOM ids and its signals' keys carry
69    /// `scope`, so neither form's labels, conditions or variants reach the other's.
70    pub(crate) fn scoped(mut self, scope: impl Into<String>) -> Self {
71        self.scope = scope.into();
72        self
73    }
74
75    /// Fetches a searchable choice's options from `url` rather than the resource's own route.
76    pub(crate) fn options_at(mut self, url: impl Into<String>) -> Self {
77        self.options = Some(url.into());
78        self
79    }
80
81    /// The prefix of the form's DOM ids and of its signals' keys.
82    pub(crate) fn scope(&self) -> &str {
83        &self.scope
84    }
85
86    /// Renders each watched field with the handlers its conditions follow.
87    pub(crate) fn watching(mut self, watched: HashMap<String, Vec<Condition>>) -> Self {
88        self.watched = watched;
89        self
90    }
91
92    /// Renders `view` guarded by `condition`, or as it is without one.
93    fn guard<'v>(
94        &self,
95        cx: &'v Cx,
96        condition: Option<&Condition>,
97        view: BoxView<'v>,
98    ) -> BoxView<'v> {
99        match condition {
100            Some(condition) => condition.guard(cx, &self.scope, self.values, view),
101            None => view,
102        }
103    }
104
105    pub(crate) fn value(&self, name: &str) -> Option<&str> {
106        self.values.get(name).map(String::as_str)
107    }
108
109    /// The errors the controls render under.
110    pub(crate) fn errors(&self) -> &FieldErrors {
111        self.errors
112    }
113
114    /// Where a searchable choice fetches its options, when not the resource's own route.
115    pub(crate) fn options(&self) -> Option<&str> {
116        self.options.as_deref()
117    }
118
119    /// The DOM id of the control posting `name`.
120    pub(crate) fn id(&self, name: &str) -> String {
121        self.placement(None).id(name)
122    }
123
124    /// Where a field renders in this form, under its parent's value `parent`.
125    fn placement<'p>(&'p self, parent: Option<&'p str>) -> Placement<'p> {
126        Placement {
127            parent,
128            scope: &self.scope,
129            options: self.options.as_deref(),
130        }
131    }
132
133    /// Returns the message the field `field` renders under its control.
134    pub(crate) fn error_for(&self, field: &Field) -> Option<String> {
135        self.errors
136            .first(field.name())
137            .map(|error| error.message(field.label_str()))
138    }
139}
140
141impl Node {
142    /// Renders this node from `source`, reading field slots from `fields`.
143    pub(crate) async fn render<'a>(
144        &self,
145        cx: &'a Cx,
146        fields: &[Field],
147        source: &Source<'_>,
148    ) -> Result<BoxView<'a>> {
149        match self {
150            Node::Field(index) => {
151                let field = &fields[*index];
152                // A repeater's rows render from the keys and errors of each of their controls.
153                if let Some(repeater) = field.as_repeater() {
154                    let view = Box::pin(repeater.render(cx, field, source)).await?;
155                    return Ok(source.guard(cx, field.condition(), view));
156                }
157                let error = source.error_for(field);
158                let parent = field.parent_key().and_then(|key| source.value(key));
159                let mut view = Box::pin(field.render_under(
160                    cx,
161                    source.value(field.name()),
162                    error.as_deref(),
163                    source.placement(parent),
164                ))
165                .await?;
166                if let Some(conditions) = source.watched.get(field.name()) {
167                    view = watch(cx, field, conditions, &source.scope, source.values, view);
168                }
169                Ok(source.guard(cx, field.condition(), view))
170            }
171            Node::Section(s) => {
172                let view = Box::pin(s.render(cx, fields, source)).await?;
173                Ok(source.guard(cx, s.condition.as_ref(), view))
174            }
175            Node::Group(g) => {
176                let view = Box::pin(g.render(cx, fields, source)).await?;
177                Ok(source.guard(cx, g.condition.as_ref(), view))
178            }
179            Node::Grid(g) => {
180                let view = Box::pin(g.render(cx, fields, source)).await?;
181                Ok(source.guard(cx, g.condition.as_ref(), view))
182            }
183            Node::Embedded(e) => Box::pin(e.render(cx, fields, source)).await,
184            // `Schema::render` refuses an unbound schema before any node renders.
185            Node::Unbound(_) => Ok(().boxed()),
186        }
187    }
188
189    /// Returns the nodes a layout container holds, or `None` for a field slot and an embedded
190    /// value.
191    pub(crate) fn children(&self) -> Option<&[Node]> {
192        match self {
193            Node::Section(s) => Some(&s.children.nodes),
194            Node::Group(g) => Some(&g.children.nodes),
195            Node::Grid(g) => Some(&g.children.nodes),
196            Node::Field(_) | Node::Embedded(_) | Node::Unbound(_) => None,
197        }
198    }
199
200    fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
201        match self {
202            Node::Section(s) => Some(&mut s.children.nodes),
203            Node::Group(g) => Some(&mut g.children.nodes),
204            Node::Grid(g) => Some(&mut g.children.nodes),
205            Node::Field(_) | Node::Embedded(_) | Node::Unbound(_) => None,
206        }
207    }
208
209    /// Shifts every field slot under this node by `by`.
210    pub(crate) fn offset(&mut self, by: usize) {
211        match self {
212            Node::Field(index) => *index += by,
213            Node::Embedded(e) => e.offset(by),
214            _ => {
215                for child in self.children_mut().into_iter().flatten() {
216                    child.offset(by);
217                }
218            }
219        }
220    }
221}
222
223/// Builds every unbound node under `nodes` through `resolver` in place, appending its fields to
224/// `fields`.
225pub(crate) fn bind_nodes(nodes: &mut Vec<Node>, fields: &mut Vec<Field>, resolver: &FieldResolver) {
226    let mut bound = Vec::with_capacity(nodes.len());
227    for node in std::mem::take(nodes) {
228        let mut node = match node {
229            Node::Unbound(unbound) => {
230                let Schema {
231                    nodes: mut built,
232                    fields: built_fields,
233                    ..
234                } = (unbound.build)(resolver);
235                let offset = fields.len();
236                for node in &mut built {
237                    node.offset(offset);
238                }
239                fields.extend(built_fields);
240                bound.extend(built);
241                continue;
242            }
243            node => node,
244        };
245        if let Some(children) = node.children_mut() {
246            bind_nodes(children, fields, resolver);
247        }
248        bound.push(node);
249    }
250    *nodes = bound;
251}
252
253/// Collects the value types of the unbound nodes under `nodes`.
254pub(crate) fn unbound_values(nodes: &[Node], out: &mut Vec<&'static str>) {
255    for node in nodes {
256        match node {
257            Node::Unbound(unbound) => out.push(unbound.value),
258            node => unbound_values(node.children().unwrap_or_default(), out),
259        }
260    }
261}
262
263/// Renders `nodes` in order as one view.
264pub(crate) async fn render_nodes<'a>(
265    cx: &'a Cx,
266    nodes: &[Node],
267    fields: &[Field],
268    source: &Source<'_>,
269) -> Result<BoxView<'a>> {
270    let mut views = Vec::with_capacity(nodes.len());
271    for node in nodes {
272        views.push(node.render(cx, fields, source).await?);
273    }
274    Ok(view! {
275        cx =>
276        for v in views {
277            (v)
278        }
279    }
280    .boxed())
281}
282
283/// Converts a field, a layout block, a tuple of either, or a schema into a [`Schema`].
284///
285/// `F` is the record form whose controls the schema places, `()` for a page's or an action's
286/// schema: a control converts into the schema of its own form only.
287#[diagnostic::on_unimplemented(
288    message = "`{Self}` cannot be placed in a `Schema<{F}>`",
289    label = "not a control, layout block, schema, or tuple of 2 to 12 of them for this schema",
290    note = "a resource's form places its record form's own controls, from `controls()`; \
291            `Field::text` and the other constructors build a page's or an action's controls"
292)]
293pub trait IntoSchema<F = ()> {
294    fn into_schema(self) -> Schema<F>;
295}
296
297/// Moves a control or a schema to the form `G`: what `#[derive(RecordForm)]` hands its controls
298/// over with.
299#[doc(hidden)]
300pub trait Retype {
301    type As<G>;
302
303    fn retype<G>(self) -> Self::As<G>;
304}
305
306impl<F> IntoSchema<F> for Schema<F> {
307    fn into_schema(self) -> Schema<F> {
308        self
309    }
310}
311
312impl IntoSchema for Field {
313    fn into_schema(self) -> Schema {
314        Schema::from_parts(vec![Node::Field(0)], vec![self])
315    }
316}
317
318/// Generates the single-node [`IntoSchema`] impls for every layout container, holding a schema or
319/// nothing.
320macro_rules! container_nodes {
321    ($($ty:ident),+ $(,)?) => {
322        $(
323            impl<F> IntoSchema<F> for $ty<Schema<F>> {
324                fn into_schema(self) -> Schema<F> {
325                    let mut fields = Vec::new();
326                    let block = self.map(|mut children| {
327                        fields = std::mem::take(&mut children.fields);
328                        children.retype()
329                    });
330                    Schema::from_parts(vec![Node::$ty(Box::new(block))], fields)
331                }
332            }
333
334            impl<F> IntoSchema<F> for $ty<()> {
335                fn into_schema(self) -> Schema<F> {
336                    self.holding(Schema::<F>::default()).into_schema()
337                }
338            }
339        )+
340    };
341}
342
343container_nodes!(Section, Group, Grid);
344
345/// Generates the tuple impls of [`IntoSchema`] from one list per arity.
346macro_rules! into_schema_tuples {
347    ($($T:ident => $v:ident),+ $(,)?) => {
348        impl<Form, $($T),+> IntoSchema<Form> for ($($T,)+)
349        where
350            $($T: IntoSchema<Form>,)+
351        {
352            fn into_schema(self) -> Schema<Form> {
353                let ($($v,)+) = self;
354                let mut schema = Schema::default();
355                $( schema.append($v.into_schema()); )+
356                schema
357            }
358        }
359    };
360}
361
362into_schema_tuples!(A => a, B => b);
363into_schema_tuples!(A => a, B => b, C => c);
364into_schema_tuples!(A => a, B => b, C => c, D => d);
365into_schema_tuples!(A => a, B => b, C => c, D => d, E => e);
366into_schema_tuples!(A => a, B => b, C => c, D => d, E => e, F => f);
367into_schema_tuples!(A => a, B => b, C => c, D => d, E => e, F => f, G => g);
368into_schema_tuples!(
369    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h
370);
371into_schema_tuples!(
372    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i
373);
374into_schema_tuples!(
375    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j
376);
377into_schema_tuples!(
378    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k
379);
380into_schema_tuples!(
381    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k, L => l
382);