Skip to main content

tablo_core/schema/
embedded.rs

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