1use std::collections::HashMap;
27
28use tablo_ui::field_group as ui_field_group;
29use toasty::stmt::Path;
30use topcoat::{
31 Result,
32 context::Cx,
33 runtime::{Event, signal},
34 view::*,
35};
36
37use super::{
38 Schema,
39 fields::Field,
40 lenses::FieldResolver,
41 tree::{Mode, Node, Source, Unbound},
42};
43use crate::{
44 form::{FieldError, FormField},
45 topcoat_compat::async_page,
46};
47
48pub trait EmbeddedForm: Sized {
51 fn write_form<M>(
54 &self,
55 cx: &Cx,
56 parent: impl Into<Path<M, Self>>,
57 out: &mut HashMap<String, String>,
58 ) where
59 M: toasty::schema::Model,
60 {
61 let schema = Self::build_schema(&FieldResolver::of(cx), parent.into());
62 self.write_node(schema.embedded_root(), out);
63 }
64
65 fn read_form<M>(
73 cx: &Cx,
74 parent: impl Into<Path<M, Self>>,
75 values: &HashMap<String, String>,
76 ) -> std::result::Result<Self, Vec<FieldError>>
77 where
78 M: toasty::schema::Model,
79 {
80 let schema = Self::build_schema(&FieldResolver::of(cx), parent.into());
81 Self::read_node(schema.embedded_root(), values)
82 }
83
84 #[doc(hidden)]
86 fn build_schema<M>(resolver: &FieldResolver, parent: Path<M, Self>) -> Schema
87 where
88 M: toasty::schema::Model;
89
90 #[doc(hidden)]
91 fn write_node(&self, node: &Embedded, out: &mut HashMap<String, String>);
92
93 #[doc(hidden)]
94 fn read_node(
95 node: &Embedded,
96 values: &HashMap<String, String>,
97 ) -> std::result::Result<Self, Vec<FieldError>>;
98}
99
100#[doc(hidden)]
102#[derive(Debug)]
103pub struct Embedded {
104 shape: Shape,
105}
106
107#[derive(Debug)]
108enum Shape {
109 Struct(Vec<Member>),
110 Enum(EnumNode),
111}
112
113#[derive(Debug)]
114struct EnumNode {
115 key: String,
116 discriminant: usize,
117 shared: Vec<usize>,
118 variants: Vec<Variant>,
119}
120
121#[derive(Debug)]
122struct Variant {
123 value: String,
124 members: Vec<Member>,
125}
126
127#[derive(Debug)]
128enum Member {
129 Leaf { key: String, field: Option<usize> },
130 Nested(Embedded),
131}
132
133impl Embedded {
134 fn members(&self, variant: Option<usize>) -> &[Member] {
135 match (&self.shape, variant) {
136 (Shape::Struct(members), None) => members,
137 (Shape::Enum(e), Some(index)) => &e.variants[index].members,
138 _ => panic!("a struct member is addressed without a variant, an enum's with one"),
139 }
140 }
141
142 pub fn key(&self, variant: Option<usize>, index: usize) -> &str {
143 match &self.members(variant)[index] {
144 Member::Leaf { key, .. } => key,
145 Member::Nested(_) => panic!("member {index} is an embedded value, not a leaf"),
146 }
147 }
148
149 pub fn nested(&self, variant: Option<usize>, index: usize) -> &Embedded {
150 match &self.members(variant)[index] {
151 Member::Nested(nested) => nested,
152 Member::Leaf { .. } => panic!("member {index} is a leaf, not an embedded value"),
153 }
154 }
155
156 fn enum_node(&self) -> &EnumNode {
157 match &self.shape {
158 Shape::Enum(e) => e,
159 Shape::Struct(_) => panic!("a struct value has no variant"),
160 }
161 }
162
163 pub fn write_variant(&self, index: usize, out: &mut HashMap<String, String>) {
164 let e = self.enum_node();
165 out.insert(e.key.clone(), e.variants[index].value.clone());
166 }
167
168 pub fn variant_index(
171 &self,
172 values: &HashMap<String, String>,
173 ) -> std::result::Result<usize, Vec<FieldError>> {
174 let e = self.enum_node();
175 let submitted = values.get(&e.key).map(|v| v.trim()).unwrap_or_default();
176 if submitted.is_empty() {
177 let own = |member: &Member| match member {
178 Member::Leaf { key, field } => field.is_some() && is_present(values, key),
179 Member::Nested(nested) => nested.any_present(values),
180 };
181 let inferred = e.variants.iter().position(|v| v.members.iter().any(own));
182 return Ok(inferred.unwrap_or(0));
183 }
184 e.variants
185 .iter()
186 .position(|v| v.value == submitted)
187 .ok_or_else(|| {
188 vec![FieldError::invalid(
189 e.key.clone(),
190 format!("`{submitted}` is not a valid variant"),
191 )]
192 })
193 }
194
195 fn any_present(&self, values: &HashMap<String, String>) -> bool {
197 let member = |member: &Member| match member {
198 Member::Leaf { key, .. } => is_present(values, key),
199 Member::Nested(nested) => nested.any_present(values),
200 };
201 match &self.shape {
202 Shape::Struct(members) => members.iter().any(member),
203 Shape::Enum(e) => {
204 is_present(values, &e.key)
205 || e.variants.iter().any(|v| v.members.iter().any(member))
206 }
207 }
208 }
209
210 pub(crate) fn keys(&self) -> Vec<String> {
212 let mut out = Vec::new();
213 self.collect_keys(&mut out);
214 out
215 }
216
217 fn collect_keys(&self, out: &mut Vec<String>) {
218 let push = |members: &[Member], out: &mut Vec<String>| {
219 for member in members {
220 match member {
221 Member::Leaf { key, .. } if !out.contains(key) => out.push(key.clone()),
222 Member::Leaf { .. } => {}
223 Member::Nested(nested) => nested.collect_keys(out),
224 }
225 }
226 };
227 match &self.shape {
228 Shape::Struct(members) => push(members, out),
229 Shape::Enum(e) => {
230 out.push(e.key.clone());
231 for variant in &e.variants {
232 push(&variant.members, out);
233 }
234 }
235 }
236 }
237
238 pub(crate) fn offset(&mut self, by: usize) {
239 let members = |members: &mut Vec<Member>| {
240 for member in members {
241 match member {
242 Member::Leaf { field, .. } => {
243 if let Some(index) = field {
244 *index += by;
245 }
246 }
247 Member::Nested(nested) => nested.offset(by),
248 }
249 }
250 };
251 match &mut self.shape {
252 Shape::Struct(list) => members(list),
253 Shape::Enum(e) => {
254 e.discriminant += by;
255 for index in &mut e.shared {
256 *index += by;
257 }
258 for variant in &mut e.variants {
259 members(&mut variant.members);
260 }
261 }
262 }
263 }
264
265 fn visit_fields(&self, f: &mut impl FnMut(usize)) {
267 fn members(members: &[Member], f: &mut impl FnMut(usize)) {
268 for member in members {
269 match member {
270 Member::Leaf {
271 field: Some(index), ..
272 } => f(*index),
273 Member::Leaf { field: None, .. } => {}
274 Member::Nested(nested) => nested.visit_fields(f),
275 }
276 }
277 }
278 match &self.shape {
279 Shape::Struct(list) => members(list, f),
280 Shape::Enum(e) => {
281 f(e.discriminant);
282 for index in &e.shared {
283 f(*index);
284 }
285 for variant in &e.variants {
286 members(&variant.members, f);
287 }
288 }
289 }
290 }
291
292 pub(crate) fn hidden_fields(&self, values: &HashMap<String, String>, out: &mut Vec<usize>) {
295 let nested = |members: &[Member], out: &mut Vec<usize>| {
296 for member in members {
297 if let Member::Nested(nested) = member {
298 nested.hidden_fields(values, out);
299 }
300 }
301 };
302 match &self.shape {
303 Shape::Struct(members) => nested(members, out),
304 Shape::Enum(e) => {
305 let chosen = values.get(&e.key).map(|v| v.trim()).unwrap_or_default();
306 for variant in &e.variants {
307 if !chosen.is_empty() && chosen != variant.value {
308 for member in &variant.members {
309 match member {
310 Member::Leaf {
311 field: Some(index), ..
312 } => out.push(*index),
313 Member::Leaf { field: None, .. } => {}
314 Member::Nested(nested) => {
315 nested.visit_fields(&mut |index| out.push(index))
316 }
317 }
318 }
319 } else {
320 nested(&variant.members, out);
321 }
322 }
323 }
324 }
325 }
326
327 pub(crate) async fn render<'a>(
330 &self,
331 cx: &'a Cx,
332 fields: &[Field],
333 source: &Source<'_>,
334 ) -> Result<BoxView<'a>> {
335 match &self.shape {
336 Shape::Struct(members) => render_members(cx, members, fields, source).await,
337 Shape::Enum(e) => {
338 let discriminant = Node::Field(e.discriminant)
339 .render(cx, fields, source)
340 .await?;
341 let stored = source.value(&e.key).map(str::trim);
342 let stored_variant = e
343 .variants
344 .iter()
345 .find(|variant| stored == Some(variant.value.as_str()));
346 let mut shared = Vec::with_capacity(e.shared.len());
347 for index in &e.shared {
348 let key = fields[*index].name();
349 let declared = stored_variant.is_some_and(|variant| {
350 variant
351 .members
352 .iter()
353 .any(|member| matches!(member, Member::Leaf { key: k, .. } if k == key))
354 });
355 if source.mode() == Mode::View && !declared {
356 continue;
357 }
358 shared.push(Node::Field(*index).render(cx, fields, source).await?);
359 }
360 let mut groups = Vec::with_capacity(e.variants.len());
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 groups.push((variant.value.clone(), members));
367 }
368 let key = e.key.clone();
369 let stored = stored.unwrap_or_default().to_string();
370 let page = topcoat::context::try_request_context::<http::request::Parts>(cx)
375 .map(|parts| parts.uri.path().to_string())
376 .unwrap_or_default();
377 Ok(async_page(async move {
378 let variant = signal(
379 &cx.keyed(("tablo-variant", page, key.as_str())),
380 move || stored,
381 );
382 let chosen = variant.clone();
383 let groups: Vec<BoxView<'a>> = groups
384 .into_iter()
385 .map(|(value, members)| {
386 let shown = variant.clone();
387 view! {
388 cx =>
389 ui_field_group(
390 attrs: attributes! {
391 data-variant=(value.clone())
392 :hidden=$(shown.get() != value)
393 },
394 (members)
395 )
396 }
397 .boxed()
398 })
399 .collect();
400 Ok(view! {
401 cx =>
402 <div
403 class="contents"
404 @change=$(|e: Event| chosen.set(e.target.value))
405 >
406 (discriminant)
407 </div>
408 for v in shared {
409 (v)
410 }
411 for g in groups {
412 (g)
413 }
414 })
415 }))
416 }
417 }
418 }
419}
420
421async fn render_members<'a>(
423 cx: &'a Cx,
424 members: &[Member],
425 fields: &[Field],
426 source: &Source<'_>,
427) -> Result<BoxView<'a>> {
428 let mut views = Vec::with_capacity(members.len());
429 for member in members {
430 match member {
431 Member::Leaf {
432 field: Some(index), ..
433 } => views.push(Node::Field(*index).render(cx, fields, source).await?),
434 Member::Leaf { field: None, .. } => {}
435 Member::Nested(nested) => {
436 views.push(Box::pin(nested.render(cx, fields, source)).await?)
437 }
438 }
439 }
440 Ok(view! {
441 cx =>
442 for v in views {
443 (v)
444 }
445 }
446 .boxed())
447}
448
449fn is_present(values: &HashMap<String, String>, key: &str) -> bool {
450 values
451 .get(key)
452 .is_some_and(|value| !value.trim().is_empty())
453}
454
455#[doc(hidden)]
458pub struct EmbeddedBuilder {
459 resolver: FieldResolver,
460 fields: Vec<Field>,
461 shape: Shape,
462 variant: Option<usize>,
463}
464
465impl EmbeddedBuilder {
466 pub fn structure(resolver: &FieldResolver) -> Self {
467 Self {
468 resolver: resolver.clone(),
469 fields: Vec::new(),
470 shape: Shape::Struct(Vec::new()),
471 variant: None,
472 }
473 }
474
475 pub fn enumeration<M, T>(resolver: &FieldResolver, parent: Path<M, T>) -> Self
478 where
479 M: toasty::schema::Model,
480 {
481 assert!(
482 resolver.has_schema(),
483 "an embedded enum resolves through the app schema, which a value reaches through the \
484 request's `Db` or the schema it binds to"
485 );
486 let shape = resolver.resolve_enum(parent).unwrap_or_else(|| {
487 panic!(
488 "{} is not an embedded enum in this app schema",
489 std::any::type_name::<T>()
490 )
491 });
492 let variants = shape
493 .variants
494 .iter()
495 .map(|(value, _)| Variant {
496 value: value.clone(),
497 members: Vec::new(),
498 })
499 .collect();
500 Self {
501 resolver: resolver.clone(),
502 fields: vec![Field::discriminant(
503 shape.discriminant.clone(),
504 shape.variants,
505 )],
506 shape: Shape::Enum(EnumNode {
507 key: shape.discriminant,
508 discriminant: 0,
509 shared: Vec::new(),
510 variants,
511 }),
512 variant: None,
513 }
514 }
515
516 pub fn variant(&mut self) {
518 let next = self.variant.map_or(0, |index| index + 1);
519 let Shape::Enum(e) = &self.shape else {
520 panic!("a struct value has no variant");
521 };
522 assert!(
523 next < e.variants.len(),
524 "the type declares more variants than the app schema"
525 );
526 self.variant = Some(next);
527 }
528
529 fn members_mut(&mut self) -> &mut Vec<Member> {
530 match (&mut self.shape, self.variant) {
531 (Shape::Struct(members), _) => members,
532 (Shape::Enum(e), Some(index)) => &mut e.variants[index].members,
533 (Shape::Enum(_), None) => panic!("an enum member needs `variant()` first"),
534 }
535 }
536
537 pub fn leaf(&mut self, field: impl Into<Field>, required: bool) {
539 let mut field = field.into();
540 field.set_required(required);
541 field.bind(&self.resolver);
542 let key = field.name().to_string();
543 let index = self.fields.len();
544 self.fields.push(field);
545 self.members_mut().push(Member::Leaf {
546 key,
547 field: Some(index),
548 });
549 }
550
551 pub fn shared(&mut self, field: impl Into<Field>, required: bool) {
553 let mut field = field.into();
554 field.set_required(required);
555 field.bind(&self.resolver);
556 let key = field.name().to_string();
557 let Shape::Enum(e) = &mut self.shape else {
558 panic!("a struct value has no shared column");
559 };
560 if !e
561 .shared
562 .iter()
563 .any(|index| self.fields[*index].name() == key)
564 {
565 e.shared.push(self.fields.len());
566 self.fields.push(field);
567 }
568 self.members_mut().push(Member::Leaf { key, field: None });
569 }
570
571 pub fn nested(&mut self, schema: Schema) {
573 let Schema { nodes, fields } = schema;
574 let Ok([Node::Embedded(mut nested)]) = <[Node; 1]>::try_from(nodes) else {
575 panic!("a nested value's schema is its one embedded node");
576 };
577 nested.offset(self.fields.len());
578 self.fields.extend(fields);
579 self.members_mut().push(Member::Nested(*nested));
580 }
581
582 pub fn finish(self) -> Schema {
584 if let Shape::Enum(e) = &self.shape {
585 assert_eq!(
586 self.variant.map_or(0, |index| index + 1),
587 e.variants.len(),
588 "the type declares fewer variants than the app schema"
589 );
590 }
591 Schema {
592 nodes: vec![Node::Embedded(Box::new(Embedded { shape: self.shape }))],
593 fields: self.fields,
594 }
595 }
596}
597
598#[doc(hidden)]
601pub fn embedded_field<M, T, K>(
602 resolver: &FieldResolver,
603 parent: impl Into<Path<M, T>>,
604 field: K,
605 name: &'static str,
606) -> FormField<K>
607where
608 M: toasty::schema::Model,
609 T: EmbeddedForm,
610{
611 let schema = T::build_schema(resolver, parent.into());
612 FormField {
613 field,
614 name,
615 keys: schema.embedded_root().keys(),
616 required: schema
617 .fields()
618 .filter(|field| field.is_required())
619 .map(|field| field.name().to_string())
620 .collect(),
621 }
622}
623
624#[doc(hidden)]
626pub fn embedded_form<M, T>(parent: Path<M, T>) -> Schema
627where
628 M: toasty::schema::Model + Send + Sync + 'static,
629 T: EmbeddedForm + Send + Sync + 'static,
630{
631 Schema {
632 nodes: vec![Node::Unbound(Unbound {
633 build: Box::new(move |resolver| T::build_schema(resolver, parent.clone())),
634 value: std::any::type_name::<T>(),
635 })],
636 fields: Vec::new(),
637 }
638}
639
640#[doc(hidden)]
642pub fn take_leaf<T>(
643 result: std::result::Result<T, FieldError>,
644 errors: &mut Vec<FieldError>,
645) -> Option<T> {
646 result.map_err(|error| errors.push(error)).ok()
647}
648
649#[doc(hidden)]
650pub fn take_value<T>(
651 result: std::result::Result<T, Vec<FieldError>>,
652 errors: &mut Vec<FieldError>,
653) -> Option<T> {
654 result.map_err(|nested| errors.extend(nested)).ok()
655}
656
657#[cfg(test)]
658mod tests;