1use serde::de::{self, Deserializer};
7use serde::ser::{SerializeMap, Serializer};
8use serde::{Deserialize, Serialize};
9
10use crate::action::Action;
11use crate::visibility::Visibility;
12
13#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum Size {
17 Xs,
18 Sm,
19 #[default]
20 Default,
21 Lg,
22}
23
24#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum IconPosition {
28 #[default]
29 Left,
30 Right,
31}
32
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum SortDirection {
37 #[default]
38 Asc,
39 Desc,
40}
41
42#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum Orientation {
46 #[default]
47 Horizontal,
48 Vertical,
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum ButtonVariant {
55 #[default]
56 Default,
57 Secondary,
58 Destructive,
59 Outline,
60 Ghost,
61 Link,
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum InputType {
68 #[default]
69 Text,
70 Email,
71 Password,
72 Number,
73 Textarea,
74 Hidden,
75 Date,
76 Time,
77 Url,
78 Tel,
79 Search,
80}
81
82#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum AlertVariant {
86 #[default]
87 Info,
88 Success,
89 Warning,
90 Error,
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum BadgeVariant {
97 #[default]
98 Default,
99 Secondary,
100 Destructive,
101 Outline,
102}
103
104#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum TextElement {
108 #[default]
109 P,
110 H1,
111 H2,
112 H3,
113 Span,
114 Div,
115 Section,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum ColumnFormat {
122 Date,
123 DateTime,
124 Currency,
125 Boolean,
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct Column {
131 pub key: String,
132 pub label: String,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub format: Option<ColumnFormat>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct SelectOption {
140 pub value: String,
141 pub label: String,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct CardProps {
147 pub title: String,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub description: Option<String>,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
151 pub children: Vec<ComponentNode>,
152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
153 pub footer: Vec<ComponentNode>,
154}
155
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct TableProps {
159 pub columns: Vec<Column>,
160 pub data_path: String,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub row_actions: Option<Vec<Action>>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub empty_message: Option<String>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub sortable: Option<bool>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub sort_column: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub sort_direction: Option<SortDirection>,
171}
172
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct FormProps {
176 pub action: Action,
177 pub fields: Vec<ComponentNode>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub method: Option<crate::action::HttpMethod>,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub struct ButtonProps {
185 pub label: String,
186 #[serde(default)]
187 pub variant: ButtonVariant,
188 #[serde(default)]
189 pub size: Size,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub disabled: Option<bool>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub icon: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub icon_position: Option<IconPosition>,
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub struct InputProps {
201 pub field: String,
203 pub label: String,
204 #[serde(default)]
205 pub input_type: InputType,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub placeholder: Option<String>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub required: Option<bool>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub disabled: Option<bool>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub error: Option<String>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub description: Option<String>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub default_value: Option<String>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub data_path: Option<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub step: Option<String>,
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct SelectProps {
229 pub field: String,
231 pub label: String,
232 pub options: Vec<SelectOption>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub placeholder: Option<String>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub required: Option<bool>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub disabled: Option<bool>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub error: Option<String>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub description: Option<String>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub default_value: Option<String>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub data_path: Option<String>,
248}
249
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
252pub struct AlertProps {
253 pub message: String,
254 #[serde(default)]
255 pub variant: AlertVariant,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub title: Option<String>,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub struct BadgeProps {
263 pub label: String,
264 #[serde(default)]
265 pub variant: BadgeVariant,
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
270pub struct ModalProps {
271 pub title: String,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub description: Option<String>,
274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
275 pub children: Vec<ComponentNode>,
276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
277 pub footer: Vec<ComponentNode>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub trigger_label: Option<String>,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284pub struct TextProps {
285 pub content: String,
286 #[serde(default)]
287 pub element: TextElement,
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct CheckboxProps {
293 pub field: String,
295 pub label: String,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub description: Option<String>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub checked: Option<bool>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub data_path: Option<String>,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub required: Option<bool>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub disabled: Option<bool>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub error: Option<String>,
309}
310
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
313pub struct SwitchProps {
314 pub field: String,
316 pub label: String,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub description: Option<String>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub checked: Option<bool>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub data_path: Option<String>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub required: Option<bool>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub disabled: Option<bool>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub error: Option<String>,
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334pub struct SeparatorProps {
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub orientation: Option<Orientation>,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct DescriptionItem {
342 pub label: String,
343 pub value: String,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub format: Option<ColumnFormat>,
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350pub struct DescriptionListProps {
351 pub items: Vec<DescriptionItem>,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub columns: Option<u8>,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
358pub struct Tab {
359 pub value: String,
360 pub label: String,
361 #[serde(default, skip_serializing_if = "Vec::is_empty")]
362 pub children: Vec<ComponentNode>,
363}
364
365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
367pub struct TabsProps {
368 pub default_tab: String,
369 pub tabs: Vec<Tab>,
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374pub struct BreadcrumbItem {
375 pub label: String,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub url: Option<String>,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382pub struct BreadcrumbProps {
383 pub items: Vec<BreadcrumbItem>,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct PaginationProps {
389 pub current_page: u32,
390 pub per_page: u32,
391 pub total: u32,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub base_url: Option<String>,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398pub struct ProgressProps {
399 pub value: u8,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub max: Option<u8>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub label: Option<String>,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409pub struct AvatarProps {
410 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub src: Option<String>,
412 pub alt: String,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub fallback: Option<String>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub size: Option<Size>,
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
421pub struct SkeletonProps {
422 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub width: Option<String>,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub height: Option<String>,
426 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub rounded: Option<bool>,
428}
429
430#[derive(Debug, Clone, PartialEq)]
436pub struct PluginProps {
437 pub plugin_type: String,
439 pub props: serde_json::Value,
441}
442
443impl Serialize for PluginProps {
444 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
445 let obj = self.props.as_object();
447 let extra_len = obj.map_or(0, |m| m.len());
448 let mut map = serializer.serialize_map(Some(1 + extra_len))?;
449 map.serialize_entry("type", &self.plugin_type)?;
450 if let Some(obj) = obj {
451 for (k, v) in obj {
452 if k != "type" {
453 map.serialize_entry(k, v)?;
454 }
455 }
456 }
457 map.end()
458 }
459}
460
461impl<'de> Deserialize<'de> for PluginProps {
462 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
463 let mut value = serde_json::Value::deserialize(deserializer)?;
464 let plugin_type = value
465 .get("type")
466 .and_then(|v| v.as_str())
467 .map(|s| s.to_string())
468 .ok_or_else(|| de::Error::missing_field("type"))?;
469 if let Some(obj) = value.as_object_mut() {
471 obj.remove("type");
472 }
473 Ok(PluginProps {
474 plugin_type,
475 props: value,
476 })
477 }
478}
479
480#[derive(Debug, Clone, PartialEq)]
486pub enum Component {
487 Card(CardProps),
488 Table(TableProps),
489 Form(FormProps),
490 Button(ButtonProps),
491 Input(InputProps),
492 Select(SelectProps),
493 Alert(AlertProps),
494 Badge(BadgeProps),
495 Modal(ModalProps),
496 Text(TextProps),
497 Checkbox(CheckboxProps),
498 Switch(SwitchProps),
499 Separator(SeparatorProps),
500 DescriptionList(DescriptionListProps),
501 Tabs(TabsProps),
502 Breadcrumb(BreadcrumbProps),
503 Pagination(PaginationProps),
504 Progress(ProgressProps),
505 Avatar(AvatarProps),
506 Skeleton(SkeletonProps),
507 Plugin(PluginProps),
508}
509
510fn serialize_tagged<S: Serializer, T: Serialize>(
515 serializer: S,
516 type_name: &str,
517 props: &T,
518) -> Result<S::Ok, S::Error> {
519 let mut value = serde_json::to_value(props).map_err(serde::ser::Error::custom)?;
520 if let Some(obj) = value.as_object_mut() {
521 obj.insert(
522 "type".to_string(),
523 serde_json::Value::String(type_name.to_string()),
524 );
525 }
526 value.serialize(serializer)
527}
528
529impl Serialize for Component {
530 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
531 match self {
532 Component::Card(p) => serialize_tagged(serializer, "Card", p),
533 Component::Table(p) => serialize_tagged(serializer, "Table", p),
534 Component::Form(p) => serialize_tagged(serializer, "Form", p),
535 Component::Button(p) => serialize_tagged(serializer, "Button", p),
536 Component::Input(p) => serialize_tagged(serializer, "Input", p),
537 Component::Select(p) => serialize_tagged(serializer, "Select", p),
538 Component::Alert(p) => serialize_tagged(serializer, "Alert", p),
539 Component::Badge(p) => serialize_tagged(serializer, "Badge", p),
540 Component::Modal(p) => serialize_tagged(serializer, "Modal", p),
541 Component::Text(p) => serialize_tagged(serializer, "Text", p),
542 Component::Checkbox(p) => serialize_tagged(serializer, "Checkbox", p),
543 Component::Switch(p) => serialize_tagged(serializer, "Switch", p),
544 Component::Separator(p) => serialize_tagged(serializer, "Separator", p),
545 Component::DescriptionList(p) => serialize_tagged(serializer, "DescriptionList", p),
546 Component::Tabs(p) => serialize_tagged(serializer, "Tabs", p),
547 Component::Breadcrumb(p) => serialize_tagged(serializer, "Breadcrumb", p),
548 Component::Pagination(p) => serialize_tagged(serializer, "Pagination", p),
549 Component::Progress(p) => serialize_tagged(serializer, "Progress", p),
550 Component::Avatar(p) => serialize_tagged(serializer, "Avatar", p),
551 Component::Skeleton(p) => serialize_tagged(serializer, "Skeleton", p),
552 Component::Plugin(p) => p.serialize(serializer),
553 }
554 }
555}
556
557impl<'de> Deserialize<'de> for Component {
560 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
561 let value = serde_json::Value::deserialize(deserializer)?;
562 let type_str = value
563 .get("type")
564 .and_then(|v| v.as_str())
565 .ok_or_else(|| de::Error::missing_field("type"))?;
566
567 match type_str {
568 "Card" => serde_json::from_value::<CardProps>(value)
569 .map(Component::Card)
570 .map_err(de::Error::custom),
571 "Table" => serde_json::from_value::<TableProps>(value)
572 .map(Component::Table)
573 .map_err(de::Error::custom),
574 "Form" => serde_json::from_value::<FormProps>(value)
575 .map(Component::Form)
576 .map_err(de::Error::custom),
577 "Button" => serde_json::from_value::<ButtonProps>(value)
578 .map(Component::Button)
579 .map_err(de::Error::custom),
580 "Input" => serde_json::from_value::<InputProps>(value)
581 .map(Component::Input)
582 .map_err(de::Error::custom),
583 "Select" => serde_json::from_value::<SelectProps>(value)
584 .map(Component::Select)
585 .map_err(de::Error::custom),
586 "Alert" => serde_json::from_value::<AlertProps>(value)
587 .map(Component::Alert)
588 .map_err(de::Error::custom),
589 "Badge" => serde_json::from_value::<BadgeProps>(value)
590 .map(Component::Badge)
591 .map_err(de::Error::custom),
592 "Modal" => serde_json::from_value::<ModalProps>(value)
593 .map(Component::Modal)
594 .map_err(de::Error::custom),
595 "Text" => serde_json::from_value::<TextProps>(value)
596 .map(Component::Text)
597 .map_err(de::Error::custom),
598 "Checkbox" => serde_json::from_value::<CheckboxProps>(value)
599 .map(Component::Checkbox)
600 .map_err(de::Error::custom),
601 "Switch" => serde_json::from_value::<SwitchProps>(value)
602 .map(Component::Switch)
603 .map_err(de::Error::custom),
604 "Separator" => serde_json::from_value::<SeparatorProps>(value)
605 .map(Component::Separator)
606 .map_err(de::Error::custom),
607 "DescriptionList" => serde_json::from_value::<DescriptionListProps>(value)
608 .map(Component::DescriptionList)
609 .map_err(de::Error::custom),
610 "Tabs" => serde_json::from_value::<TabsProps>(value)
611 .map(Component::Tabs)
612 .map_err(de::Error::custom),
613 "Breadcrumb" => serde_json::from_value::<BreadcrumbProps>(value)
614 .map(Component::Breadcrumb)
615 .map_err(de::Error::custom),
616 "Pagination" => serde_json::from_value::<PaginationProps>(value)
617 .map(Component::Pagination)
618 .map_err(de::Error::custom),
619 "Progress" => serde_json::from_value::<ProgressProps>(value)
620 .map(Component::Progress)
621 .map_err(de::Error::custom),
622 "Avatar" => serde_json::from_value::<AvatarProps>(value)
623 .map(Component::Avatar)
624 .map_err(de::Error::custom),
625 "Skeleton" => serde_json::from_value::<SkeletonProps>(value)
626 .map(Component::Skeleton)
627 .map_err(de::Error::custom),
628 _ => {
629 let plugin_type = type_str.to_string();
631 let mut props = value;
632 if let Some(obj) = props.as_object_mut() {
633 obj.remove("type");
634 }
635 Ok(Component::Plugin(PluginProps { plugin_type, props }))
636 }
637 }
638 }
639}
640
641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
647pub struct ComponentNode {
648 pub key: String,
649 #[serde(flatten)]
650 pub component: Component,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub action: Option<Action>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub visibility: Option<Visibility>,
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660 use crate::action::HttpMethod;
661 use crate::visibility::{VisibilityCondition, VisibilityOperator};
662
663 #[test]
664 fn card_component_tagged_serialization() {
665 let card = Component::Card(CardProps {
666 title: "Test Card".to_string(),
667 description: Some("A description".to_string()),
668 children: vec![],
669 footer: vec![],
670 });
671 let json = serde_json::to_value(&card).unwrap();
672 assert_eq!(json["type"], "Card");
673 assert_eq!(json["title"], "Test Card");
674 assert_eq!(json["description"], "A description");
675 }
676
677 #[test]
678 fn button_variant_defaults_to_default() {
679 let json = r#"{"type": "Button", "label": "Click me"}"#;
680 let component: Component = serde_json::from_str(json).unwrap();
681 match component {
682 Component::Button(props) => {
683 assert_eq!(props.variant, ButtonVariant::Default);
684 assert_eq!(props.label, "Click me");
685 }
686 _ => panic!("expected Button"),
687 }
688 }
689
690 #[test]
691 fn input_type_defaults_to_text() {
692 let json = r#"{"type": "Input", "field": "email", "label": "Email"}"#;
693 let component: Component = serde_json::from_str(json).unwrap();
694 match component {
695 Component::Input(props) => {
696 assert_eq!(props.input_type, InputType::Text);
697 assert_eq!(props.field, "email");
698 }
699 _ => panic!("expected Input"),
700 }
701 }
702
703 #[test]
704 fn alert_variant_defaults_to_info() {
705 let json = r#"{"type": "Alert", "message": "Hello"}"#;
706 let component: Component = serde_json::from_str(json).unwrap();
707 match component {
708 Component::Alert(props) => assert_eq!(props.variant, AlertVariant::Info),
709 _ => panic!("expected Alert"),
710 }
711 }
712
713 #[test]
714 fn badge_variant_defaults_to_default() {
715 let json = r#"{"type": "Badge", "label": "New"}"#;
716 let component: Component = serde_json::from_str(json).unwrap();
717 match component {
718 Component::Badge(props) => assert_eq!(props.variant, BadgeVariant::Default),
719 _ => panic!("expected Badge"),
720 }
721 }
722
723 #[test]
724 fn text_element_defaults_to_p() {
725 let json = r#"{"type": "Text", "content": "Hello world"}"#;
726 let component: Component = serde_json::from_str(json).unwrap();
727 match component {
728 Component::Text(props) => {
729 assert_eq!(props.element, TextElement::P);
730 assert_eq!(props.content, "Hello world");
731 }
732 _ => panic!("expected Text"),
733 }
734 }
735
736 #[test]
737 fn table_component_round_trips() {
738 let table = Component::Table(TableProps {
739 columns: vec![
740 Column {
741 key: "name".to_string(),
742 label: "Name".to_string(),
743 format: None,
744 },
745 Column {
746 key: "created_at".to_string(),
747 label: "Created".to_string(),
748 format: Some(ColumnFormat::Date),
749 },
750 ],
751 data_path: "/data/users".to_string(),
752 row_actions: None,
753 empty_message: Some("No users found".to_string()),
754 sortable: None,
755 sort_column: None,
756 sort_direction: None,
757 });
758 let json = serde_json::to_string(&table).unwrap();
759 let parsed: Component = serde_json::from_str(&json).unwrap();
760 assert_eq!(parsed, table);
761 }
762
763 #[test]
764 fn select_component_round_trips() {
765 let select = Component::Select(SelectProps {
766 field: "role".to_string(),
767 label: "Role".to_string(),
768 options: vec![
769 SelectOption {
770 value: "admin".to_string(),
771 label: "Administrator".to_string(),
772 },
773 SelectOption {
774 value: "user".to_string(),
775 label: "User".to_string(),
776 },
777 ],
778 placeholder: Some("Select a role".to_string()),
779 required: Some(true),
780 disabled: None,
781 error: None,
782 description: None,
783 default_value: None,
784 data_path: None,
785 });
786 let json = serde_json::to_string(&select).unwrap();
787 let parsed: Component = serde_json::from_str(&json).unwrap();
788 assert_eq!(parsed, select);
789 }
790
791 #[test]
792 fn modal_component_round_trips() {
793 let modal = Component::Modal(ModalProps {
794 title: "Confirm".to_string(),
795 description: None,
796 children: vec![ComponentNode {
797 key: "msg".to_string(),
798 component: Component::Text(TextProps {
799 content: "Are you sure?".to_string(),
800 element: TextElement::P,
801 }),
802 action: None,
803 visibility: None,
804 }],
805 footer: vec![],
806 trigger_label: Some("Open".to_string()),
807 });
808 let json = serde_json::to_string(&modal).unwrap();
809 let parsed: Component = serde_json::from_str(&json).unwrap();
810 assert_eq!(parsed, modal);
811 }
812
813 #[test]
814 fn form_component_round_trips() {
815 let form = Component::Form(FormProps {
816 action: Action {
817 handler: "users.store".to_string(),
818 url: None,
819 method: HttpMethod::Post,
820 confirm: None,
821 on_success: None,
822 on_error: None,
823 },
824 fields: vec![ComponentNode {
825 key: "email-input".to_string(),
826 component: Component::Input(InputProps {
827 field: "email".to_string(),
828 label: "Email".to_string(),
829 input_type: InputType::Email,
830 placeholder: Some("user@example.com".to_string()),
831 required: Some(true),
832 disabled: None,
833 error: None,
834 description: None,
835 default_value: None,
836 data_path: None,
837 step: None,
838 }),
839 action: None,
840 visibility: None,
841 }],
842 method: None,
843 });
844 let json = serde_json::to_string(&form).unwrap();
845 let parsed: Component = serde_json::from_str(&json).unwrap();
846 assert_eq!(parsed, form);
847 }
848
849 #[test]
850 fn component_node_with_action_and_visibility() {
851 let node = ComponentNode {
852 key: "create-btn".to_string(),
853 component: Component::Button(ButtonProps {
854 label: "Create User".to_string(),
855 variant: ButtonVariant::Default,
856 size: Size::Default,
857 disabled: None,
858 icon: None,
859 icon_position: None,
860 }),
861 action: Some(Action {
862 handler: "users.create".to_string(),
863 url: None,
864 method: HttpMethod::Post,
865 confirm: None,
866 on_success: None,
867 on_error: None,
868 }),
869 visibility: Some(Visibility::Condition(VisibilityCondition {
870 path: "/auth/user/role".to_string(),
871 operator: VisibilityOperator::Eq,
872 value: Some(serde_json::Value::String("admin".to_string())),
873 })),
874 };
875 let json = serde_json::to_string(&node).unwrap();
876 let parsed: ComponentNode = serde_json::from_str(&json).unwrap();
877 assert_eq!(parsed, node);
878
879 let value = serde_json::to_value(&node).unwrap();
881 assert_eq!(value["type"], "Button");
882 assert_eq!(value["key"], "create-btn");
883 assert!(value.get("action").is_some());
884 assert!(value.get("visibility").is_some());
885 }
886
887 #[test]
888 fn all_component_variants_serialize() {
889 let components: Vec<Component> = vec![
890 Component::Card(CardProps {
891 title: "t".to_string(),
892 description: None,
893 children: vec![],
894 footer: vec![],
895 }),
896 Component::Table(TableProps {
897 columns: vec![],
898 data_path: "/d".to_string(),
899 row_actions: None,
900 empty_message: None,
901 sortable: None,
902 sort_column: None,
903 sort_direction: None,
904 }),
905 Component::Form(FormProps {
906 action: Action {
907 handler: "h.m".to_string(),
908 url: None,
909 method: HttpMethod::Post,
910 confirm: None,
911 on_success: None,
912 on_error: None,
913 },
914 fields: vec![],
915 method: None,
916 }),
917 Component::Button(ButtonProps {
918 label: "b".to_string(),
919 variant: ButtonVariant::Default,
920 size: Size::Default,
921 disabled: None,
922 icon: None,
923 icon_position: None,
924 }),
925 Component::Input(InputProps {
926 field: "f".to_string(),
927 label: "l".to_string(),
928 input_type: InputType::Text,
929 placeholder: None,
930 required: None,
931 disabled: None,
932 error: None,
933 description: None,
934 default_value: None,
935 data_path: None,
936 step: None,
937 }),
938 Component::Select(SelectProps {
939 field: "f".to_string(),
940 label: "l".to_string(),
941 options: vec![],
942 placeholder: None,
943 required: None,
944 disabled: None,
945 error: None,
946 description: None,
947 default_value: None,
948 data_path: None,
949 }),
950 Component::Alert(AlertProps {
951 message: "m".to_string(),
952 variant: AlertVariant::Info,
953 title: None,
954 }),
955 Component::Badge(BadgeProps {
956 label: "b".to_string(),
957 variant: BadgeVariant::Default,
958 }),
959 Component::Modal(ModalProps {
960 title: "t".to_string(),
961 description: None,
962 children: vec![],
963 footer: vec![],
964 trigger_label: None,
965 }),
966 Component::Text(TextProps {
967 content: "c".to_string(),
968 element: TextElement::P,
969 }),
970 Component::Checkbox(CheckboxProps {
971 field: "f".to_string(),
972 label: "l".to_string(),
973 description: None,
974 checked: None,
975 data_path: None,
976 required: None,
977 disabled: None,
978 error: None,
979 }),
980 Component::Switch(SwitchProps {
981 field: "f".to_string(),
982 label: "l".to_string(),
983 description: None,
984 checked: None,
985 data_path: None,
986 required: None,
987 disabled: None,
988 error: None,
989 }),
990 Component::Separator(SeparatorProps { orientation: None }),
991 Component::DescriptionList(DescriptionListProps {
992 items: vec![DescriptionItem {
993 label: "k".to_string(),
994 value: "v".to_string(),
995 format: None,
996 }],
997 columns: None,
998 }),
999 Component::Tabs(TabsProps {
1000 default_tab: "t1".to_string(),
1001 tabs: vec![Tab {
1002 value: "t1".to_string(),
1003 label: "Tab 1".to_string(),
1004 children: vec![],
1005 }],
1006 }),
1007 Component::Breadcrumb(BreadcrumbProps {
1008 items: vec![BreadcrumbItem {
1009 label: "Home".to_string(),
1010 url: Some("/".to_string()),
1011 }],
1012 }),
1013 Component::Pagination(PaginationProps {
1014 current_page: 1,
1015 per_page: 10,
1016 total: 100,
1017 base_url: None,
1018 }),
1019 Component::Progress(ProgressProps {
1020 value: 50,
1021 max: None,
1022 label: None,
1023 }),
1024 Component::Avatar(AvatarProps {
1025 src: None,
1026 alt: "User".to_string(),
1027 fallback: Some("U".to_string()),
1028 size: None,
1029 }),
1030 Component::Skeleton(SkeletonProps {
1031 width: None,
1032 height: None,
1033 rounded: None,
1034 }),
1035 ];
1036 assert_eq!(components.len(), 20, "should have 20 component variants");
1037 let expected_types = [
1038 "Card",
1039 "Table",
1040 "Form",
1041 "Button",
1042 "Input",
1043 "Select",
1044 "Alert",
1045 "Badge",
1046 "Modal",
1047 "Text",
1048 "Checkbox",
1049 "Switch",
1050 "Separator",
1051 "DescriptionList",
1052 "Tabs",
1053 "Breadcrumb",
1054 "Pagination",
1055 "Progress",
1056 "Avatar",
1057 "Skeleton",
1058 ];
1059 for (component, expected_type) in components.iter().zip(expected_types.iter()) {
1060 let json = serde_json::to_value(component).unwrap();
1061 assert_eq!(
1062 json["type"], *expected_type,
1063 "component should serialize with type={expected_type}"
1064 );
1065 let roundtripped: Component = serde_json::from_value(json).unwrap();
1066 assert_eq!(&roundtripped, component);
1067 }
1068 }
1069
1070 #[test]
1071 fn size_enum_serialization() {
1072 let cases = [
1073 (Size::Xs, "xs"),
1074 (Size::Sm, "sm"),
1075 (Size::Default, "default"),
1076 (Size::Lg, "lg"),
1077 ];
1078 for (size, expected) in &cases {
1079 let json = serde_json::to_value(size).unwrap();
1080 assert_eq!(json, *expected);
1081 let parsed: Size = serde_json::from_value(json).unwrap();
1082 assert_eq!(&parsed, size);
1083 }
1084 }
1085
1086 #[test]
1087 fn icon_position_serialization() {
1088 let cases = [(IconPosition::Left, "left"), (IconPosition::Right, "right")];
1089 for (pos, expected) in &cases {
1090 let json = serde_json::to_value(pos).unwrap();
1091 assert_eq!(json, *expected);
1092 let parsed: IconPosition = serde_json::from_value(json).unwrap();
1093 assert_eq!(&parsed, pos);
1094 }
1095 }
1096
1097 #[test]
1098 fn sort_direction_serialization() {
1099 let cases = [(SortDirection::Asc, "asc"), (SortDirection::Desc, "desc")];
1100 for (dir, expected) in &cases {
1101 let json = serde_json::to_value(dir).unwrap();
1102 assert_eq!(json, *expected);
1103 let parsed: SortDirection = serde_json::from_value(json).unwrap();
1104 assert_eq!(&parsed, dir);
1105 }
1106 }
1107
1108 #[test]
1109 fn button_with_size_and_icon() {
1110 let button = Component::Button(ButtonProps {
1111 label: "Save".to_string(),
1112 variant: ButtonVariant::Default,
1113 size: Size::Lg,
1114 disabled: None,
1115 icon: Some("save".to_string()),
1116 icon_position: Some(IconPosition::Left),
1117 });
1118 let json = serde_json::to_value(&button).unwrap();
1119 assert_eq!(json["size"], "lg");
1120 assert_eq!(json["icon"], "save");
1121 assert_eq!(json["icon_position"], "left");
1122 let parsed: Component = serde_json::from_value(json).unwrap();
1123 assert_eq!(parsed, button);
1124 }
1125
1126 #[test]
1127 fn card_with_footer() {
1128 let card = Component::Card(CardProps {
1129 title: "Actions".to_string(),
1130 description: None,
1131 children: vec![],
1132 footer: vec![ComponentNode {
1133 key: "cancel".to_string(),
1134 component: Component::Button(ButtonProps {
1135 label: "Cancel".to_string(),
1136 variant: ButtonVariant::Outline,
1137 size: Size::Default,
1138 disabled: None,
1139 icon: None,
1140 icon_position: None,
1141 }),
1142 action: None,
1143 visibility: None,
1144 }],
1145 });
1146 let json = serde_json::to_value(&card).unwrap();
1147 assert!(json["footer"].is_array());
1148 assert_eq!(json["footer"][0]["label"], "Cancel");
1149 let parsed: Component = serde_json::from_value(json).unwrap();
1150 assert_eq!(parsed, card);
1151 }
1152
1153 #[test]
1154 fn input_with_error_and_description() {
1155 let input = Component::Input(InputProps {
1156 field: "email".to_string(),
1157 label: "Email".to_string(),
1158 input_type: InputType::Email,
1159 placeholder: None,
1160 required: Some(true),
1161 disabled: Some(false),
1162 error: Some("Invalid email".to_string()),
1163 description: Some("Your work email".to_string()),
1164 default_value: Some("user@example.com".to_string()),
1165 data_path: None,
1166 step: None,
1167 });
1168 let json = serde_json::to_value(&input).unwrap();
1169 assert_eq!(json["error"], "Invalid email");
1170 assert_eq!(json["description"], "Your work email");
1171 assert_eq!(json["default_value"], "user@example.com");
1172 assert_eq!(json["disabled"], false);
1173 let parsed: Component = serde_json::from_value(json).unwrap();
1174 assert_eq!(parsed, input);
1175 }
1176
1177 #[test]
1178 fn select_with_default_value() {
1179 let select = Component::Select(SelectProps {
1180 field: "role".to_string(),
1181 label: "Role".to_string(),
1182 options: vec![SelectOption {
1183 value: "admin".to_string(),
1184 label: "Admin".to_string(),
1185 }],
1186 placeholder: None,
1187 required: None,
1188 disabled: Some(true),
1189 error: Some("Required field".to_string()),
1190 description: Some("User role".to_string()),
1191 default_value: Some("admin".to_string()),
1192 data_path: None,
1193 });
1194 let json = serde_json::to_value(&select).unwrap();
1195 assert_eq!(json["default_value"], "admin");
1196 assert_eq!(json["error"], "Required field");
1197 assert_eq!(json["description"], "User role");
1198 assert_eq!(json["disabled"], true);
1199 let parsed: Component = serde_json::from_value(json).unwrap();
1200 assert_eq!(parsed, select);
1201 }
1202
1203 #[test]
1204 fn alert_with_title() {
1205 let alert = Component::Alert(AlertProps {
1206 message: "Something happened".to_string(),
1207 variant: AlertVariant::Warning,
1208 title: Some("Warning".to_string()),
1209 });
1210 let json = serde_json::to_value(&alert).unwrap();
1211 assert_eq!(json["title"], "Warning");
1212 assert_eq!(json["message"], "Something happened");
1213 let parsed: Component = serde_json::from_value(json).unwrap();
1214 assert_eq!(parsed, alert);
1215 }
1216
1217 #[test]
1218 fn modal_with_footer_and_description() {
1219 let modal = Component::Modal(ModalProps {
1220 title: "Delete Item".to_string(),
1221 description: Some("This action cannot be undone.".to_string()),
1222 children: vec![],
1223 footer: vec![ComponentNode {
1224 key: "confirm".to_string(),
1225 component: Component::Button(ButtonProps {
1226 label: "Delete".to_string(),
1227 variant: ButtonVariant::Destructive,
1228 size: Size::Default,
1229 disabled: None,
1230 icon: None,
1231 icon_position: None,
1232 }),
1233 action: None,
1234 visibility: None,
1235 }],
1236 trigger_label: Some("Delete".to_string()),
1237 });
1238 let json = serde_json::to_value(&modal).unwrap();
1239 assert_eq!(json["description"], "This action cannot be undone.");
1240 assert!(json["footer"].is_array());
1241 assert_eq!(json["footer"][0]["label"], "Delete");
1242 let parsed: Component = serde_json::from_value(json).unwrap();
1243 assert_eq!(parsed, modal);
1244 }
1245
1246 #[test]
1247 fn table_with_sort_props() {
1248 let table = Component::Table(TableProps {
1249 columns: vec![Column {
1250 key: "name".to_string(),
1251 label: "Name".to_string(),
1252 format: None,
1253 }],
1254 data_path: "/data/users".to_string(),
1255 row_actions: None,
1256 empty_message: None,
1257 sortable: Some(true),
1258 sort_column: Some("name".to_string()),
1259 sort_direction: Some(SortDirection::Desc),
1260 });
1261 let json = serde_json::to_value(&table).unwrap();
1262 assert_eq!(json["sortable"], true);
1263 assert_eq!(json["sort_column"], "name");
1264 assert_eq!(json["sort_direction"], "desc");
1265 let parsed: Component = serde_json::from_value(json).unwrap();
1266 assert_eq!(parsed, table);
1267 }
1268
1269 #[test]
1270 fn aligned_button_variants_serialize() {
1271 let cases = [
1272 (ButtonVariant::Default, "default"),
1273 (ButtonVariant::Secondary, "secondary"),
1274 (ButtonVariant::Destructive, "destructive"),
1275 (ButtonVariant::Outline, "outline"),
1276 (ButtonVariant::Ghost, "ghost"),
1277 (ButtonVariant::Link, "link"),
1278 ];
1279 for (variant, expected) in &cases {
1280 let json = serde_json::to_value(variant).unwrap();
1281 assert_eq!(
1282 json, *expected,
1283 "ButtonVariant::{variant:?} should serialize as {expected}"
1284 );
1285 let parsed: ButtonVariant = serde_json::from_value(json).unwrap();
1286 assert_eq!(&parsed, variant);
1287 }
1288 }
1289
1290 #[test]
1291 fn aligned_badge_variants_serialize() {
1292 let cases = [
1293 (BadgeVariant::Default, "default"),
1294 (BadgeVariant::Secondary, "secondary"),
1295 (BadgeVariant::Destructive, "destructive"),
1296 (BadgeVariant::Outline, "outline"),
1297 ];
1298 for (variant, expected) in &cases {
1299 let json = serde_json::to_value(variant).unwrap();
1300 assert_eq!(
1301 json, *expected,
1302 "BadgeVariant::{variant:?} should serialize as {expected}"
1303 );
1304 let parsed: BadgeVariant = serde_json::from_value(json).unwrap();
1305 assert_eq!(&parsed, variant);
1306 }
1307 }
1308
1309 #[test]
1310 fn checkbox_round_trips() {
1311 let checkbox = Component::Checkbox(CheckboxProps {
1312 field: "terms".to_string(),
1313 label: "Accept Terms".to_string(),
1314 description: Some("You must accept the terms".to_string()),
1315 checked: Some(true),
1316 data_path: None,
1317 required: Some(true),
1318 disabled: Some(false),
1319 error: None,
1320 });
1321 let json = serde_json::to_value(&checkbox).unwrap();
1322 assert_eq!(json["type"], "Checkbox");
1323 assert_eq!(json["field"], "terms");
1324 assert_eq!(json["checked"], true);
1325 assert_eq!(json["description"], "You must accept the terms");
1326 let parsed: Component = serde_json::from_value(json).unwrap();
1327 assert_eq!(parsed, checkbox);
1328 }
1329
1330 #[test]
1331 fn switch_round_trips() {
1332 let switch = Component::Switch(SwitchProps {
1333 field: "notifications".to_string(),
1334 label: "Enable Notifications".to_string(),
1335 description: Some("Receive email notifications".to_string()),
1336 checked: Some(false),
1337 data_path: None,
1338 required: None,
1339 disabled: Some(false),
1340 error: None,
1341 });
1342 let json = serde_json::to_value(&switch).unwrap();
1343 assert_eq!(json["type"], "Switch");
1344 assert_eq!(json["field"], "notifications");
1345 assert_eq!(json["checked"], false);
1346 let parsed: Component = serde_json::from_value(json).unwrap();
1347 assert_eq!(parsed, switch);
1348 }
1349
1350 #[test]
1351 fn separator_defaults_to_horizontal() {
1352 let json = r#"{"type": "Separator"}"#;
1353 let component: Component = serde_json::from_str(json).unwrap();
1354 match component {
1355 Component::Separator(props) => {
1356 assert_eq!(props.orientation, None);
1357 let explicit = Component::Separator(SeparatorProps {
1360 orientation: Some(Orientation::Horizontal),
1361 });
1362 let v = serde_json::to_value(&explicit).unwrap();
1363 assert_eq!(v["orientation"], "horizontal");
1364 let parsed: Component = serde_json::from_value(v).unwrap();
1365 assert_eq!(parsed, explicit);
1366 }
1367 _ => panic!("expected Separator"),
1368 }
1369 }
1370
1371 #[test]
1372 fn description_list_with_format() {
1373 let dl = Component::DescriptionList(DescriptionListProps {
1374 items: vec![
1375 DescriptionItem {
1376 label: "Created".to_string(),
1377 value: "2026-01-15".to_string(),
1378 format: Some(ColumnFormat::Date),
1379 },
1380 DescriptionItem {
1381 label: "Name".to_string(),
1382 value: "Alice".to_string(),
1383 format: None,
1384 },
1385 ],
1386 columns: Some(2),
1387 });
1388 let json = serde_json::to_value(&dl).unwrap();
1389 assert_eq!(json["type"], "DescriptionList");
1390 assert_eq!(json["columns"], 2);
1391 assert_eq!(json["items"][0]["format"], "date");
1392 assert!(json["items"][1].get("format").is_none());
1393 let parsed: Component = serde_json::from_value(json).unwrap();
1394 assert_eq!(parsed, dl);
1395 }
1396
1397 #[test]
1398 fn checkbox_with_error() {
1399 let checkbox = Component::Checkbox(CheckboxProps {
1400 field: "agree".to_string(),
1401 label: "I agree".to_string(),
1402 description: None,
1403 checked: None,
1404 data_path: None,
1405 required: Some(true),
1406 disabled: None,
1407 error: Some("You must agree".to_string()),
1408 });
1409 let json = serde_json::to_value(&checkbox).unwrap();
1410 assert_eq!(json["error"], "You must agree");
1411 assert!(json.get("description").is_none());
1412 assert!(json.get("checked").is_none());
1413 let parsed: Component = serde_json::from_value(json).unwrap();
1414 assert_eq!(parsed, checkbox);
1415 }
1416
1417 #[test]
1418 fn tabs_round_trips() {
1419 let tabs = Component::Tabs(TabsProps {
1420 default_tab: "general".to_string(),
1421 tabs: vec![
1422 Tab {
1423 value: "general".to_string(),
1424 label: "General".to_string(),
1425 children: vec![ComponentNode {
1426 key: "name-input".to_string(),
1427 component: Component::Input(InputProps {
1428 field: "name".to_string(),
1429 label: "Name".to_string(),
1430 input_type: InputType::Text,
1431 placeholder: None,
1432 required: None,
1433 disabled: None,
1434 error: None,
1435 description: None,
1436 default_value: None,
1437 data_path: None,
1438 step: None,
1439 }),
1440 action: None,
1441 visibility: None,
1442 }],
1443 },
1444 Tab {
1445 value: "security".to_string(),
1446 label: "Security".to_string(),
1447 children: vec![ComponentNode {
1448 key: "password-input".to_string(),
1449 component: Component::Input(InputProps {
1450 field: "password".to_string(),
1451 label: "Password".to_string(),
1452 input_type: InputType::Password,
1453 placeholder: None,
1454 required: None,
1455 disabled: None,
1456 error: None,
1457 description: None,
1458 default_value: None,
1459 data_path: None,
1460 step: None,
1461 }),
1462 action: None,
1463 visibility: None,
1464 }],
1465 },
1466 ],
1467 });
1468 let json = serde_json::to_string(&tabs).unwrap();
1469 let parsed: Component = serde_json::from_str(&json).unwrap();
1470 assert_eq!(parsed, tabs);
1471 }
1472
1473 #[test]
1474 fn breadcrumb_round_trips() {
1475 let breadcrumb = Component::Breadcrumb(BreadcrumbProps {
1476 items: vec![
1477 BreadcrumbItem {
1478 label: "Home".to_string(),
1479 url: Some("/".to_string()),
1480 },
1481 BreadcrumbItem {
1482 label: "Users".to_string(),
1483 url: Some("/users".to_string()),
1484 },
1485 BreadcrumbItem {
1486 label: "Edit User".to_string(),
1487 url: None,
1488 },
1489 ],
1490 });
1491 let json = serde_json::to_string(&breadcrumb).unwrap();
1492 let parsed: Component = serde_json::from_str(&json).unwrap();
1493 assert_eq!(parsed, breadcrumb);
1494
1495 let value = serde_json::to_value(&breadcrumb).unwrap();
1497 assert!(value["items"][2].get("url").is_none());
1498 }
1499
1500 #[test]
1501 fn pagination_round_trips() {
1502 let pagination = Component::Pagination(PaginationProps {
1503 current_page: 3,
1504 per_page: 25,
1505 total: 150,
1506 base_url: None,
1507 });
1508 let json = serde_json::to_string(&pagination).unwrap();
1509 let parsed: Component = serde_json::from_str(&json).unwrap();
1510 assert_eq!(parsed, pagination);
1511 }
1512
1513 #[test]
1514 fn progress_round_trips() {
1515 let progress = Component::Progress(ProgressProps {
1516 value: 75,
1517 max: Some(100),
1518 label: Some("Uploading...".to_string()),
1519 });
1520 let json = serde_json::to_string(&progress).unwrap();
1521 let parsed: Component = serde_json::from_str(&json).unwrap();
1522 assert_eq!(parsed, progress);
1523
1524 let value = serde_json::to_value(&progress).unwrap();
1525 assert_eq!(value["value"], 75);
1526 assert_eq!(value["max"], 100);
1527 assert_eq!(value["label"], "Uploading...");
1528 }
1529
1530 #[test]
1531 fn avatar_with_fallback() {
1532 let avatar = Component::Avatar(AvatarProps {
1533 src: None,
1534 alt: "John Doe".to_string(),
1535 fallback: Some("JD".to_string()),
1536 size: Some(Size::Lg),
1537 });
1538 let json = serde_json::to_string(&avatar).unwrap();
1539 let parsed: Component = serde_json::from_str(&json).unwrap();
1540 assert_eq!(parsed, avatar);
1541
1542 let value = serde_json::to_value(&avatar).unwrap();
1543 assert!(value.get("src").is_none());
1544 assert_eq!(value["fallback"], "JD");
1545 assert_eq!(value["size"], "lg");
1546 }
1547
1548 #[test]
1549 fn skeleton_round_trips() {
1550 let skeleton = Component::Skeleton(SkeletonProps {
1551 width: Some("100%".to_string()),
1552 height: Some("40px".to_string()),
1553 rounded: Some(true),
1554 });
1555 let json = serde_json::to_string(&skeleton).unwrap();
1556 let parsed: Component = serde_json::from_str(&json).unwrap();
1557 assert_eq!(parsed, skeleton);
1558
1559 let value = serde_json::to_value(&skeleton).unwrap();
1560 assert_eq!(value["width"], "100%");
1561 assert_eq!(value["height"], "40px");
1562 assert_eq!(value["rounded"], true);
1563 }
1564
1565 #[test]
1566 fn tabs_deserializes_from_json() {
1567 let json = r#"{
1568 "type": "Tabs",
1569 "default_tab": "general",
1570 "tabs": [
1571 {
1572 "value": "general",
1573 "label": "General",
1574 "children": [
1575 {
1576 "key": "name-input",
1577 "type": "Input",
1578 "field": "name",
1579 "label": "Name"
1580 }
1581 ]
1582 },
1583 {
1584 "value": "security",
1585 "label": "Security"
1586 }
1587 ]
1588 }"#;
1589 let component: Component = serde_json::from_str(json).unwrap();
1590 match component {
1591 Component::Tabs(props) => {
1592 assert_eq!(props.default_tab, "general");
1593 assert_eq!(props.tabs.len(), 2);
1594 assert_eq!(props.tabs[0].value, "general");
1595 assert_eq!(props.tabs[0].children.len(), 1);
1596 assert_eq!(props.tabs[1].value, "security");
1597 assert!(props.tabs[1].children.is_empty());
1598 }
1599 _ => panic!("expected Tabs"),
1600 }
1601 }
1602
1603 #[test]
1604 fn input_data_path_round_trips() {
1605 let input = Component::Input(InputProps {
1606 field: "name".to_string(),
1607 label: "Name".to_string(),
1608 input_type: InputType::Text,
1609 placeholder: None,
1610 required: None,
1611 disabled: None,
1612 error: None,
1613 description: None,
1614 default_value: None,
1615 data_path: Some("/data/user/name".to_string()),
1616 step: None,
1617 });
1618 let json = serde_json::to_value(&input).unwrap();
1619 assert_eq!(json["data_path"], "/data/user/name");
1620 let parsed: Component = serde_json::from_value(json).unwrap();
1621 assert_eq!(parsed, input);
1622 }
1623
1624 #[test]
1625 fn select_data_path_round_trips() {
1626 let select = Component::Select(SelectProps {
1627 field: "role".to_string(),
1628 label: "Role".to_string(),
1629 options: vec![SelectOption {
1630 value: "admin".to_string(),
1631 label: "Admin".to_string(),
1632 }],
1633 placeholder: None,
1634 required: None,
1635 disabled: None,
1636 error: None,
1637 description: None,
1638 default_value: None,
1639 data_path: Some("/data/user/role".to_string()),
1640 });
1641 let json = serde_json::to_value(&select).unwrap();
1642 assert_eq!(json["data_path"], "/data/user/role");
1643 let parsed: Component = serde_json::from_value(json).unwrap();
1644 assert_eq!(parsed, select);
1645 }
1646
1647 #[test]
1648 fn checkbox_data_path_round_trips() {
1649 let checkbox = Component::Checkbox(CheckboxProps {
1650 field: "terms".to_string(),
1651 label: "Accept Terms".to_string(),
1652 description: None,
1653 checked: None,
1654 data_path: Some("/data/user/accepted_terms".to_string()),
1655 required: None,
1656 disabled: None,
1657 error: None,
1658 });
1659 let json = serde_json::to_value(&checkbox).unwrap();
1660 assert_eq!(json["data_path"], "/data/user/accepted_terms");
1661 let parsed: Component = serde_json::from_value(json).unwrap();
1662 assert_eq!(parsed, checkbox);
1663 }
1664
1665 #[test]
1666 fn switch_data_path_round_trips() {
1667 let switch = Component::Switch(SwitchProps {
1668 field: "notifications".to_string(),
1669 label: "Enable Notifications".to_string(),
1670 description: None,
1671 checked: None,
1672 data_path: Some("/data/user/notifications_enabled".to_string()),
1673 required: None,
1674 disabled: None,
1675 error: None,
1676 });
1677 let json = serde_json::to_value(&switch).unwrap();
1678 assert_eq!(json["data_path"], "/data/user/notifications_enabled");
1679 let parsed: Component = serde_json::from_value(json).unwrap();
1680 assert_eq!(parsed, switch);
1681 }
1682
1683 #[test]
1686 fn unknown_type_deserializes_as_plugin() {
1687 let json = r#"{"type": "Map", "center": [40.7, -74.0], "zoom": 12}"#;
1688 let component: Component = serde_json::from_str(json).unwrap();
1689 match component {
1690 Component::Plugin(props) => {
1691 assert_eq!(props.plugin_type, "Map");
1692 assert_eq!(props.props["center"][0], 40.7);
1693 assert_eq!(props.props["center"][1], -74.0);
1694 assert_eq!(props.props["zoom"], 12);
1695 assert!(props.props.get("type").is_none());
1697 }
1698 _ => panic!("expected Plugin"),
1699 }
1700 }
1701
1702 #[test]
1703 fn plugin_round_trips() {
1704 let plugin = Component::Plugin(PluginProps {
1705 plugin_type: "Chart".to_string(),
1706 props: serde_json::json!({"data": [1, 2, 3], "style": "bar"}),
1707 });
1708 let json = serde_json::to_value(&plugin).unwrap();
1709 assert_eq!(json["type"], "Chart");
1710 assert_eq!(json["data"], serde_json::json!([1, 2, 3]));
1711 assert_eq!(json["style"], "bar");
1712
1713 let parsed: Component = serde_json::from_value(json).unwrap();
1714 assert_eq!(parsed, plugin);
1715 }
1716
1717 #[test]
1718 fn plugin_serializes_with_type_field() {
1719 let plugin = Component::Plugin(PluginProps {
1720 plugin_type: "Map".to_string(),
1721 props: serde_json::json!({"lat": 51.5, "lng": -0.1}),
1722 });
1723 let json = serde_json::to_value(&plugin).unwrap();
1724 assert_eq!(json["type"], "Map");
1725 assert_eq!(json["lat"], 51.5);
1726 assert_eq!(json["lng"], -0.1);
1727 }
1728
1729 #[test]
1730 fn plugin_with_empty_props() {
1731 let json = r#"{"type": "CustomWidget"}"#;
1732 let component: Component = serde_json::from_str(json).unwrap();
1733 match component {
1734 Component::Plugin(props) => {
1735 assert_eq!(props.plugin_type, "CustomWidget");
1736 assert!(props.props.as_object().unwrap().is_empty());
1737 }
1738 _ => panic!("expected Plugin"),
1739 }
1740 }
1741
1742 #[test]
1743 fn plugin_in_component_node() {
1744 let node = ComponentNode {
1745 key: "map-1".to_string(),
1746 component: Component::Plugin(PluginProps {
1747 plugin_type: "Map".to_string(),
1748 props: serde_json::json!({"center": [0.0, 0.0]}),
1749 }),
1750 action: None,
1751 visibility: None,
1752 };
1753 let json = serde_json::to_string(&node).unwrap();
1754 let parsed: ComponentNode = serde_json::from_str(&json).unwrap();
1755 assert_eq!(parsed, node);
1756
1757 let value = serde_json::to_value(&node).unwrap();
1758 assert_eq!(value["type"], "Map");
1759 assert_eq!(value["key"], "map-1");
1760 }
1761
1762 #[test]
1763 fn known_types_not_treated_as_plugin() {
1764 let known_types = [
1766 "Card",
1767 "Table",
1768 "Form",
1769 "Button",
1770 "Input",
1771 "Select",
1772 "Alert",
1773 "Badge",
1774 "Modal",
1775 "Text",
1776 "Checkbox",
1777 "Switch",
1778 "Separator",
1779 "DescriptionList",
1780 "Tabs",
1781 "Breadcrumb",
1782 "Pagination",
1783 "Progress",
1784 "Avatar",
1785 "Skeleton",
1786 ];
1787 for type_name in &known_types {
1788 let json_str = match *type_name {
1791 "Card" => r#"{"type":"Card","title":"t"}"#,
1792 "Table" => r#"{"type":"Table","columns":[],"data_path":"/d"}"#,
1793 "Form" => r#"{"type":"Form","action":{"handler":"h","method":"POST"},"fields":[]}"#,
1794 "Button" => r#"{"type":"Button","label":"b"}"#,
1795 "Input" => r#"{"type":"Input","field":"f","label":"l"}"#,
1796 "Select" => r#"{"type":"Select","field":"f","label":"l","options":[]}"#,
1797 "Alert" => r#"{"type":"Alert","message":"m"}"#,
1798 "Badge" => r#"{"type":"Badge","label":"b"}"#,
1799 "Modal" => r#"{"type":"Modal","title":"t"}"#,
1800 "Text" => r#"{"type":"Text","content":"c"}"#,
1801 "Checkbox" => r#"{"type":"Checkbox","field":"f","label":"l"}"#,
1802 "Switch" => r#"{"type":"Switch","field":"f","label":"l"}"#,
1803 "Separator" => r#"{"type":"Separator"}"#,
1804 "DescriptionList" => r#"{"type":"DescriptionList","items":[]}"#,
1805 "Tabs" => r#"{"type":"Tabs","default_tab":"t","tabs":[]}"#,
1806 "Breadcrumb" => r#"{"type":"Breadcrumb","items":[]}"#,
1807 "Pagination" => r#"{"type":"Pagination","current_page":1,"per_page":10,"total":0}"#,
1808 "Progress" => r#"{"type":"Progress","value":0}"#,
1809 "Avatar" => r#"{"type":"Avatar","alt":"a"}"#,
1810 "Skeleton" => r#"{"type":"Skeleton"}"#,
1811 _ => unreachable!(),
1812 };
1813 let component: Component = serde_json::from_str(json_str).unwrap();
1814 assert!(
1815 !matches!(component, Component::Plugin(_)),
1816 "type {type_name} should not deserialize as Plugin"
1817 );
1818 }
1819 }
1820}