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::{
4    collections::HashMap,
5    sync::{Arc, OnceLock},
6};
7
8use topcoat::{context::Cx, view::*};
9
10use super::{Column, ColumnWidth};
11use crate::{
12    Lens,
13    schema::{Binding, EmbeddedForm, FieldResolver, Schema},
14};
15
16/// A column of an [`EmbeddedForm`] value.
17///
18/// A detail page shows each of the value's leaves under its own label; an enum shows its variant's
19/// name and that variant's leaves only. A table cell lists the same leaves as `label: value`.
20///
21/// ```rust
22/// # #[derive(Debug, Clone, toasty::Embed, tablo_core::EmbeddedForm)]
23/// # struct Seo { title: String, description: String }
24/// # #[derive(Debug, Clone, toasty::Model)]
25/// # struct Post { #[key] #[auto] id: uuid::Uuid, seo: Seo }
26/// tablo_core::EmbeddedColumn::new(tablo_core::lens!(Post.seo));
27/// ```
28///
29/// The value's leaves resolve through the app schema, which the column reads when it binds: a
30/// panel binds the declarations it mounts, and [`Detail::bind`](crate::Detail::bind) binds one
31/// built outside a panel.
32pub struct EmbeddedColumn<M, T> {
33    lens: Lens<M, T>,
34    binding: Binding,
35    /// The declared label, over the binding's.
36    label: Option<String>,
37    /// The value's schema, built when the column binds.
38    schema: Arc<OnceLock<Schema>>,
39    width: ColumnWidth,
40}
41
42impl<M, T> EmbeddedColumn<M, T>
43where
44    M: toasty::schema::Model,
45    T: EmbeddedForm,
46{
47    /// Bind the column to the embedded value `lens` reads.
48    pub fn new(lens: Lens<M, T>) -> Self {
49        let binding = Binding::of(lens.path());
50        Self {
51            lens,
52            binding,
53            label: None,
54            schema: Arc::default(),
55            width: ColumnWidth::Wide,
56        }
57    }
58
59    /// Replace the label the field's name gives it.
60    pub fn label(mut self, label: impl Into<String>) -> Self {
61        self.label = Some(label.into());
62        self
63    }
64
65    /// Declare this column's width.
66    pub fn width(mut self, width: ColumnWidth) -> Self {
67        self.width = width;
68        self
69    }
70
71    /// The value's schema and its leaves as the form spells them, once the column is bound.
72    fn spelled(&self, row: &M) -> Option<(&Schema, HashMap<String, String>)> {
73        let schema = self.schema.get()?;
74        let mut values = HashMap::new();
75        self.lens
76            .read(row)
77            .write_node(schema.embedded_root(), &mut values);
78        Some((schema, values))
79    }
80}
81
82impl<M, T> Column<M> for EmbeddedColumn<M, T>
83where
84    M: toasty::schema::Model + Send + Sync + 'static,
85    T: EmbeddedForm + Send + Sync + 'static,
86{
87    fn name(&self) -> &str {
88        self.binding.name()
89    }
90
91    fn label(&self) -> &str {
92        self.label.as_deref().unwrap_or(self.binding.label())
93    }
94
95    fn text(&self, _cx: &Cx, row: &M) -> String {
96        let Some((schema, values)) = self.spelled(row) else {
97            return String::new();
98        };
99        schema
100            .embedded_root()
101            .shown(&schema.fields, &values)
102            .into_iter()
103            .filter_map(|index| {
104                let field = &schema.fields[index];
105                let value = values.get(field.name()).map_or("", String::as_str);
106                let read = field.read(value)?;
107                Some(format!("{}: {read}", field.label_str()))
108            })
109            .collect::<Vec<_>>()
110            .join(", ")
111    }
112
113    fn entry<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
114        match self.spelled(row) {
115            Some((schema, values)) => schema.embedded_root().display(cx, &schema.fields, &values),
116            None => ().boxed(),
117        }
118    }
119
120    fn column_width(&self) -> ColumnWidth {
121        self.width
122    }
123
124    fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
125        if let Some(error) = self.binding.misdeclared() {
126            return Some(error);
127        }
128        match self.schema.get() {
129            Some(schema) => schema.declaration_errors().into_iter().next(),
130            None => Some(crate::DeclarationErrorKind::Unbound {
131                item: std::any::type_name::<T>(),
132            }),
133        }
134    }
135
136    fn bind(&self, resolver: &FieldResolver) {
137        self.binding.bind(resolver);
138        self.schema
139            .get_or_init(|| T::build_schema(resolver, self.lens.path().clone()));
140    }
141}
142
143impl<M, T> Clone for EmbeddedColumn<M, T> {
144    fn clone(&self) -> Self {
145        Self {
146            lens: self.lens.clone(),
147            binding: self.binding.clone(),
148            label: self.label.clone(),
149            schema: Arc::clone(&self.schema),
150            width: self.width,
151        }
152    }
153}
154
155impl<M, T> std::fmt::Debug for EmbeddedColumn<M, T> {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        f.debug_struct("EmbeddedColumn")
158            .field("name", &self.binding.name())
159            .field(
160                "label",
161                &self.label.as_deref().unwrap_or(self.binding.label()),
162            )
163            .field("value", &std::any::type_name::<T>())
164            .field("bound", &self.schema.get().is_some())
165            .finish_non_exhaustive()
166    }
167}