Skip to main content

voce_schema/
lib.rs

1//! Voce IR Schema — FlatBuffers type definitions and generated bindings.
2//!
3//! This crate contains the IR schema definitions and the Rust bindings
4//! generated from FlatBuffers `.fbs` files. The schema defines every
5//! node type in the Voce IR: layout, state, motion, navigation,
6//! accessibility, theming, data, forms, SEO, and i18n.
7//!
8//! # Architecture
9//!
10//! The schema is the contract between the AI generation layer and the
11//! compilation pipeline. Any tool that produces JSON conforming to this
12//! schema can generate valid Voce IR.
13//!
14//! FlatBuffers `.fbs` files live in `schemas/` and are compiled to Rust
15//! bindings in `src/generated/` via `flatc`.
16//!
17//! # Regenerating Bindings
18//!
19//! ```bash
20//! ./scripts/regenerate-schema.sh
21//! ```
22//!
23//! The script combines all `.fbs` files into a single compilation unit
24//! to avoid FlatBuffers cross-module codegen issues, then compiles to Rust.
25
26// All domain schemas are combined into a single generated file by the
27// regeneration script. Individual .fbs files remain the source of truth
28// for editing; the combined file is a build artifact.
29#[allow(
30    unused_imports,
31    dead_code,
32    clippy::all,
33    mismatched_lifetime_syntaxes,
34    non_snake_case,
35    non_camel_case_types,
36    missing_docs,
37    unsafe_op_in_unsafe_fn
38)]
39mod generated {
40    include!("generated/_combined_generated.rs");
41}
42
43pub mod errors;
44
45/// All Voce IR types — re-exported for ergonomic access.
46///
47/// ```ignore
48/// use voce_schema::voce::*;
49///
50/// // Layout
51/// let _ = ContainerLayout::Flex;
52/// let _ = FontWeight::Bold;
53///
54/// // State
55/// let _ = CacheStrategy::StaleWhileRevalidate;
56///
57/// // Motion
58/// let _ = GestureType::Tap;
59/// let _ = ReducedMotionStrategy::Simplify;
60///
61/// // Navigation
62/// let _ = RouteTransitionType::SharedElement;
63/// ```
64pub use generated::voce;
65
66#[cfg(test)]
67mod tests {
68    use super::voce::*;
69    use flatbuffers::FlatBufferBuilder;
70
71    #[test]
72    fn build_and_read_minimal_document() {
73        let mut builder = FlatBufferBuilder::new();
74
75        let content = builder.create_string("Hello, Voce");
76        let node_id = builder.create_string("heading");
77        let text_node = TextNode::create(
78            &mut builder,
79            &TextNodeArgs {
80                node_id: Some(node_id),
81                content: Some(content),
82                font_weight: FontWeight::Bold,
83                heading_level: 1,
84                line_height: 1.5,
85                opacity: 1.0,
86                ..Default::default()
87            },
88        );
89
90        let child = ChildNode::create(
91            &mut builder,
92            &ChildNodeArgs {
93                value_type: ChildUnion::TextNode,
94                value: Some(text_node.as_union_value()),
95            },
96        );
97        let children = builder.create_vector(&[child]);
98
99        let root_id = builder.create_string("root");
100        let lang = builder.create_string("en");
101        let root = ViewRoot::create(
102            &mut builder,
103            &ViewRootArgs {
104                node_id: Some(root_id),
105                children: Some(children),
106                document_language: Some(lang),
107                ..Default::default()
108            },
109        );
110
111        let doc = VoceDocument::create(
112            &mut builder,
113            &VoceDocumentArgs {
114                schema_version_major: 0,
115                schema_version_minor: 1,
116                root: Some(root),
117                ..Default::default()
118            },
119        );
120
121        builder.finish(doc, Some("VOCE"));
122        let buf = builder.finished_data();
123
124        let doc = flatbuffers::root::<VoceDocument>(buf).expect("valid FlatBuffer");
125        assert_eq!(doc.schema_version_major(), 0);
126        assert_eq!(doc.schema_version_minor(), 1);
127
128        let root = doc.root();
129        assert_eq!(root.node_id(), "root");
130        assert_eq!(root.document_language(), Some("en"));
131
132        let children = root.children().expect("has children");
133        assert_eq!(children.len(), 1);
134
135        let child = children.get(0);
136        assert_eq!(child.value_type(), ChildUnion::TextNode);
137
138        let text = child.value_as_text_node().expect("is TextNode");
139        assert_eq!(text.node_id(), "heading");
140        assert_eq!(text.content(), Some("Hello, Voce"));
141        assert_eq!(text.font_weight(), FontWeight::Bold);
142        assert_eq!(text.heading_level(), 1);
143    }
144
145    #[test]
146    fn verify_file_identifier() {
147        let mut builder = FlatBufferBuilder::new();
148
149        let root_id = builder.create_string("root");
150        let root = ViewRoot::create(
151            &mut builder,
152            &ViewRootArgs {
153                node_id: Some(root_id),
154                ..Default::default()
155            },
156        );
157        let doc = VoceDocument::create(
158            &mut builder,
159            &VoceDocumentArgs {
160                root: Some(root),
161                ..Default::default()
162            },
163        );
164
165        builder.finish(doc, Some("VOCE"));
166        let buf = builder.finished_data();
167
168        assert!(flatbuffers::buffer_has_identifier(buf, "VOCE", false));
169    }
170
171    #[test]
172    fn container_with_layout_properties() {
173        let mut builder = FlatBufferBuilder::new();
174
175        let node_id = builder.create_string("main");
176        let gap = Length::create(
177            &mut builder,
178            &LengthArgs {
179                value: 16.0,
180                unit: LengthUnit::Px,
181            },
182        );
183
184        let container = Container::create(
185            &mut builder,
186            &ContainerArgs {
187                node_id: Some(node_id),
188                layout: ContainerLayout::Flex,
189                direction: LayoutDirection::Row,
190                main_align: Alignment::SpaceBetween,
191                cross_align: Alignment::Center,
192                gap: Some(gap),
193                wrap: true,
194                opacity: 1.0,
195                ..Default::default()
196            },
197        );
198
199        let child = ChildNode::create(
200            &mut builder,
201            &ChildNodeArgs {
202                value_type: ChildUnion::Container,
203                value: Some(container.as_union_value()),
204            },
205        );
206        let children = builder.create_vector(&[child]);
207
208        let root_id = builder.create_string("root");
209        let root = ViewRoot::create(
210            &mut builder,
211            &ViewRootArgs {
212                node_id: Some(root_id),
213                children: Some(children),
214                ..Default::default()
215            },
216        );
217
218        let doc = VoceDocument::create(
219            &mut builder,
220            &VoceDocumentArgs {
221                root: Some(root),
222                ..Default::default()
223            },
224        );
225
226        builder.finish(doc, Some("VOCE"));
227        let buf = builder.finished_data();
228
229        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
230        let children = doc.root().children().unwrap();
231        let container = children.get(0).value_as_container().unwrap();
232
233        assert_eq!(container.node_id(), "main");
234        assert_eq!(container.layout(), ContainerLayout::Flex);
235        assert_eq!(container.direction(), LayoutDirection::Row);
236        assert_eq!(container.main_align(), Alignment::SpaceBetween);
237        assert_eq!(container.cross_align(), Alignment::Center);
238        assert!(container.wrap());
239
240        let gap = container.gap().unwrap();
241        assert_eq!(gap.value(), 16.0);
242        assert_eq!(gap.unit(), LengthUnit::Px);
243    }
244
245    #[test]
246    fn state_machine_creation() {
247        let mut builder = FlatBufferBuilder::new();
248
249        let idle = builder.create_string("idle");
250        let loading = builder.create_string("loading");
251        let loaded = builder.create_string("loaded");
252
253        let state_idle = State::create(
254            &mut builder,
255            &StateArgs {
256                name: Some(idle),
257                initial: true,
258                terminal: false,
259            },
260        );
261        let state_loading = State::create(
262            &mut builder,
263            &StateArgs {
264                name: Some(loading),
265                initial: false,
266                terminal: false,
267            },
268        );
269        let state_loaded = State::create(
270            &mut builder,
271            &StateArgs {
272                name: Some(loaded),
273                initial: false,
274                terminal: true,
275            },
276        );
277        let states = builder.create_vector(&[state_idle, state_loading, state_loaded]);
278
279        // Transitions
280        let ev_click = builder.create_string("click");
281        let ev_resolve = builder.create_string("resolve");
282        let from_idle = builder.create_string("idle");
283        let to_loading = builder.create_string("loading");
284        let from_loading = builder.create_string("loading");
285        let to_loaded = builder.create_string("loaded");
286
287        let t1 = Transition::create(
288            &mut builder,
289            &TransitionArgs {
290                event: Some(ev_click),
291                from: Some(from_idle),
292                to: Some(to_loading),
293                ..Default::default()
294            },
295        );
296        let t2 = Transition::create(
297            &mut builder,
298            &TransitionArgs {
299                event: Some(ev_resolve),
300                from: Some(from_loading),
301                to: Some(to_loaded),
302                ..Default::default()
303            },
304        );
305        let transitions = builder.create_vector(&[t1, t2]);
306
307        let sm_id = builder.create_string("fetch-machine");
308        let sm_name = builder.create_string("Fetch Data");
309        let sm = StateMachine::create(
310            &mut builder,
311            &StateMachineArgs {
312                node_id: Some(sm_id),
313                name: Some(sm_name),
314                states: Some(states),
315                transitions: Some(transitions),
316            },
317        );
318
319        let child = ChildNode::create(
320            &mut builder,
321            &ChildNodeArgs {
322                value_type: ChildUnion::StateMachine,
323                value: Some(sm.as_union_value()),
324            },
325        );
326        let children = builder.create_vector(&[child]);
327
328        let root_id = builder.create_string("root");
329        let root = ViewRoot::create(
330            &mut builder,
331            &ViewRootArgs {
332                node_id: Some(root_id),
333                children: Some(children),
334                ..Default::default()
335            },
336        );
337
338        let doc = VoceDocument::create(
339            &mut builder,
340            &VoceDocumentArgs {
341                root: Some(root),
342                ..Default::default()
343            },
344        );
345
346        builder.finish(doc, Some("VOCE"));
347        let buf = builder.finished_data();
348
349        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
350        let sm = doc
351            .root()
352            .children()
353            .unwrap()
354            .get(0)
355            .value_as_state_machine()
356            .unwrap();
357
358        assert_eq!(sm.node_id(), "fetch-machine");
359        assert_eq!(sm.name(), Some("Fetch Data"));
360        // states and transitions are required fields — return Vector directly
361        assert_eq!(sm.states().len(), 3);
362        assert_eq!(sm.transitions().len(), 2);
363
364        let first_state = sm.states().get(0);
365        assert_eq!(first_state.name(), "idle");
366        assert!(first_state.initial());
367
368        let first_transition = sm.transitions().get(0);
369        assert_eq!(first_transition.event(), "click");
370        assert_eq!(first_transition.from(), "idle");
371        assert_eq!(first_transition.to(), "loading");
372    }
373
374    #[test]
375    fn animation_transition_with_spring() {
376        let mut builder = FlatBufferBuilder::new();
377
378        let prop_str = builder.create_string("transform.translateY");
379        let from_str = builder.create_string("20px");
380        let to_str = builder.create_string("0px");
381        let prop = AnimatedProperty::create(
382            &mut builder,
383            &AnimatedPropertyArgs {
384                property: Some(prop_str),
385                from: Some(from_str),
386                to: Some(to_str),
387            },
388        );
389        let props = builder.create_vector(&[prop]);
390
391        let dur = Duration::create(&mut builder, &DurationArgs { ms: 300.0 });
392
393        let easing = Easing::create(
394            &mut builder,
395            &EasingArgs {
396                easing_type: EasingType::Spring,
397                stiffness: 300.0,
398                damping: 25.0,
399                mass: 1.0,
400                ..Default::default()
401            },
402        );
403
404        let rm = ReducedMotion::create(
405            &mut builder,
406            &ReducedMotionArgs {
407                strategy: ReducedMotionStrategy::Remove,
408                ..Default::default()
409            },
410        );
411
412        let target = builder.create_string("hero-text");
413        let anim_id = builder.create_string("hero-entrance");
414        let anim = AnimationTransition::create(
415            &mut builder,
416            &AnimationTransitionArgs {
417                node_id: Some(anim_id),
418                target_node_id: Some(target),
419                properties: Some(props),
420                duration: Some(dur),
421                easing: Some(easing),
422                reduced_motion: Some(rm),
423                ..Default::default()
424            },
425        );
426
427        let child = ChildNode::create(
428            &mut builder,
429            &ChildNodeArgs {
430                value_type: ChildUnion::AnimationTransition,
431                value: Some(anim.as_union_value()),
432            },
433        );
434        let children = builder.create_vector(&[child]);
435
436        let root_id = builder.create_string("root");
437        let root = ViewRoot::create(
438            &mut builder,
439            &ViewRootArgs {
440                node_id: Some(root_id),
441                children: Some(children),
442                ..Default::default()
443            },
444        );
445
446        let doc = VoceDocument::create(
447            &mut builder,
448            &VoceDocumentArgs {
449                root: Some(root),
450                ..Default::default()
451            },
452        );
453
454        builder.finish(doc, Some("VOCE"));
455        let buf = builder.finished_data();
456
457        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
458        let anim = doc
459            .root()
460            .children()
461            .unwrap()
462            .get(0)
463            .value_as_animation_transition()
464            .unwrap();
465
466        assert_eq!(anim.node_id(), "hero-entrance");
467        // target_node_id is required — returns &str directly
468        assert_eq!(anim.target_node_id(), "hero-text");
469
470        let easing = anim.easing().unwrap();
471        assert_eq!(easing.easing_type(), EasingType::Spring);
472        assert_eq!(easing.stiffness(), 300.0);
473        assert_eq!(easing.damping(), 25.0);
474
475        let rm = anim.reduced_motion().unwrap();
476        assert_eq!(rm.strategy(), ReducedMotionStrategy::Remove);
477
478        // properties is required — returns Vector directly
479        let props = anim.properties();
480        assert_eq!(props.len(), 1);
481        assert_eq!(props.get(0).property(), "transform.translateY");
482    }
483
484    #[test]
485    fn child_union_covers_all_types() {
486        // Verify the ChildUnion has entries for ALL Phase 1 node types (27 total)
487        // Layout (S02)
488        assert_eq!(ChildUnion::Container.0, 1);
489        assert_eq!(ChildUnion::Surface.0, 2);
490        assert_eq!(ChildUnion::TextNode.0, 3);
491        assert_eq!(ChildUnion::MediaNode.0, 4);
492        // State (S03)
493        assert_eq!(ChildUnion::StateMachine.0, 5);
494        assert_eq!(ChildUnion::DataNode.0, 6);
495        assert_eq!(ChildUnion::ComputeNode.0, 7);
496        assert_eq!(ChildUnion::EffectNode.0, 8);
497        assert_eq!(ChildUnion::ContextNode.0, 9);
498        // Motion (S03)
499        assert_eq!(ChildUnion::AnimationTransition.0, 10);
500        assert_eq!(ChildUnion::Sequence.0, 11);
501        assert_eq!(ChildUnion::GestureHandler.0, 12);
502        assert_eq!(ChildUnion::ScrollBinding.0, 13);
503        assert_eq!(ChildUnion::PhysicsBody.0, 14);
504        // Navigation (S03)
505        assert_eq!(ChildUnion::RouteMap.0, 15);
506        // A11y (S04)
507        assert_eq!(ChildUnion::SemanticNode.0, 16);
508        assert_eq!(ChildUnion::LiveRegion.0, 17);
509        assert_eq!(ChildUnion::FocusTrap.0, 18);
510        // Theming (S04)
511        assert_eq!(ChildUnion::ThemeNode.0, 19);
512        assert_eq!(ChildUnion::PersonalizationSlot.0, 20);
513        assert_eq!(ChildUnion::ResponsiveRule.0, 21);
514        // Data & Backend (S05)
515        assert_eq!(ChildUnion::ActionNode.0, 22);
516        assert_eq!(ChildUnion::SubscriptionNode.0, 23);
517        assert_eq!(ChildUnion::AuthContextNode.0, 24);
518        assert_eq!(ChildUnion::ContentSlot.0, 25);
519        assert_eq!(ChildUnion::RichTextNode.0, 26);
520        // Forms (S05)
521        assert_eq!(ChildUnion::FormNode.0, 27);
522    }
523
524    #[test]
525    fn semantic_node_with_button_role() {
526        let mut builder = FlatBufferBuilder::new();
527
528        let node_id = builder.create_string("sem-cta");
529        let role = builder.create_string("button");
530        let label = builder.create_string("Add to cart");
531
532        let sem = SemanticNode::create(
533            &mut builder,
534            &SemanticNodeArgs {
535                node_id: Some(node_id),
536                role: Some(role),
537                label: Some(label),
538                tab_index: 0,
539                aria_required: false,
540                ..Default::default()
541            },
542        );
543
544        let child = ChildNode::create(
545            &mut builder,
546            &ChildNodeArgs {
547                value_type: ChildUnion::SemanticNode,
548                value: Some(sem.as_union_value()),
549            },
550        );
551        let children = builder.create_vector(&[child]);
552
553        let root_id = builder.create_string("root");
554        let root = ViewRoot::create(
555            &mut builder,
556            &ViewRootArgs {
557                node_id: Some(root_id),
558                children: Some(children),
559                ..Default::default()
560            },
561        );
562
563        let doc = VoceDocument::create(
564            &mut builder,
565            &VoceDocumentArgs {
566                root: Some(root),
567                ..Default::default()
568            },
569        );
570
571        builder.finish(doc, Some("VOCE"));
572        let buf = builder.finished_data();
573
574        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
575        let sem = doc
576            .root()
577            .children()
578            .unwrap()
579            .get(0)
580            .value_as_semantic_node()
581            .unwrap();
582
583        assert_eq!(sem.node_id(), "sem-cta");
584        assert_eq!(sem.role(), "button");
585        assert_eq!(sem.label(), Some("Add to cart"));
586        assert_eq!(sem.tab_index(), 0);
587    }
588
589    #[test]
590    fn live_region_assertive() {
591        let mut builder = FlatBufferBuilder::new();
592
593        let node_id = builder.create_string("cart-updates");
594        let target = builder.create_string("cart-count");
595        let desc = builder.create_string("Shopping cart updates");
596
597        let lr = LiveRegion::create(
598            &mut builder,
599            &LiveRegionArgs {
600                node_id: Some(node_id),
601                target_node_id: Some(target),
602                politeness: LiveRegionPoliteness::Assertive,
603                atomic: true,
604                role_description: Some(desc),
605                ..Default::default()
606            },
607        );
608
609        let child = ChildNode::create(
610            &mut builder,
611            &ChildNodeArgs {
612                value_type: ChildUnion::LiveRegion,
613                value: Some(lr.as_union_value()),
614            },
615        );
616        let children = builder.create_vector(&[child]);
617
618        let root_id = builder.create_string("root");
619        let root = ViewRoot::create(
620            &mut builder,
621            &ViewRootArgs {
622                node_id: Some(root_id),
623                children: Some(children),
624                ..Default::default()
625            },
626        );
627
628        let doc = VoceDocument::create(
629            &mut builder,
630            &VoceDocumentArgs {
631                root: Some(root),
632                ..Default::default()
633            },
634        );
635
636        builder.finish(doc, Some("VOCE"));
637        let buf = builder.finished_data();
638
639        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
640        let lr = doc
641            .root()
642            .children()
643            .unwrap()
644            .get(0)
645            .value_as_live_region()
646            .unwrap();
647
648        assert_eq!(lr.node_id(), "cart-updates");
649        assert_eq!(lr.target_node_id(), "cart-count");
650        assert_eq!(lr.politeness(), LiveRegionPoliteness::Assertive);
651        assert!(lr.atomic());
652        assert_eq!(lr.role_description(), Some("Shopping cart updates"));
653    }
654
655    #[test]
656    fn theme_node_with_color_palette() {
657        let mut builder = FlatBufferBuilder::new();
658
659        let node_id = builder.create_string("theme-dark");
660        let name = builder.create_string("dark");
661
662        let colors = ColorPalette::create(
663            &mut builder,
664            &ColorPaletteArgs {
665                background: Some(&Color::new(12, 12, 14, 255)),
666                foreground: Some(&Color::new(232, 230, 225, 255)),
667                primary: Some(&Color::new(232, 89, 60, 255)),
668                surface: Some(&Color::new(20, 20, 23, 255)),
669                ..Default::default()
670            },
671        );
672
673        let theme = ThemeNode::create(
674            &mut builder,
675            &ThemeNodeArgs {
676                node_id: Some(node_id),
677                name: Some(name),
678                colors: Some(colors),
679                ..Default::default()
680            },
681        );
682
683        let child = ChildNode::create(
684            &mut builder,
685            &ChildNodeArgs {
686                value_type: ChildUnion::ThemeNode,
687                value: Some(theme.as_union_value()),
688            },
689        );
690        let children = builder.create_vector(&[child]);
691
692        let root_id = builder.create_string("root");
693        let root = ViewRoot::create(
694            &mut builder,
695            &ViewRootArgs {
696                node_id: Some(root_id),
697                children: Some(children),
698                ..Default::default()
699            },
700        );
701
702        let doc = VoceDocument::create(
703            &mut builder,
704            &VoceDocumentArgs {
705                root: Some(root),
706                theme: Some(theme),
707                ..Default::default()
708            },
709        );
710
711        builder.finish(doc, Some("VOCE"));
712        let buf = builder.finished_data();
713
714        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
715
716        // Check theme on document
717        let theme = doc.theme().unwrap();
718        assert_eq!(theme.name(), "dark");
719
720        let colors = theme.colors().unwrap();
721        let bg = colors.background().unwrap();
722        assert_eq!(bg.r(), 12);
723        assert_eq!(bg.g(), 12);
724        assert_eq!(bg.b(), 14);
725        assert_eq!(bg.a(), 255);
726
727        let primary = colors.primary().unwrap();
728        assert_eq!(primary.r(), 232);
729        assert_eq!(primary.g(), 89);
730        assert_eq!(primary.b(), 60);
731    }
732
733    #[test]
734    fn action_node_with_optimistic_update() {
735        let mut builder = FlatBufferBuilder::new();
736
737        let endpoint = builder.create_string("https://api.example.com/todos");
738        let resource = builder.create_string("todos");
739        let source = DataSource::create(
740            &mut builder,
741            &DataSourceArgs {
742                endpoint: Some(endpoint),
743                resource: Some(resource),
744                ..Default::default()
745            },
746        );
747
748        let target = builder.create_string("todo-list");
749        let optimistic = OptimisticConfig::create(
750            &mut builder,
751            &OptimisticConfigArgs {
752                strategy: OptimisticStrategy::MirrorInput,
753                target_data_node_id: Some(target),
754                ..Default::default()
755            },
756        );
757
758        let invalidate = builder.create_string("todo-list");
759        let invalidates = builder.create_vector(&[invalidate]);
760
761        let node_id = builder.create_string("create-todo");
762        let action = ActionNode::create(
763            &mut builder,
764            &ActionNodeArgs {
765                node_id: Some(node_id),
766                source: Some(source),
767                method: HttpMethod::POST,
768                optimistic: Some(optimistic),
769                invalidates: Some(invalidates),
770                csrf_protected: true,
771                ..Default::default()
772            },
773        );
774
775        let child = ChildNode::create(
776            &mut builder,
777            &ChildNodeArgs {
778                value_type: ChildUnion::ActionNode,
779                value: Some(action.as_union_value()),
780            },
781        );
782        let children = builder.create_vector(&[child]);
783
784        let root_id = builder.create_string("root");
785        let root = ViewRoot::create(
786            &mut builder,
787            &ViewRootArgs {
788                node_id: Some(root_id),
789                children: Some(children),
790                ..Default::default()
791            },
792        );
793
794        let doc = VoceDocument::create(
795            &mut builder,
796            &VoceDocumentArgs {
797                root: Some(root),
798                ..Default::default()
799            },
800        );
801
802        builder.finish(doc, Some("VOCE"));
803        let buf = builder.finished_data();
804
805        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
806        let action = doc
807            .root()
808            .children()
809            .unwrap()
810            .get(0)
811            .value_as_action_node()
812            .unwrap();
813
814        assert_eq!(action.node_id(), "create-todo");
815        assert_eq!(action.method(), HttpMethod::POST);
816        assert!(action.csrf_protected());
817
818        let opt = action.optimistic().unwrap();
819        assert_eq!(opt.strategy(), OptimisticStrategy::MirrorInput);
820        assert_eq!(opt.target_data_node_id(), Some("todo-list"));
821    }
822
823    #[test]
824    fn form_node_with_fields_and_validation() {
825        let mut builder = FlatBufferBuilder::new();
826
827        // Email field with validation
828        let field_name = builder.create_string("email");
829        let field_label = builder.create_string("Email address");
830        let placeholder = builder.create_string("you@example.com");
831
832        let req_msg = builder.create_string("Email is required");
833        let required = ValidationRule::create(
834            &mut builder,
835            &ValidationRuleArgs {
836                rule_type: ValidationType::Required,
837                message: Some(req_msg),
838                ..Default::default()
839            },
840        );
841
842        let email_msg = builder.create_string("Must be a valid email");
843        let email_rule = ValidationRule::create(
844            &mut builder,
845            &ValidationRuleArgs {
846                rule_type: ValidationType::Email,
847                message: Some(email_msg),
848                ..Default::default()
849            },
850        );
851
852        let validations = builder.create_vector(&[required, email_rule]);
853
854        let email_field = FormField::create(
855            &mut builder,
856            &FormFieldArgs {
857                name: Some(field_name),
858                field_type: FormFieldType::Email,
859                label: Some(field_label),
860                placeholder: Some(placeholder),
861                validations: Some(validations),
862                autocomplete: AutocompleteHint::Email,
863                ..Default::default()
864            },
865        );
866
867        let fields = builder.create_vector(&[email_field]);
868
869        // Submission
870        let action_id = builder.create_string("submit-contact");
871        let submission = FormSubmission::create(
872            &mut builder,
873            &FormSubmissionArgs {
874                action_node_id: Some(action_id),
875                encoding: FormEncoding::Json,
876                progressive: true,
877                ..Default::default()
878            },
879        );
880
881        let node_id = builder.create_string("contact-form");
882        let form = FormNode::create(
883            &mut builder,
884            &FormNodeArgs {
885                node_id: Some(node_id),
886                fields: Some(fields),
887                validation_mode: ValidationMode::OnBlurThenChange,
888                submission: Some(submission),
889                ..Default::default()
890            },
891        );
892
893        let child = ChildNode::create(
894            &mut builder,
895            &ChildNodeArgs {
896                value_type: ChildUnion::FormNode,
897                value: Some(form.as_union_value()),
898            },
899        );
900        let children = builder.create_vector(&[child]);
901
902        let root_id = builder.create_string("root");
903        let root = ViewRoot::create(
904            &mut builder,
905            &ViewRootArgs {
906                node_id: Some(root_id),
907                children: Some(children),
908                ..Default::default()
909            },
910        );
911
912        let doc = VoceDocument::create(
913            &mut builder,
914            &VoceDocumentArgs {
915                root: Some(root),
916                ..Default::default()
917            },
918        );
919
920        builder.finish(doc, Some("VOCE"));
921        let buf = builder.finished_data();
922
923        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
924        let form = doc
925            .root()
926            .children()
927            .unwrap()
928            .get(0)
929            .value_as_form_node()
930            .unwrap();
931
932        assert_eq!(form.node_id(), "contact-form");
933        assert_eq!(form.validation_mode(), ValidationMode::OnBlurThenChange);
934
935        let fields = form.fields();
936        assert_eq!(fields.len(), 1);
937
938        let email = fields.get(0);
939        assert_eq!(email.name(), "email");
940        assert_eq!(email.field_type(), FormFieldType::Email);
941        assert_eq!(email.label(), "Email address");
942        assert_eq!(email.autocomplete(), AutocompleteHint::Email);
943
944        let validations = email.validations().unwrap();
945        assert_eq!(validations.len(), 2);
946        assert_eq!(validations.get(0).rule_type(), ValidationType::Required);
947        assert_eq!(validations.get(1).rule_type(), ValidationType::Email);
948
949        let sub = form.submission();
950        assert_eq!(sub.action_node_id(), "submit-contact");
951        assert!(sub.progressive());
952    }
953
954    #[test]
955    fn document_with_i18n_config() {
956        let mut builder = FlatBufferBuilder::new();
957
958        let default_locale = builder.create_string("en-US");
959        let fr = builder.create_string("fr-FR");
960        let ar = builder.create_string("ar-SA");
961        let locales = builder.create_vector(&[default_locale, fr, ar]);
962
963        // Re-create default_locale since the string was consumed
964        let default_locale2 = builder.create_string("en-US");
965        let mode = builder.create_string("static");
966
967        let i18n = I18nConfig::create(
968            &mut builder,
969            &I18nConfigArgs {
970                default_locale: Some(default_locale2),
971                supported_locales: Some(locales),
972                mode: Some(mode),
973            },
974        );
975
976        let root_id = builder.create_string("root");
977        let root = ViewRoot::create(
978            &mut builder,
979            &ViewRootArgs {
980                node_id: Some(root_id),
981                ..Default::default()
982            },
983        );
984
985        let doc = VoceDocument::create(
986            &mut builder,
987            &VoceDocumentArgs {
988                root: Some(root),
989                i18n: Some(i18n),
990                ..Default::default()
991            },
992        );
993
994        builder.finish(doc, Some("VOCE"));
995        let buf = builder.finished_data();
996
997        let doc = flatbuffers::root::<VoceDocument>(buf).unwrap();
998        let i18n = doc.i18n().unwrap();
999        assert_eq!(i18n.default_locale(), "en-US");
1000        assert_eq!(i18n.supported_locales().len(), 3);
1001        assert_eq!(i18n.mode(), Some("static"));
1002    }
1003}