Skip to main content

tablo_core/schema/
embedded.rs

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