1use tablo_ui::field_group as ui_field_group;
39use toasty::stmt::Path;
40use topcoat::{
41 Result,
42 context::Cx,
43 runtime::{Event, signal},
44 view::*,
45};
46
47use super::{
48 Schema,
49 condition::Condition,
50 fields::Field,
51 lenses::FieldResolver,
52 tree::{Node, Source, Unbound},
53};
54use crate::{
55 form::{FieldError, FormField, FormValues},
56 topcoat_compat::async_page,
57};
58
59pub trait EmbeddedForm: Sized {
62 fn write_form<M>(&self, cx: &Cx, parent: impl Into<Path<M, Self>>, out: &mut FormValues)
65 where
66 M: toasty::schema::Model,
67 {
68 let schema = Self::build_schema(&FieldResolver::of(cx), parent.into());
69 self.write_node(schema.embedded_root(), out);
70 }
71
72 fn read_form<M>(
80 cx: &Cx,
81 parent: impl Into<Path<M, Self>>,
82 values: &FormValues,
83 ) -> std::result::Result<Self, Vec<FieldError>>
84 where
85 M: toasty::schema::Model,
86 {
87 let schema = Self::build_schema(&FieldResolver::of(cx), parent.into());
88 Self::read_node(schema.embedded_root(), values)
89 }
90
91 #[doc(hidden)]
93 fn build_schema<M>(resolver: &FieldResolver, parent: Path<M, Self>) -> Schema
94 where
95 M: toasty::schema::Model;
96
97 #[doc(hidden)]
98 fn write_node(&self, node: &Embedded, out: &mut FormValues);
99
100 #[doc(hidden)]
101 fn read_node(
102 node: &Embedded,
103 values: &FormValues,
104 ) -> std::result::Result<Self, Vec<FieldError>>;
105}
106
107#[doc(hidden)]
109#[derive(Debug)]
110pub struct Embedded {
111 shape: Shape,
112}
113
114#[derive(Debug)]
115enum Shape {
116 Struct(Vec<Member>),
117 Enum(EnumNode),
118}
119
120#[derive(Debug)]
121struct EnumNode {
122 key: String,
123 discriminant: usize,
124 shared: Vec<usize>,
125 variants: Vec<Variant>,
126}
127
128#[derive(Debug)]
129struct Variant {
130 value: String,
131 members: Vec<Member>,
132}
133
134#[derive(Debug)]
135enum Member {
136 Leaf { key: String, field: Option<usize> },
137 Nested(Embedded),
138}
139
140impl Embedded {
141 fn members(&self, variant: Option<usize>) -> &[Member] {
142 match (&self.shape, variant) {
143 (Shape::Struct(members), None) => members,
144 (Shape::Enum(e), Some(index)) => &e.variants[index].members,
145 _ => panic!("a struct member is addressed without a variant, an enum's with one"),
146 }
147 }
148
149 pub fn key(&self, variant: Option<usize>, index: usize) -> &str {
150 match &self.members(variant)[index] {
151 Member::Leaf { key, .. } => key,
152 Member::Nested(_) => panic!("member {index} is an embedded value, not a leaf"),
153 }
154 }
155
156 pub fn nested(&self, variant: Option<usize>, index: usize) -> &Embedded {
157 match &self.members(variant)[index] {
158 Member::Nested(nested) => nested,
159 Member::Leaf { .. } => panic!("member {index} is a leaf, not an embedded value"),
160 }
161 }
162
163 fn enum_node(&self) -> &EnumNode {
164 match &self.shape {
165 Shape::Enum(e) => e,
166 Shape::Struct(_) => panic!("a struct value has no variant"),
167 }
168 }
169
170 pub fn write_variant(&self, index: usize, out: &mut FormValues) {
171 let e = self.enum_node();
172 out.insert(e.key.clone(), e.variants[index].value.as_str());
173 }
174
175 pub fn variant_index(
178 &self,
179 values: &FormValues,
180 ) -> std::result::Result<usize, Vec<FieldError>> {
181 let e = self.enum_node();
182 let submitted = values.text(&e.key).map(str::trim).unwrap_or_default();
183 if submitted.is_empty() {
184 let own = |member: &Member| match member {
185 Member::Leaf { key, field } => field.is_some() && is_present(values, key),
186 Member::Nested(nested) => nested.any_present(values),
187 };
188 let inferred = e.variants.iter().position(|v| v.members.iter().any(own));
189 return Ok(inferred.unwrap_or(0));
190 }
191 e.variants
192 .iter()
193 .position(|v| v.value == submitted)
194 .ok_or_else(|| {
195 vec![FieldError::invalid(
196 e.key.clone(),
197 format!("`{submitted}` is not a valid variant"),
198 )]
199 })
200 }
201
202 fn any_present(&self, values: &FormValues) -> bool {
204 let member = |member: &Member| match member {
205 Member::Leaf { key, .. } => is_present(values, key),
206 Member::Nested(nested) => nested.any_present(values),
207 };
208 match &self.shape {
209 Shape::Struct(members) => members.iter().any(member),
210 Shape::Enum(e) => {
211 is_present(values, &e.key)
212 || e.variants.iter().any(|v| v.members.iter().any(member))
213 }
214 }
215 }
216
217 pub(crate) fn keys(&self) -> Vec<String> {
219 let mut out = Vec::new();
220 self.collect_keys(&mut out);
221 out
222 }
223
224 fn collect_keys(&self, out: &mut Vec<String>) {
225 let push = |members: &[Member], out: &mut Vec<String>| {
226 for member in members {
227 match member {
228 Member::Leaf { key, .. } if !out.contains(key) => out.push(key.clone()),
229 Member::Leaf { .. } => {}
230 Member::Nested(nested) => nested.collect_keys(out),
231 }
232 }
233 };
234 match &self.shape {
235 Shape::Struct(members) => push(members, out),
236 Shape::Enum(e) => {
237 out.push(e.key.clone());
238 for variant in &e.variants {
239 push(&variant.members, out);
240 }
241 }
242 }
243 }
244
245 pub(crate) fn offset(&mut self, by: usize) {
246 let members = |members: &mut Vec<Member>| {
247 for member in members {
248 match member {
249 Member::Leaf { field, .. } => {
250 if let Some(index) = field {
251 *index += by;
252 }
253 }
254 Member::Nested(nested) => nested.offset(by),
255 }
256 }
257 };
258 match &mut self.shape {
259 Shape::Struct(list) => members(list),
260 Shape::Enum(e) => {
261 e.discriminant += by;
262 for index in &mut e.shared {
263 *index += by;
264 }
265 for variant in &mut e.variants {
266 members(&mut variant.members);
267 }
268 }
269 }
270 }
271
272 pub(crate) fn visit_fields(&self, f: &mut impl FnMut(usize)) {
274 fn members(members: &[Member], f: &mut impl FnMut(usize)) {
275 for member in members {
276 match member {
277 Member::Leaf {
278 field: Some(index), ..
279 } => f(*index),
280 Member::Leaf { field: None, .. } => {}
281 Member::Nested(nested) => nested.visit_fields(f),
282 }
283 }
284 }
285 match &self.shape {
286 Shape::Struct(list) => members(list, f),
287 Shape::Enum(e) => {
288 f(e.discriminant);
289 for index in &e.shared {
290 f(*index);
291 }
292 for variant in &e.variants {
293 members(&variant.members, f);
294 }
295 }
296 }
297 }
298
299 pub(crate) fn hidden_fields(&self, values: &FormValues, out: &mut Vec<usize>) {
302 let nested = |members: &[Member], out: &mut Vec<usize>| {
303 for member in members {
304 if let Member::Nested(nested) = member {
305 nested.hidden_fields(values, out);
306 }
307 }
308 };
309 match &self.shape {
310 Shape::Struct(members) => nested(members, out),
311 Shape::Enum(e) => {
312 let chosen = values.text(&e.key).map(str::trim).unwrap_or_default();
313 for variant in &e.variants {
314 if !chosen.is_empty() && chosen != variant.value {
315 for member in &variant.members {
316 match member {
317 Member::Leaf {
318 field: Some(index), ..
319 } => out.push(*index),
320 Member::Leaf { field: None, .. } => {}
321 Member::Nested(nested) => {
322 nested.visit_fields(&mut |index| out.push(index))
323 }
324 }
325 }
326 } else {
327 nested(&variant.members, out);
328 }
329 }
330 }
331 }
332 }
333
334 pub(crate) async fn render<'a>(
336 &self,
337 cx: &'a Cx,
338 fields: &[Field],
339 source: &Source<'_>,
340 ) -> Result<BoxView<'a>> {
341 match &self.shape {
342 Shape::Struct(members) => render_members(cx, members, fields, source).await,
343 Shape::Enum(e) => {
344 let discriminant = Node::Field(e.discriminant)
345 .render(cx, fields, source)
346 .await?;
347 let stored = source.text(&e.key).map(str::trim);
348 let mut shared = Vec::with_capacity(e.shared.len());
349 for index in &e.shared {
350 shared.push(Node::Field(*index).render(cx, fields, source).await?);
351 }
352 let mut groups = Vec::with_capacity(e.variants.len());
353 for variant in &e.variants {
354 let members = render_members(cx, &variant.members, fields, source).await?;
355 groups.push((variant.value.clone(), members));
356 }
357 let key = e.key.clone();
358 let stored = stored.unwrap_or_default().to_string();
359 let page = topcoat::context::try_request_context::<http::request::Parts>(cx)
367 .map(|parts| parts.uri.path().to_string())
368 .unwrap_or_default();
369 let scope = source.scope().to_string();
370 Ok(async_page(async move {
371 let variant = signal(
372 &cx.keyed(("tablo-variant", page, scope, key.as_str())),
373 move || stored,
374 );
375 let chosen = variant.clone();
376 let groups: Vec<BoxView<'a>> = groups
377 .into_iter()
378 .map(|(value, members)| {
379 let (shown, enabled) = (variant.clone(), variant.clone());
380 let named = value.clone();
381 view! {
382 cx =>
383 <fieldset
384 class="contents"
385 :disabled=$(enabled.get() != named)
386 >
387 ui_field_group(
388 attrs: attributes! {
389 data-variant=(value.clone())
390 :hidden=$(shown.get() != value)
391 },
392 (members)
393 )
394 </fieldset>
395 }
396 .boxed()
397 })
398 .collect();
399 Ok(view! {
400 cx =>
401 <div
402 class="contents"
403 @change=$(|e: Event| chosen.set(e.target.value))
404 >
405 (discriminant)
406 </div>
407 for v in shared {
408 (v)
409 }
410 for g in groups {
411 (g)
412 }
413 })
414 }))
415 }
416 }
417 }
418}
419
420impl Embedded {
421 pub(crate) fn display<'a>(
424 &self,
425 cx: &'a Cx,
426 fields: &[Field],
427 values: &FormValues,
428 ) -> BoxView<'a> {
429 let views: Vec<BoxView<'a>> = self
430 .shown(fields, values)
431 .into_iter()
432 .map(|index| {
433 let field = &fields[index];
434 field.display(cx, values.text(field.name()).unwrap_or_default())
435 })
436 .collect();
437 view! {
438 cx =>
439 for v in views {
440 (v)
441 }
442 }
443 .boxed()
444 }
445
446 pub(crate) fn shown(&self, fields: &[Field], values: &FormValues) -> Vec<usize> {
450 let mut out = Vec::new();
451 self.shown_into(fields, values, &mut out);
452 out
453 }
454
455 fn shown_into(&self, fields: &[Field], values: &FormValues, out: &mut Vec<usize>) {
456 let members = |members: &[Member], out: &mut Vec<usize>| {
457 for member in members {
458 match member {
459 Member::Leaf {
460 field: Some(index), ..
461 } => out.push(*index),
462 Member::Leaf { field: None, .. } => {}
463 Member::Nested(nested) => nested.shown_into(fields, values, out),
464 }
465 }
466 };
467 match &self.shape {
468 Shape::Struct(list) => members(list, out),
469 Shape::Enum(e) => {
470 let stored = values.text(&e.key).map(str::trim);
471 let Some(variant) = e
472 .variants
473 .iter()
474 .find(|variant| stored == Some(variant.value.as_str()))
475 else {
476 return;
477 };
478 out.push(e.discriminant);
479 out.extend(e.shared.iter().copied().filter(|index| {
482 let key = fields[*index].name();
483 variant
484 .members
485 .iter()
486 .any(|member| matches!(member, Member::Leaf { key: k, .. } if k == key))
487 }));
488 members(&variant.members, out);
489 }
490 }
491 }
492}
493
494async fn render_members<'a>(
496 cx: &'a Cx,
497 members: &[Member],
498 fields: &[Field],
499 source: &Source<'_>,
500) -> Result<BoxView<'a>> {
501 let mut views = Vec::with_capacity(members.len());
502 for member in members {
503 match member {
504 Member::Leaf {
505 field: Some(index), ..
506 } => views.push(Node::Field(*index).render(cx, fields, source).await?),
507 Member::Leaf { field: None, .. } => {}
508 Member::Nested(nested) => {
509 views.push(Box::pin(nested.render(cx, fields, source)).await?)
510 }
511 }
512 }
513 Ok(view! {
514 cx =>
515 for v in views {
516 (v)
517 }
518 }
519 .boxed())
520}
521
522fn is_present(values: &FormValues, key: &str) -> bool {
523 values
524 .text(key)
525 .is_some_and(|value| !value.trim().is_empty())
526}
527
528#[doc(hidden)]
531pub struct EmbeddedBuilder {
532 resolver: FieldResolver,
533 fields: Vec<Field>,
534 shape: Shape,
535 variant: Option<usize>,
536}
537
538impl EmbeddedBuilder {
539 pub fn structure(resolver: &FieldResolver) -> Self {
540 Self {
541 resolver: resolver.clone(),
542 fields: Vec::new(),
543 shape: Shape::Struct(Vec::new()),
544 variant: None,
545 }
546 }
547
548 pub fn enumeration<M, T>(resolver: &FieldResolver, parent: Path<M, T>) -> Self
551 where
552 M: toasty::schema::Model,
553 {
554 assert!(
555 resolver.has_schema(),
556 "an embedded enum resolves through the app schema, which a value reaches through the \
557 request's `Db` or the schema it binds to"
558 );
559 let shape = resolver.resolve_enum(parent).unwrap_or_else(|| {
560 panic!(
561 "{} is not an embedded enum in this app schema",
562 std::any::type_name::<T>()
563 )
564 });
565 let variants = shape
566 .variants
567 .iter()
568 .map(|(value, _)| Variant {
569 value: value.clone(),
570 members: Vec::new(),
571 })
572 .collect();
573 Self {
574 resolver: resolver.clone(),
575 fields: vec![Field::discriminant(
576 shape.discriminant.clone(),
577 shape.variants,
578 )],
579 shape: Shape::Enum(EnumNode {
580 key: shape.discriminant,
581 discriminant: 0,
582 shared: Vec::new(),
583 variants,
584 }),
585 variant: None,
586 }
587 }
588
589 pub fn variant(&mut self) {
591 let next = self.variant.map_or(0, |index| index + 1);
592 let Shape::Enum(e) = &self.shape else {
593 panic!("a struct value has no variant");
594 };
595 assert!(
596 next < e.variants.len(),
597 "the type declares more variants than the app schema"
598 );
599 self.variant = Some(next);
600 }
601
602 fn members_mut(&mut self) -> &mut Vec<Member> {
603 match (&mut self.shape, self.variant) {
604 (Shape::Struct(members), _) => members,
605 (Shape::Enum(e), Some(index)) => &mut e.variants[index].members,
606 (Shape::Enum(_), None) => panic!("an enum member needs `variant()` first"),
607 }
608 }
609
610 pub fn leaf(&mut self, field: impl Into<Field>, required: bool) {
612 let mut field = field.into();
613 field.set_required(required);
614 field.bind(&self.resolver);
615 let key = field.name().to_string();
616 let index = self.fields.len();
617 self.fields.push(field);
618 self.members_mut().push(Member::Leaf {
619 key,
620 field: Some(index),
621 });
622 }
623
624 pub fn shared(&mut self, field: impl Into<Field>, required: bool) {
627 let mut field = field.into();
628 field.set_required(required);
629 field.bind(&self.resolver);
630 let key = field.name().to_string();
631 let Shape::Enum(e) = &mut self.shape else {
632 panic!("a struct value has no shared column");
633 };
634 if !e
635 .shared
636 .iter()
637 .any(|index| self.fields[*index].name() == key)
638 {
639 e.shared.push(self.fields.len());
640 self.fields.push(field);
641 }
642 self.members_mut().push(Member::Leaf { key, field: None });
643 }
644
645 pub fn nested(&mut self, schema: Schema) {
647 let Schema { nodes, fields, .. } = schema;
648 let Ok([Node::Embedded(mut nested)]) = <[Node; 1]>::try_from(nodes) else {
649 panic!("a nested value's schema is its one embedded node");
650 };
651 nested.offset(self.fields.len());
652 self.fields.extend(fields);
653 self.members_mut().push(Member::Nested(*nested));
654 }
655
656 pub fn finish(mut self) -> Schema {
658 if let Shape::Enum(e) = &self.shape {
659 assert_eq!(
660 self.variant.map_or(0, |index| index + 1),
661 e.variants.len(),
662 "the type declares fewer variants than the app schema"
663 );
664 for index in &e.shared {
669 let key = self.fields[*index].name().to_string();
670 let declares = |variant: &Variant| {
671 variant
672 .members
673 .iter()
674 .any(|member| matches!(member, Member::Leaf { key: k, .. } if *k == key))
675 };
676 let declaring: Vec<String> = e
677 .variants
678 .iter()
679 .filter(|variant| declares(variant))
680 .map(|variant| variant.value.clone())
681 .collect();
682 if declaring.len() < e.variants.len() {
683 assert!(
684 self.fields[*index].condition().is_none(),
685 "a shared column's visibility comes from the variants that declare it"
686 );
687 let values = std::iter::once(String::new()).chain(declaring);
688 self.fields[*index].set_condition(Condition::watching(e.key.clone(), values));
689 }
690 }
691 }
692 Schema::from_parts(
693 vec![Node::Embedded(Box::new(Embedded { shape: self.shape }))],
694 self.fields,
695 )
696 }
697}
698
699#[doc(hidden)]
702pub fn embedded_field<M, T, K>(
703 resolver: &FieldResolver,
704 parent: impl Into<Path<M, T>>,
705 field: K,
706 name: &'static str,
707) -> FormField<K>
708where
709 M: toasty::schema::Model,
710 T: EmbeddedForm,
711{
712 let schema = T::build_schema(resolver, parent.into());
713 FormField {
714 field,
715 name,
716 keys: schema.embedded_root().keys(),
717 required: schema
718 .fields()
719 .filter(|field| field.is_required())
720 .map(|field| field.name().to_string())
721 .collect(),
722 }
723}
724
725#[doc(hidden)]
727pub fn embedded_form<M, T>(parent: Path<M, T>) -> Schema
728where
729 M: toasty::schema::Model + Send + Sync + 'static,
730 T: EmbeddedForm + Send + Sync + 'static,
731{
732 Schema::from_parts(
733 vec![Node::Unbound(Unbound {
734 build: Box::new(move |resolver| T::build_schema(resolver, parent.clone())),
735 value: std::any::type_name::<T>(),
736 })],
737 Vec::new(),
738 )
739}
740
741#[doc(hidden)]
743pub fn take_leaf<T>(
744 result: std::result::Result<T, FieldError>,
745 errors: &mut Vec<FieldError>,
746) -> Option<T> {
747 result.map_err(|error| errors.push(error)).ok()
748}
749
750#[doc(hidden)]
751pub fn take_value<T>(
752 result: std::result::Result<T, Vec<FieldError>>,
753 errors: &mut Vec<FieldError>,
754) -> Option<T> {
755 result.map_err(|nested| errors.extend(nested)).ok()
756}
757
758#[cfg(test)]
759mod tests;