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_request_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                                cx =>
382                                <fieldset
383                                    class="contents"
384                                    :disabled=$(enabled.get() != named)
385                                >
386                                    ui_field_group(
387                                        attrs: attributes! {
388                                            data-variant=(value.clone())
389                                            :hidden=$(shown.get() != value)
390                                        },
391                                        (members)
392                                    )
393                                </fieldset>
394                            }
395                            .boxed()
396                        })
397                        .collect();
398                    Ok(view! {
399                        cx =>
400                        <div
401                            class="contents"
402                            @change=$(|e: Event| chosen.set(e.target.value))
403                        >
404                            (discriminant)
405                        </div>
406                        for v in shared {
407                            (v)
408                        }
409                        for g in groups {
410                            (g)
411                        }
412                    })
413                }))
414            }
415        }
416    }
417}
418
419impl Embedded {
420    /// Renders the value `values` spells read-only: each [shown](Self::shown) field's label over
421    /// its stored value.
422    pub(crate) fn display<'a>(
423        &self,
424        cx: &'a Cx,
425        fields: &[Field],
426        values: &FormValues,
427    ) -> BoxView<'a> {
428        let views: Vec<BoxView<'a>> = self
429            .shown(fields, values)
430            .into_iter()
431            .map(|index| {
432                let field = &fields[index];
433                field.display(cx, values.text(field.name()).unwrap_or_default())
434            })
435            .collect();
436        view! {
437            cx =>
438            for v in views {
439                (v)
440            }
441        }
442        .boxed()
443    }
444
445    /// The slots of the fields a reader of the value `values` spells sees, in order: a struct's
446    /// leaves, or an enum's discriminant, the shared leaves its stored variant declares, and that
447    /// variant's own leaves. An enum whose stored discriminant names no variant shows nothing.
448    pub(crate) fn shown(&self, fields: &[Field], values: &FormValues) -> Vec<usize> {
449        let mut out = Vec::new();
450        self.shown_into(fields, values, &mut out);
451        out
452    }
453
454    fn shown_into(&self, fields: &[Field], values: &FormValues, out: &mut Vec<usize>) {
455        let members = |members: &[Member], out: &mut Vec<usize>| {
456            for member in members {
457                match member {
458                    Member::Leaf {
459                        field: Some(index), ..
460                    } => out.push(*index),
461                    Member::Leaf { field: None, .. } => {}
462                    Member::Nested(nested) => nested.shown_into(fields, values, out),
463                }
464            }
465        };
466        match &self.shape {
467            Shape::Struct(list) => members(list, out),
468            Shape::Enum(e) => {
469                let stored = values.text(&e.key).map(str::trim);
470                let Some(variant) = e
471                    .variants
472                    .iter()
473                    .find(|variant| stored == Some(variant.value.as_str()))
474                else {
475                    return;
476                };
477                out.push(e.discriminant);
478                // A shared column belongs to the variants that declare it; a unit variant stores
479                // nothing in it.
480                out.extend(e.shared.iter().copied().filter(|index| {
481                    let key = fields[*index].name();
482                    variant
483                        .members
484                        .iter()
485                        .any(|member| matches!(member, Member::Leaf { key: k, .. } if k == key))
486                }));
487                members(&variant.members, out);
488            }
489        }
490    }
491}
492
493/// Renders a struct's or a variant group's members in order.
494async fn render_members<'a>(
495    cx: &'a Cx,
496    members: &[Member],
497    fields: &[Field],
498    source: &Source<'_>,
499) -> Result<BoxView<'a>> {
500    let mut views = Vec::with_capacity(members.len());
501    for member in members {
502        match member {
503            Member::Leaf {
504                field: Some(index), ..
505            } => views.push(Node::Field(*index).render(cx, fields, source).await?),
506            Member::Leaf { field: None, .. } => {}
507            Member::Nested(nested) => {
508                views.push(Box::pin(nested.render(cx, fields, source)).await?)
509            }
510        }
511    }
512    Ok(view! {
513        cx =>
514        for v in views {
515            (v)
516        }
517    }
518    .boxed())
519}
520
521fn is_present(values: &FormValues, key: &str) -> bool {
522    values
523        .text(key)
524        .is_some_and(|value| !value.trim().is_empty())
525}
526
527/// Builds an embedded value's schema node, one member at a time, in the order the derive declares
528/// them.
529#[doc(hidden)]
530pub struct EmbeddedBuilder {
531    fields: Vec<Field>,
532    shape: Shape,
533    variant: Option<usize>,
534    /// Whether the enum's model compiles to no schema, so the type, not the schema, lists its
535    /// variants.
536    refused: bool,
537}
538
539impl EmbeddedBuilder {
540    pub fn structure() -> Self {
541        Self {
542            fields: Vec::new(),
543            shape: Shape::Struct(Vec::new()),
544            variant: None,
545            refused: false,
546        }
547    }
548
549    /// Builds an enum value at `parent` and panics when `parent` names no embedded enum.
550    ///
551    /// On a model no driver compiles, the variant control carries the error to mount instead, and
552    /// each `variant()` opens one as the type declares it.
553    pub fn enumeration<M, T>(parent: &Path<M, T>) -> Self
554    where
555        M: toasty::schema::Model,
556    {
557        let (binding, variants, refused) = match lenses::resolve_enum(parent) {
558            Ok(Some(shape)) => {
559                let label = Text::field(&shape.discriminant);
560                (
561                    Binding::named(shape.discriminant, label),
562                    shape.variants,
563                    false,
564                )
565            }
566            Ok(None) => panic!(
567                "{} is not an embedded enum of {}",
568                std::any::type_name::<T>(),
569                std::any::type_name::<M>()
570            ),
571            Err(error) => (Binding::refused(parent, error), Vec::new(), true),
572        };
573        let discriminant = Field::discriminant(binding, variants.clone());
574        Self {
575            shape: Shape::Enum(EnumNode {
576                key: discriminant.name().to_string(),
577                discriminant: 0,
578                shared: Vec::new(),
579                variants: variants
580                    .into_iter()
581                    .map(|(value, _)| Variant {
582                        value,
583                        members: Vec::new(),
584                    })
585                    .collect(),
586            }),
587            fields: vec![discriminant],
588            variant: None,
589            refused,
590        }
591    }
592
593    /// Starts the next variant's members.
594    pub fn variant(&mut self) {
595        let next = self.variant.map_or(0, |index| index + 1);
596        let Shape::Enum(e) = &mut self.shape else {
597            panic!("a struct value has no variant");
598        };
599        if self.refused {
600            e.variants.push(Variant {
601                value: next.to_string(),
602                members: Vec::new(),
603            });
604        }
605        assert!(
606            next < e.variants.len(),
607            "the type declares more variants than the app schema"
608        );
609        self.variant = Some(next);
610    }
611
612    fn members_mut(&mut self) -> &mut Vec<Member> {
613        match (&mut self.shape, self.variant) {
614            (Shape::Struct(members), _) => members,
615            (Shape::Enum(e), Some(index)) => &mut e.variants[index].members,
616            (Shape::Enum(_), None) => panic!("an enum member needs `variant()` first"),
617        }
618    }
619
620    /// Adds a leaf, required when it has no blank answer.
621    pub fn leaf(&mut self, field: impl Into<Field>, required: bool) {
622        let mut field = field.into();
623        field.set_required(required);
624        let key = field.name().to_string();
625        let index = self.fields.len();
626        self.fields.push(field);
627        self.members_mut().push(Member::Leaf {
628            key,
629            field: Some(index),
630        });
631    }
632
633    /// Adds a `#[shared(..)]` leaf that renders once, outside the variant groups, and shows only
634    /// while the discriminant names a variant that declares it.
635    pub fn shared(&mut self, field: impl Into<Field>, required: bool) {
636        let mut field = field.into();
637        field.set_required(required);
638        let key = field.name().to_string();
639        let Shape::Enum(e) = &mut self.shape else {
640            panic!("a struct value has no shared column");
641        };
642        if !e
643            .shared
644            .iter()
645            .any(|index| self.fields[*index].name() == key)
646        {
647            e.shared.push(self.fields.len());
648            self.fields.push(field);
649        }
650        self.members_mut().push(Member::Leaf { key, field: None });
651    }
652
653    /// Adds a nested value from its own `build_schema`.
654    pub fn nested(&mut self, schema: Schema) {
655        let Schema { nodes, fields, .. } = schema;
656        let Ok([Node::Embedded(mut nested)]) = <[Node; 1]>::try_from(nodes) else {
657            panic!("a nested value's schema is its one embedded node");
658        };
659        nested.offset(self.fields.len());
660        self.fields.extend(fields);
661        self.members_mut().push(Member::Nested(*nested));
662    }
663
664    /// Finishes the value's schema as one embedded node.
665    pub fn finish(mut self) -> Schema {
666        if let Shape::Enum(e) = &self.shape {
667            assert_eq!(
668                self.variant.map_or(0, |index| index + 1),
669                e.variants.len(),
670                "the type declares fewer variants than the app schema"
671            );
672            // A shared column every variant declares shows for every one of them. One a variant
673            // does not declare shows only while the discriminant names a variant that does, so it
674            // neither renders nor submits there. A submission naming no variant reads the variant
675            // its payload names, so it keeps the column too.
676            for index in &e.shared {
677                let key = self.fields[*index].name().to_string();
678                let declares = |variant: &Variant| {
679                    variant
680                        .members
681                        .iter()
682                        .any(|member| matches!(member, Member::Leaf { key: k, .. } if *k == key))
683                };
684                let declaring: Vec<String> = e
685                    .variants
686                    .iter()
687                    .filter(|variant| declares(variant))
688                    .map(|variant| variant.value.clone())
689                    .collect();
690                if declaring.len() < e.variants.len() {
691                    assert!(
692                        self.fields[*index].condition().is_none(),
693                        "a shared column's visibility comes from the variants that declare it"
694                    );
695                    let values = std::iter::once(String::new()).chain(declaring);
696                    self.fields[*index].set_condition(Condition::watching(e.key.clone(), values));
697                }
698            }
699        }
700        Schema::from_parts(
701            vec![Node::Embedded(Box::new(Embedded { shape: self.shape }))],
702            self.fields,
703        )
704    }
705}
706
707/// The record-form field binding the embedded value at `parent`: every form key it occupies, and
708/// the ones its leaves require.
709#[doc(hidden)]
710pub fn embedded_field<M, T, K>(
711    parent: impl Into<Path<M, T>>,
712    field: K,
713    name: &'static str,
714) -> FormField<K>
715where
716    M: toasty::schema::Model,
717    T: EmbeddedForm,
718{
719    let schema = T::build_schema(parent.into());
720    FormField {
721        field,
722        name,
723        keys: schema.embedded_root().keys(),
724        required: schema
725            .fields()
726            .filter(|field| field.is_required())
727            .map(|field| field.name().to_string())
728            .collect(),
729    }
730}
731
732/// Moves `result`'s value out, or its errors into `errors`.
733#[doc(hidden)]
734pub fn take_leaf<T>(
735    result: std::result::Result<T, FieldError>,
736    errors: &mut Vec<FieldError>,
737) -> Option<T> {
738    result.map_err(|error| errors.push(error)).ok()
739}
740
741#[doc(hidden)]
742pub fn take_value<T>(
743    result: std::result::Result<T, Vec<FieldError>>,
744    errors: &mut Vec<FieldError>,
745) -> Option<T> {
746    result.map_err(|nested| errors.extend(nested)).ok()
747}
748
749#[cfg(test)]
750mod tests;