Skip to main content

tablo_core/schema/
embedded.rs

1//! Embedded values: a typed value and the flat form map converted in one declared place, plus the
2//! schema node that renders it.
3//!
4//! An embedded leaf binds one flattened storage column and a value binds as a whole; a payload
5//! selects the variant only when the submission carries no discriminant at all, a shared column
6//! never selects one, and an unknown discriminant is refused.
7//!
8//! # What an app writes
9//!
10//! ```text
11//! #[derive(Clone, toasty::Embed, tablo_core::EmbeddedForm)]
12//! pub struct Seo { pub title: String, pub description: String }
13//!
14//! Section::new("SEO").schema(Seo::form(Post::fields().seo()));
15//! record.seo.write_form(cx, Post::fields().seo(), &mut values);
16//! let seo = Seo::read_form(cx, Post::fields().seo(), &values)?;
17//! ```
18//!
19//! # What is not covered
20//!
21//! A `#[document]` inside an embedded value, a relation inside one, and an embedded enum nested
22//! inside an enum variant are not covered.
23//!
24//! Every leaf under an embedded step reports nullable.
25
26use std::collections::HashMap;
27
28use tablo_ui::field_group as ui_field_group;
29use toasty::stmt::Path;
30use topcoat::{
31    Result,
32    context::Cx,
33    runtime::{Event, signal},
34    view::*,
35};
36
37use super::{
38    Schema,
39    fields::Field,
40    lenses::FieldResolver,
41    tree::{Mode, Node, Source, Unbound},
42};
43use crate::{
44    form::{FieldError, FormField},
45    topcoat_compat::async_page,
46};
47
48/// Reads an embedded value from and writes it to the flat form map; derive it to generate the
49/// value's schema.
50pub trait EmbeddedForm: Sized {
51    /// Writes this value's leaves into `out`, including the active variant's discriminant for an
52    /// enum.
53    fn write_form<M>(
54        &self,
55        cx: &Cx,
56        parent: impl Into<Path<M, Self>>,
57        out: &mut HashMap<String, String>,
58    ) where
59        M: toasty::schema::Model,
60    {
61        let schema = Self::build_schema(&FieldResolver::of(cx), parent.into());
62        self.write_node(schema.embedded_root(), out);
63    }
64
65    /// Reads a value back from a submission, taking an embedded enum's variant from the
66    /// discriminant key and refusing a discriminant that names no variant.
67    ///
68    /// # Errors
69    ///
70    /// Every leaf whose value its type refuses, and a discriminant that names
71    /// no variant.
72    fn read_form<M>(
73        cx: &Cx,
74        parent: impl Into<Path<M, Self>>,
75        values: &HashMap<String, String>,
76    ) -> std::result::Result<Self, Vec<FieldError>>
77    where
78        M: toasty::schema::Model,
79    {
80        let schema = Self::build_schema(&FieldResolver::of(cx), parent.into());
81        Self::read_node(schema.embedded_root(), values)
82    }
83
84    /// The value's schema: one node holding its fields, resolved through `resolver`'s app schema.
85    #[doc(hidden)]
86    fn build_schema<M>(resolver: &FieldResolver, parent: Path<M, Self>) -> Schema
87    where
88        M: toasty::schema::Model;
89
90    #[doc(hidden)]
91    fn write_node(&self, node: &Embedded, out: &mut HashMap<String, String>);
92
93    #[doc(hidden)]
94    fn read_node(
95        node: &Embedded,
96        values: &HashMap<String, String>,
97    ) -> std::result::Result<Self, Vec<FieldError>>;
98}
99
100/// Holds an embedded value's resolved keys, rendering fields, and variant groups for an enum.
101#[doc(hidden)]
102#[derive(Debug)]
103pub struct Embedded {
104    shape: Shape,
105}
106
107#[derive(Debug)]
108enum Shape {
109    Struct(Vec<Member>),
110    Enum(EnumNode),
111}
112
113#[derive(Debug)]
114struct EnumNode {
115    key: String,
116    discriminant: usize,
117    shared: Vec<usize>,
118    variants: Vec<Variant>,
119}
120
121#[derive(Debug)]
122struct Variant {
123    value: String,
124    members: Vec<Member>,
125}
126
127#[derive(Debug)]
128enum Member {
129    Leaf { key: String, field: Option<usize> },
130    Nested(Embedded),
131}
132
133impl Embedded {
134    fn members(&self, variant: Option<usize>) -> &[Member] {
135        match (&self.shape, variant) {
136            (Shape::Struct(members), None) => members,
137            (Shape::Enum(e), Some(index)) => &e.variants[index].members,
138            _ => panic!("a struct member is addressed without a variant, an enum's with one"),
139        }
140    }
141
142    pub fn key(&self, variant: Option<usize>, index: usize) -> &str {
143        match &self.members(variant)[index] {
144            Member::Leaf { key, .. } => key,
145            Member::Nested(_) => panic!("member {index} is an embedded value, not a leaf"),
146        }
147    }
148
149    pub fn nested(&self, variant: Option<usize>, index: usize) -> &Embedded {
150        match &self.members(variant)[index] {
151            Member::Nested(nested) => nested,
152            Member::Leaf { .. } => panic!("member {index} is a leaf, not an embedded value"),
153        }
154    }
155
156    fn enum_node(&self) -> &EnumNode {
157        match &self.shape {
158            Shape::Enum(e) => e,
159            Shape::Struct(_) => panic!("a struct value has no variant"),
160        }
161    }
162
163    pub fn write_variant(&self, index: usize, out: &mut HashMap<String, String>) {
164        let e = self.enum_node();
165        out.insert(e.key.clone(), e.variants[index].value.clone());
166    }
167
168    /// Returns the variant a submission reads as, falling back to the first variant with a
169    /// submitted payload when it names no discriminant, and refuses an unknown discriminant.
170    pub fn variant_index(
171        &self,
172        values: &HashMap<String, String>,
173    ) -> std::result::Result<usize, Vec<FieldError>> {
174        let e = self.enum_node();
175        let submitted = values.get(&e.key).map(|v| v.trim()).unwrap_or_default();
176        if submitted.is_empty() {
177            let own = |member: &Member| match member {
178                Member::Leaf { key, field } => field.is_some() && is_present(values, key),
179                Member::Nested(nested) => nested.any_present(values),
180            };
181            let inferred = e.variants.iter().position(|v| v.members.iter().any(own));
182            return Ok(inferred.unwrap_or(0));
183        }
184        e.variants
185            .iter()
186            .position(|v| v.value == submitted)
187            .ok_or_else(|| {
188                vec![FieldError::invalid(
189                    e.key.clone(),
190                    format!("`{submitted}` is not a valid variant"),
191                )]
192            })
193    }
194
195    /// Whether a submission mentions any key of this value.
196    fn any_present(&self, values: &HashMap<String, String>) -> bool {
197        let member = |member: &Member| match member {
198            Member::Leaf { key, .. } => is_present(values, key),
199            Member::Nested(nested) => nested.any_present(values),
200        };
201        match &self.shape {
202            Shape::Struct(members) => members.iter().any(member),
203            Shape::Enum(e) => {
204                is_present(values, &e.key)
205                    || e.variants.iter().any(|v| v.members.iter().any(member))
206            }
207        }
208    }
209
210    /// Collects every form key the value occupies, each once.
211    pub(crate) fn keys(&self) -> Vec<String> {
212        let mut out = Vec::new();
213        self.collect_keys(&mut out);
214        out
215    }
216
217    fn collect_keys(&self, out: &mut Vec<String>) {
218        let push = |members: &[Member], out: &mut Vec<String>| {
219            for member in members {
220                match member {
221                    Member::Leaf { key, .. } if !out.contains(key) => out.push(key.clone()),
222                    Member::Leaf { .. } => {}
223                    Member::Nested(nested) => nested.collect_keys(out),
224                }
225            }
226        };
227        match &self.shape {
228            Shape::Struct(members) => push(members, out),
229            Shape::Enum(e) => {
230                out.push(e.key.clone());
231                for variant in &e.variants {
232                    push(&variant.members, out);
233                }
234            }
235        }
236    }
237
238    pub(crate) fn offset(&mut self, by: usize) {
239        let members = |members: &mut Vec<Member>| {
240            for member in members {
241                match member {
242                    Member::Leaf { field, .. } => {
243                        if let Some(index) = field {
244                            *index += by;
245                        }
246                    }
247                    Member::Nested(nested) => nested.offset(by),
248                }
249            }
250        };
251        match &mut self.shape {
252            Shape::Struct(list) => members(list),
253            Shape::Enum(e) => {
254                e.discriminant += by;
255                for index in &mut e.shared {
256                    *index += by;
257                }
258                for variant in &mut e.variants {
259                    members(&mut variant.members);
260                }
261            }
262        }
263    }
264
265    /// Visits every field slot the value renders.
266    fn visit_fields(&self, f: &mut impl FnMut(usize)) {
267        fn members(members: &[Member], f: &mut impl FnMut(usize)) {
268            for member in members {
269                match member {
270                    Member::Leaf {
271                        field: Some(index), ..
272                    } => f(*index),
273                    Member::Leaf { field: None, .. } => {}
274                    Member::Nested(nested) => nested.visit_fields(f),
275                }
276            }
277        }
278        match &self.shape {
279            Shape::Struct(list) => members(list, f),
280            Shape::Enum(e) => {
281                f(e.discriminant);
282                for index in &e.shared {
283                    f(*index);
284                }
285                for variant in &e.variants {
286                    members(&variant.members, f);
287                }
288            }
289        }
290    }
291
292    /// Collects the field slots of every variant group the submission hides, hiding nothing when it
293    /// names no variant.
294    pub(crate) fn hidden_fields(&self, values: &HashMap<String, String>, out: &mut Vec<usize>) {
295        let nested = |members: &[Member], out: &mut Vec<usize>| {
296            for member in members {
297                if let Member::Nested(nested) = member {
298                    nested.hidden_fields(values, out);
299                }
300            }
301        };
302        match &self.shape {
303            Shape::Struct(members) => nested(members, out),
304            Shape::Enum(e) => {
305                let chosen = values.get(&e.key).map(|v| v.trim()).unwrap_or_default();
306                for variant in &e.variants {
307                    if !chosen.is_empty() && chosen != variant.value {
308                        for member in &variant.members {
309                            match member {
310                                Member::Leaf {
311                                    field: Some(index), ..
312                                } => out.push(*index),
313                                Member::Leaf { field: None, .. } => {}
314                                Member::Nested(nested) => {
315                                    nested.visit_fields(&mut |index| out.push(index))
316                                }
317                            }
318                        }
319                    } else {
320                        nested(&variant.members, out);
321                    }
322                }
323            }
324        }
325    }
326
327    /// Renders the value: in a form, every variant group, showing the chosen variant's; in a view,
328    /// only the stored variant's group.
329    pub(crate) async fn render<'a>(
330        &self,
331        cx: &'a Cx,
332        fields: &[Field],
333        source: &Source<'_>,
334    ) -> Result<BoxView<'a>> {
335        match &self.shape {
336            Shape::Struct(members) => render_members(cx, members, fields, source).await,
337            Shape::Enum(e) => {
338                let discriminant = Node::Field(e.discriminant)
339                    .render(cx, fields, source)
340                    .await?;
341                let stored = source.value(&e.key).map(str::trim);
342                let stored_variant = e
343                    .variants
344                    .iter()
345                    .find(|variant| stored == Some(variant.value.as_str()));
346                let mut shared = Vec::with_capacity(e.shared.len());
347                for index in &e.shared {
348                    let key = fields[*index].name();
349                    let declared = stored_variant.is_some_and(|variant| {
350                        variant
351                            .members
352                            .iter()
353                            .any(|member| matches!(member, Member::Leaf { key: k, .. } if k == key))
354                    });
355                    if source.mode() == Mode::View && !declared {
356                        continue;
357                    }
358                    shared.push(Node::Field(*index).render(cx, fields, source).await?);
359                }
360                let mut groups = Vec::with_capacity(e.variants.len());
361                for variant in &e.variants {
362                    if source.mode() == Mode::View && stored != Some(variant.value.as_str()) {
363                        continue;
364                    }
365                    let members = render_members(cx, &variant.members, fields, source).await?;
366                    groups.push((variant.value.clone(), members));
367                }
368                let key = e.key.clone();
369                let stored = stored.unwrap_or_default().to_string();
370                // The variant the select names is a signal, so choosing another shows its group
371                // in place; every group still submits, and the server parses the chosen one.
372                // Keyed by the page's path: navigation carries the values of signals two pages
373                // share, and another record's form starts from its own stored variant.
374                let page = topcoat::context::try_request_context::<http::request::Parts>(cx)
375                    .map(|parts| parts.uri.path().to_string())
376                    .unwrap_or_default();
377                Ok(async_page(async move {
378                    let variant = signal(
379                        &cx.keyed(("tablo-variant", page, key.as_str())),
380                        move || stored,
381                    );
382                    let chosen = variant.clone();
383                    let groups: Vec<BoxView<'a>> = groups
384                        .into_iter()
385                        .map(|(value, members)| {
386                            let shown = variant.clone();
387                            view! {
388                                cx =>
389                                ui_field_group(
390                                    attrs: attributes! {
391                                        data-variant=(value.clone())
392                                        :hidden=$(shown.get() != value)
393                                    },
394                                    (members)
395                                )
396                            }
397                            .boxed()
398                        })
399                        .collect();
400                    Ok(view! {
401                        cx =>
402                        <div
403                            class="contents"
404                            @change=$(|e: Event| chosen.set(e.target.value))
405                        >
406                            (discriminant)
407                        </div>
408                        for v in shared {
409                            (v)
410                        }
411                        for g in groups {
412                            (g)
413                        }
414                    })
415                }))
416            }
417        }
418    }
419}
420
421/// Renders a struct's or a variant group's members in order.
422async fn render_members<'a>(
423    cx: &'a Cx,
424    members: &[Member],
425    fields: &[Field],
426    source: &Source<'_>,
427) -> Result<BoxView<'a>> {
428    let mut views = Vec::with_capacity(members.len());
429    for member in members {
430        match member {
431            Member::Leaf {
432                field: Some(index), ..
433            } => views.push(Node::Field(*index).render(cx, fields, source).await?),
434            Member::Leaf { field: None, .. } => {}
435            Member::Nested(nested) => {
436                views.push(Box::pin(nested.render(cx, fields, source)).await?)
437            }
438        }
439    }
440    Ok(view! {
441        cx =>
442        for v in views {
443            (v)
444        }
445    }
446    .boxed())
447}
448
449fn is_present(values: &HashMap<String, String>, key: &str) -> bool {
450    values
451        .get(key)
452        .is_some_and(|value| !value.trim().is_empty())
453}
454
455/// Builds an embedded value's schema node, one member at a time, in the order the derive declares
456/// them.
457#[doc(hidden)]
458pub struct EmbeddedBuilder {
459    resolver: FieldResolver,
460    fields: Vec<Field>,
461    shape: Shape,
462    variant: Option<usize>,
463}
464
465impl EmbeddedBuilder {
466    pub fn structure(resolver: &FieldResolver) -> Self {
467        Self {
468            resolver: resolver.clone(),
469            fields: Vec::new(),
470            shape: Shape::Struct(Vec::new()),
471            variant: None,
472        }
473    }
474
475    /// Builds an enum value at `parent` from `resolver`'s app schema and panics when it has none
476    /// or `parent` names no embedded enum.
477    pub fn enumeration<M, T>(resolver: &FieldResolver, parent: Path<M, T>) -> Self
478    where
479        M: toasty::schema::Model,
480    {
481        assert!(
482            resolver.has_schema(),
483            "an embedded enum resolves through the app schema, which a value reaches through the \
484             request's `Db` or the schema it binds to"
485        );
486        let shape = resolver.resolve_enum(parent).unwrap_or_else(|| {
487            panic!(
488                "{} is not an embedded enum in this app schema",
489                std::any::type_name::<T>()
490            )
491        });
492        let variants = shape
493            .variants
494            .iter()
495            .map(|(value, _)| Variant {
496                value: value.clone(),
497                members: Vec::new(),
498            })
499            .collect();
500        Self {
501            resolver: resolver.clone(),
502            fields: vec![Field::discriminant(
503                shape.discriminant.clone(),
504                shape.variants,
505            )],
506            shape: Shape::Enum(EnumNode {
507                key: shape.discriminant,
508                discriminant: 0,
509                shared: Vec::new(),
510                variants,
511            }),
512            variant: None,
513        }
514    }
515
516    /// Starts the next variant's members.
517    pub fn variant(&mut self) {
518        let next = self.variant.map_or(0, |index| index + 1);
519        let Shape::Enum(e) = &self.shape else {
520            panic!("a struct value has no variant");
521        };
522        assert!(
523            next < e.variants.len(),
524            "the type declares more variants than the app schema"
525        );
526        self.variant = Some(next);
527    }
528
529    fn members_mut(&mut self) -> &mut Vec<Member> {
530        match (&mut self.shape, self.variant) {
531            (Shape::Struct(members), _) => members,
532            (Shape::Enum(e), Some(index)) => &mut e.variants[index].members,
533            (Shape::Enum(_), None) => panic!("an enum member needs `variant()` first"),
534        }
535    }
536
537    /// Adds a leaf, required when it has no blank answer.
538    pub fn leaf(&mut self, field: impl Into<Field>, required: bool) {
539        let mut field = field.into();
540        field.set_required(required);
541        field.bind(&self.resolver);
542        let key = field.name().to_string();
543        let index = self.fields.len();
544        self.fields.push(field);
545        self.members_mut().push(Member::Leaf {
546            key,
547            field: Some(index),
548        });
549    }
550
551    /// Adds a `#[shared(..)]` leaf that renders once, outside the variant groups.
552    pub fn shared(&mut self, field: impl Into<Field>, required: bool) {
553        let mut field = field.into();
554        field.set_required(required);
555        field.bind(&self.resolver);
556        let key = field.name().to_string();
557        let Shape::Enum(e) = &mut self.shape else {
558            panic!("a struct value has no shared column");
559        };
560        if !e
561            .shared
562            .iter()
563            .any(|index| self.fields[*index].name() == key)
564        {
565            e.shared.push(self.fields.len());
566            self.fields.push(field);
567        }
568        self.members_mut().push(Member::Leaf { key, field: None });
569    }
570
571    /// Adds a nested value from its own `build_schema`.
572    pub fn nested(&mut self, schema: Schema) {
573        let Schema { nodes, fields } = schema;
574        let Ok([Node::Embedded(mut nested)]) = <[Node; 1]>::try_from(nodes) else {
575            panic!("a nested value's schema is its one embedded node");
576        };
577        nested.offset(self.fields.len());
578        self.fields.extend(fields);
579        self.members_mut().push(Member::Nested(*nested));
580    }
581
582    /// Finishes the value's schema as one embedded node.
583    pub fn finish(self) -> Schema {
584        if let Shape::Enum(e) = &self.shape {
585            assert_eq!(
586                self.variant.map_or(0, |index| index + 1),
587                e.variants.len(),
588                "the type declares fewer variants than the app schema"
589            );
590        }
591        Schema {
592            nodes: vec![Node::Embedded(Box::new(Embedded { shape: self.shape }))],
593            fields: self.fields,
594        }
595    }
596}
597
598/// The record-form field binding the embedded value at `parent`: every form key it occupies, and
599/// the ones its leaves require.
600#[doc(hidden)]
601pub fn embedded_field<M, T, K>(
602    resolver: &FieldResolver,
603    parent: impl Into<Path<M, T>>,
604    field: K,
605    name: &'static str,
606) -> FormField<K>
607where
608    M: toasty::schema::Model,
609    T: EmbeddedForm,
610{
611    let schema = T::build_schema(resolver, parent.into());
612    FormField {
613        field,
614        name,
615        keys: schema.embedded_root().keys(),
616        required: schema
617            .fields()
618            .filter(|field| field.is_required())
619            .map(|field| field.name().to_string())
620            .collect(),
621    }
622}
623
624/// The embedded value at `parent` as a schema node its schema builds when it binds.
625#[doc(hidden)]
626pub fn embedded_form<M, T>(parent: Path<M, T>) -> Schema
627where
628    M: toasty::schema::Model + Send + Sync + 'static,
629    T: EmbeddedForm + Send + Sync + 'static,
630{
631    Schema {
632        nodes: vec![Node::Unbound(Unbound {
633            build: Box::new(move |resolver| T::build_schema(resolver, parent.clone())),
634            value: std::any::type_name::<T>(),
635        })],
636        fields: Vec::new(),
637    }
638}
639
640/// Moves `result`'s value out, or its errors into `errors`.
641#[doc(hidden)]
642pub fn take_leaf<T>(
643    result: std::result::Result<T, FieldError>,
644    errors: &mut Vec<FieldError>,
645) -> Option<T> {
646    result.map_err(|error| errors.push(error)).ok()
647}
648
649#[doc(hidden)]
650pub fn take_value<T>(
651    result: std::result::Result<T, Vec<FieldError>>,
652    errors: &mut Vec<FieldError>,
653) -> Option<T> {
654    result.map_err(|nested| errors.extend(nested)).ok()
655}
656
657#[cfg(test)]
658mod tests;