1use 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
40pub trait EmbeddedForm: Sized {
43 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 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 #[doc(hidden)]
80 fn build_schema<M>(parent: Path<M, Self>) -> Schema
81 where
82 M: toasty::schema::Model;
83
84 #[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#[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 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 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 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 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 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 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
391async 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#[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 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 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 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 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 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#[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#[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#[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;