Skip to main content

dampen_core/schema/
mod.rs

1//! validation for attribute checking.
2//!
3//! # Examples
4//!
5//! ```
6//! use dampen_core::ir::WidgetKind;
7//! use dampen_core::schema::get_widget_schema;
8//!
9//! let schema = get_widget_schema(&WidgetKind::Button);
10//! assert!(schema.events.contains(&"on_click"));
11//! ```
12
13use crate::ir::WidgetKind;
14use std::collections::HashSet;
15
16/// Represents the validation contract for a single widget type.
17///
18/// Contains lists of valid attributes categorized by type (required, optional, events, etc.).
19///
20/// # Examples
21///
22/// ```
23/// use dampen_core::schema::WidgetSchema;
24///
25/// let schema = WidgetSchema {
26///     required: &["value"],
27///     optional: &[],
28///     events: &[],
29///     style_attributes: &[],
30///     layout_attributes: &[],
31/// };
32///
33/// assert!(schema.all_valid().contains("value"));
34/// ```
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct WidgetSchema {
37    /// Attributes that MUST be present on the widget.
38    pub required: &'static [&'static str],
39    /// Attributes that MAY be present on the widget.
40    pub optional: &'static [&'static str],
41    /// Event handler attributes (e.g., "on_click").
42    pub events: &'static [&'static str],
43    /// Styling attributes (e.g., "background", "color").
44    pub style_attributes: &'static [&'static str],
45    /// Layout attributes (e.g., "width", "padding").
46    pub layout_attributes: &'static [&'static str],
47}
48
49/// Common styling attributes shared by most widgets.
50pub const COMMON_STYLE_ATTRIBUTES: &[&str] = &[
51    "background",
52    "color",
53    "border_color",
54    "border_width",
55    "border_radius",
56    "border_style",
57    "shadow",
58    "opacity",
59    "transform",
60    "style",
61    "text_color",
62    "shadow_color",
63    "shadow_offset",
64    "shadow_blur_radius",
65];
66
67/// Common layout attributes shared by most widgets.
68pub const COMMON_LAYOUT_ATTRIBUTES: &[&str] = &[
69    "id",
70    "width",
71    "height",
72    "min_width",
73    "max_width",
74    "min_height",
75    "max_height",
76    "padding",
77    "spacing",
78    "align_items",
79    "justify_content",
80    "align",
81    "align_x",
82    "align_y",
83    "align_self",
84    "direction",
85    "position",
86    "top",
87    "right",
88    "bottom",
89    "left",
90    "z_index",
91    "class",
92    "theme",
93    "theme_ref",
94];
95
96/// Common event attributes shared by most interactive widgets.
97pub const COMMON_EVENTS: &[&str] = &[
98    "on_click",
99    "on_press",
100    "on_release",
101    "on_change",
102    "on_input",
103    "on_submit",
104    "on_select",
105    "on_toggle",
106    "on_scroll",
107];
108
109impl WidgetSchema {
110    /// Returns a `HashSet` containing all valid attributes for this schema.
111    ///
112    /// This combines required, optional, events, style, and layout attributes.
113    pub fn all_valid(&self) -> HashSet<&'static str> {
114        let mut set = HashSet::new();
115        set.extend(self.required.iter().cloned());
116        set.extend(self.optional.iter().cloned());
117        set.extend(self.events.iter().cloned());
118        set.extend(self.style_attributes.iter().cloned());
119        set.extend(self.layout_attributes.iter().cloned());
120        set
121    }
122
123    /// Returns a `Vec` containing all valid attribute names.
124    pub fn all_valid_names(&self) -> Vec<&'static str> {
125        let mut names = Vec::new();
126        names.extend_from_slice(self.required);
127        names.extend_from_slice(self.optional);
128        names.extend_from_slice(self.events);
129        names.extend_from_slice(self.style_attributes);
130        names.extend_from_slice(self.layout_attributes);
131        names
132    }
133}
134
135/// Returns the validation schema for a given widget kind.
136///
137/// # Examples
138///
139/// ```
140/// use dampen_core::ir::WidgetKind;
141/// use dampen_core::schema::get_widget_schema;
142///
143/// let schema = get_widget_schema(&WidgetKind::Text);
144/// assert!(schema.required.contains(&"value"));
145/// ```
146pub fn get_widget_schema(kind: &WidgetKind) -> WidgetSchema {
147    match kind {
148        WidgetKind::Text => WidgetSchema {
149            required: &["value"],
150            optional: &["size", "weight", "color"],
151            events: COMMON_EVENTS,
152            style_attributes: COMMON_STYLE_ATTRIBUTES,
153            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
154        },
155        WidgetKind::Image => WidgetSchema {
156            required: &["src"],
157            optional: &["width", "height", "fit", "filter_method", "path"],
158            events: COMMON_EVENTS,
159            style_attributes: COMMON_STYLE_ATTRIBUTES,
160            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
161        },
162        WidgetKind::Button => WidgetSchema {
163            required: &[],
164            optional: &["label", "enabled"],
165            events: &["on_click", "on_press", "on_release"],
166            style_attributes: COMMON_STYLE_ATTRIBUTES,
167            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
168        },
169        WidgetKind::TextInput => WidgetSchema {
170            required: &[],
171            optional: &["placeholder", "value", "password", "icon", "size"],
172            events: &["on_input", "on_submit", "on_change", "on_paste"],
173            style_attributes: COMMON_STYLE_ATTRIBUTES,
174            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
175        },
176        WidgetKind::Checkbox => WidgetSchema {
177            required: &[],
178            optional: &["checked", "label", "icon", "size"],
179            events: &["on_toggle"],
180            style_attributes: COMMON_STYLE_ATTRIBUTES,
181            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
182        },
183        WidgetKind::Radio => WidgetSchema {
184            required: &["label", "value"],
185            optional: &[
186                "id",
187                "selected",
188                "disabled",
189                "size",
190                "text_size",
191                "text_line_height",
192                "text_shaping",
193            ],
194            events: &["on_select"],
195            style_attributes: COMMON_STYLE_ATTRIBUTES,
196            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
197        },
198        WidgetKind::Slider => WidgetSchema {
199            required: &[],
200            optional: &["min", "max", "value", "step"],
201            events: &["on_change", "on_release"],
202            style_attributes: COMMON_STYLE_ATTRIBUTES,
203            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
204        },
205        WidgetKind::Column | WidgetKind::Row | WidgetKind::Container => WidgetSchema {
206            required: &[],
207            optional: &[],
208            events: COMMON_EVENTS,
209            style_attributes: COMMON_STYLE_ATTRIBUTES,
210            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
211        },
212        WidgetKind::Scrollable => WidgetSchema {
213            required: &[],
214            optional: &[],
215            events: &["on_scroll"],
216            style_attributes: COMMON_STYLE_ATTRIBUTES,
217            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
218        },
219        WidgetKind::Stack => WidgetSchema {
220            required: &[],
221            optional: &[],
222            events: COMMON_EVENTS,
223            style_attributes: COMMON_STYLE_ATTRIBUTES,
224            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
225        },
226        WidgetKind::Svg => WidgetSchema {
227            required: &["src"],
228            optional: &["width", "height", "path"],
229            events: COMMON_EVENTS,
230            style_attributes: COMMON_STYLE_ATTRIBUTES,
231            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
232        },
233        WidgetKind::PickList => WidgetSchema {
234            required: &[],
235            optional: &["placeholder", "selected", "options"],
236            events: &["on_select"],
237            style_attributes: COMMON_STYLE_ATTRIBUTES,
238            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
239        },
240        WidgetKind::Toggler => WidgetSchema {
241            required: &[],
242            optional: &["checked", "active", "toggled", "label"],
243            events: &["on_toggle"],
244            style_attributes: COMMON_STYLE_ATTRIBUTES,
245            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
246        },
247        WidgetKind::Space | WidgetKind::Rule => WidgetSchema {
248            required: &[],
249            optional: &[],
250            events: &[],
251            style_attributes: COMMON_STYLE_ATTRIBUTES,
252            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
253        },
254        WidgetKind::ComboBox => WidgetSchema {
255            required: &[],
256            optional: &["placeholder", "value", "selected", "options"],
257            events: &["on_input", "on_select"],
258            style_attributes: COMMON_STYLE_ATTRIBUTES,
259            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
260        },
261        WidgetKind::ProgressBar => WidgetSchema {
262            required: &[],
263            optional: &[
264                "value",
265                "min",
266                "max",
267                "style",
268                "bar_color",
269                "background_color",
270                "border_radius",
271                "height",
272                "width",
273            ],
274            events: &[],
275            style_attributes: COMMON_STYLE_ATTRIBUTES,
276            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
277        },
278        WidgetKind::Tooltip => WidgetSchema {
279            required: &[],
280            optional: &["message", "position", "delay"],
281            events: COMMON_EVENTS,
282            style_attributes: COMMON_STYLE_ATTRIBUTES,
283            // Tooltip is a special case that typically wraps another widget but doesn't have layout itself in the same way?
284            // Data model says "no layout attributes".
285            layout_attributes: &[],
286        },
287        WidgetKind::Grid => WidgetSchema {
288            required: &[],
289            optional: &["columns"],
290            events: COMMON_EVENTS,
291            style_attributes: COMMON_STYLE_ATTRIBUTES,
292            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
293        },
294        WidgetKind::Canvas => WidgetSchema {
295            required: &[],
296            optional: &["width", "height", "program", "cache"],
297            events: &["on_click", "on_drag", "on_move", "on_release"],
298            style_attributes: COMMON_STYLE_ATTRIBUTES,
299            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
300        },
301        WidgetKind::CanvasRect => WidgetSchema {
302            required: &["x", "y", "width", "height"],
303            optional: &["fill", "stroke", "stroke_width", "radius"],
304            events: &[],
305            style_attributes: &[],
306            layout_attributes: &[],
307        },
308        WidgetKind::CanvasCircle => WidgetSchema {
309            required: &["cx", "cy", "radius"],
310            optional: &["fill", "stroke", "stroke_width"],
311            events: &[],
312            style_attributes: &[],
313            layout_attributes: &[],
314        },
315        WidgetKind::CanvasLine => WidgetSchema {
316            required: &["x1", "y1", "x2", "y2"],
317            optional: &["stroke", "stroke_width"],
318            events: &[],
319            style_attributes: &[],
320            layout_attributes: &[],
321        },
322        WidgetKind::CanvasText => WidgetSchema {
323            required: &["x", "y", "content"],
324            optional: &["size", "color"],
325            events: &[],
326            style_attributes: &[],
327            layout_attributes: &[],
328        },
329        WidgetKind::CanvasGroup => WidgetSchema {
330            required: &[],
331            optional: &["transform"],
332            events: &[],
333            style_attributes: &[],
334            layout_attributes: &[],
335        },
336        WidgetKind::DatePicker => WidgetSchema {
337            required: &[],
338            optional: &["value", "format", "show", "min_date", "max_date"],
339            events: &["on_submit", "on_cancel"],
340            style_attributes: COMMON_STYLE_ATTRIBUTES,
341            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
342        },
343        WidgetKind::TimePicker => WidgetSchema {
344            required: &[],
345            optional: &["value", "format", "show", "use_24h", "show_seconds"],
346            events: &["on_submit", "on_cancel"],
347            style_attributes: COMMON_STYLE_ATTRIBUTES,
348            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
349        },
350        WidgetKind::ColorPicker => WidgetSchema {
351            required: &[],
352            optional: &["value", "show", "show_alpha", "enabled"],
353            events: &["on_submit", "on_cancel", "on_change"],
354            style_attributes: COMMON_STYLE_ATTRIBUTES,
355            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
356        },
357        WidgetKind::Menu => WidgetSchema {
358            required: &[],
359            optional: &["position", "close_on_select", "width", "spacing", "class"],
360            events: &["on_open", "on_close"],
361            style_attributes: COMMON_STYLE_ATTRIBUTES,
362            layout_attributes: &["width", "padding"],
363        },
364        WidgetKind::MenuItem => WidgetSchema {
365            required: &["label"],
366            optional: &["icon", "shortcut", "disabled", "class"],
367            events: &["on_click"],
368            style_attributes: COMMON_STYLE_ATTRIBUTES,
369            layout_attributes: &["padding"],
370        },
371        WidgetKind::MenuSeparator => WidgetSchema {
372            required: &[],
373            optional: &[],
374            events: &[],
375            style_attributes: &["color", "opacity"],
376            layout_attributes: &["height"],
377        },
378        WidgetKind::ContextMenu => WidgetSchema {
379            required: &[],
380            optional: &["context"],
381            events: &["on_open", "on_close"],
382            style_attributes: COMMON_STYLE_ATTRIBUTES,
383            layout_attributes: &[],
384        },
385        WidgetKind::Float => WidgetSchema {
386            required: &[],
387            optional: &[],
388            events: COMMON_EVENTS,
389            style_attributes: COMMON_STYLE_ATTRIBUTES,
390            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
391        },
392        WidgetKind::DataTable => WidgetSchema {
393            required: &["data"],
394            optional: &[
395                "width",
396                "height",
397                "min_width",
398                "max_width",
399                "scrollbar_width",
400            ],
401            events: &["on_row_click"],
402            style_attributes: COMMON_STYLE_ATTRIBUTES,
403            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
404        },
405        WidgetKind::DataColumn => WidgetSchema {
406            required: &["header"],
407            optional: &[
408                "field",
409                "width",
410                "min_width",
411                "max_width",
412                "align_x",
413                "align_y",
414            ],
415            events: &[],
416            style_attributes: &[],
417            layout_attributes: &[],
418        },
419        WidgetKind::For => WidgetSchema {
420            required: &["each", "in"],
421            optional: &["template"],
422            events: &[],
423            style_attributes: COMMON_STYLE_ATTRIBUTES,
424            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
425        },
426        WidgetKind::If => WidgetSchema {
427            required: &["condition"],
428            optional: &[],
429            events: &[],
430            style_attributes: COMMON_STYLE_ATTRIBUTES,
431            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
432        },
433        WidgetKind::TreeView => WidgetSchema {
434            required: &[],
435            optional: &[
436                "nodes",
437                "expanded",
438                "selected",
439                "indent_size",
440                "node_height",
441                "icon_size",
442                "expand_icon",
443                "collapse_icon",
444                "leaf_icon",
445            ],
446            events: &["on_toggle", "on_select", "on_double_click"],
447            style_attributes: COMMON_STYLE_ATTRIBUTES,
448            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
449        },
450        WidgetKind::TreeNode => WidgetSchema {
451            required: &["id", "label"],
452            optional: &["icon", "expanded", "selected", "disabled", "class"],
453            events: &[],
454            style_attributes: &[],
455            layout_attributes: &[],
456        },
457        WidgetKind::TabBar => WidgetSchema {
458            required: &["selected"],
459            optional: &[
460                "spacing",
461                "padding",
462                "icon_size",
463                "text_size",
464                "width",
465                "height",
466                "class",
467            ],
468            events: &["on_select"],
469            style_attributes: COMMON_STYLE_ATTRIBUTES,
470            layout_attributes: &["width", "height"],
471        },
472        WidgetKind::Tab => WidgetSchema {
473            required: &[],
474            optional: &["label", "icon", "enabled"],
475            events: &["on_click"],
476            style_attributes: COMMON_STYLE_ATTRIBUTES,
477            layout_attributes: &[],
478        },
479        WidgetKind::Custom(_) => WidgetSchema {
480            required: &[],
481            optional: &[],
482            events: &[],
483            style_attributes: &[],
484            layout_attributes: &[],
485        },
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use crate::ir::WidgetKind;
493
494    #[test]
495    fn test_button_schema_contains_expected_attributes() {
496        let schema = WidgetKind::Button.schema();
497        let valid = schema.all_valid();
498
499        assert!(valid.contains("on_click"));
500        assert!(valid.contains("label"));
501        // Should also contain common attributes
502        assert!(valid.contains("width"));
503        assert!(valid.contains("background"));
504    }
505
506    #[test]
507    fn test_container_schema_includes_layout_attributes() {
508        let schema = WidgetKind::Container.schema();
509        let valid = schema.all_valid();
510
511        assert!(valid.contains("padding"));
512        assert!(valid.contains("align_x"));
513        assert!(valid.contains("width"));
514    }
515
516    #[test]
517    fn test_textinput_schema_includes_size() {
518        let schema = WidgetKind::TextInput.schema();
519        let valid = schema.all_valid();
520
521        assert!(valid.contains("size"));
522        assert!(valid.contains("placeholder"));
523    }
524
525    #[test]
526    fn test_custom_widget_returns_permissive_schema() {
527        let schema = WidgetKind::Custom("MyWidget".to_string()).schema();
528        assert!(schema.required.is_empty());
529    }
530
531    #[test]
532    fn test_all_widget_kinds_have_schema() {
533        let kinds = [
534            WidgetKind::Column,
535            WidgetKind::Row,
536            WidgetKind::Text,
537            WidgetKind::Button,
538            WidgetKind::Image,
539        ];
540
541        for kind in kinds {
542            let _ = kind.schema();
543        }
544    }
545}