1use 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
61pub trait EmbeddedForm: Sized {
64 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 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 #[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#[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 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 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 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 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 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 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 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 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 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 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
500async 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#[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 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 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 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 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 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 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#[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#[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#[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;