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