Skip to main content

tablo_core/table/column/
embedded.rs

1//! [`EmbeddedColumn`]: an embedded value read off the record, one leaf at a time.
2
3use std::sync::{Arc, OnceLock};
4
5use derive_where::derive_where;
6use topcoat::{context::Cx, view::*};
7
8use super::{Column, ColumnBase, ColumnWidth};
9use crate::{
10    Lens,
11    form::FormValues,
12    schema::{EmbeddedForm, FieldResolver, Schema},
13};
14
15/// A column of an [`EmbeddedForm`] value.
16///
17/// A detail page shows each of the value's leaves under its own label; an enum shows its variant's
18/// name and that variant's leaves only. A table cell lists the same leaves as `label: value`.
19///
20/// ```rust
21/// # #[derive(Debug, Clone, toasty::Embed, tablo_core::EmbeddedForm)]
22/// # struct Seo { title: String, description: String }
23/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
24/// # struct Post { #[key] #[auto] id: uuid::Uuid, seo: Seo }
25/// tablo_core::EmbeddedColumn::new(tablo_core::lens!(Post.seo));
26/// ```
27///
28/// The value's leaves resolve through the app schema, which the column reads when it binds: a
29/// panel binds the declarations it mounts, and [`Detail::bind`](crate::Detail::bind) binds one
30/// built outside a panel.
31#[derive_where(Clone, Debug)]
32pub struct EmbeddedColumn<M, T> {
33    lens: Lens<M, T>,
34    base: ColumnBase,
35    /// The value's schema, built when the column binds.
36    schema: Arc<OnceLock<Schema>>,
37}
38
39impl<M, T> EmbeddedColumn<M, T>
40where
41    M: toasty::schema::Model,
42    T: EmbeddedForm,
43{
44    /// Bind the column to the embedded value `lens` reads.
45    pub fn new(lens: Lens<M, T>) -> Self {
46        Self {
47            base: ColumnBase::new(lens.path(), ColumnWidth::Wide),
48            lens,
49            schema: Arc::default(),
50        }
51    }
52
53    base_builders!();
54
55    /// The value's schema and its leaves as the form spells them, once the column is bound.
56    fn spelled(&self, row: &M) -> Option<(&Schema, FormValues)> {
57        let schema = self.schema.get()?;
58        let mut values = FormValues::new();
59        self.lens
60            .read(row)
61            .write_node(schema.embedded_root(), &mut values);
62        Some((schema, values))
63    }
64}
65
66impl<M, T> Column<M> for EmbeddedColumn<M, T>
67where
68    M: toasty::schema::Model + Send + Sync + 'static,
69    T: EmbeddedForm + Send + Sync + 'static,
70{
71    base_column_methods!();
72
73    fn text(&self, _cx: &Cx, row: &M) -> String {
74        let Some((schema, values)) = self.spelled(row) else {
75            return String::new();
76        };
77        schema
78            .embedded_root()
79            .shown(&schema.fields, &values)
80            .into_iter()
81            .filter_map(|index| {
82                let field = &schema.fields[index];
83                let value = values.text(field.name()).unwrap_or_default();
84                let read = field.read(value)?;
85                Some(format!("{}: {read}", field.label_str()))
86            })
87            .collect::<Vec<_>>()
88            .join(", ")
89    }
90
91    fn entry<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
92        match self.spelled(row) {
93            Some((schema, values)) => schema.embedded_root().display(cx, &schema.fields, &values),
94            None => ().boxed(),
95        }
96    }
97
98    fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
99        if let Some(error) = self.base.binding.misdeclared() {
100            return Some(error);
101        }
102        match self.schema.get() {
103            Some(schema) => schema.declaration_errors().into_iter().next(),
104            None => Some(crate::DeclarationErrorKind::Unbound {
105                item: std::any::type_name::<T>(),
106            }),
107        }
108    }
109
110    fn bind(&self, resolver: &FieldResolver) {
111        self.base.binding.bind(resolver);
112        self.schema
113            .get_or_init(|| T::build_schema(resolver, self.lens.path().clone()));
114    }
115}