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