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            ],
273            events: &[],
274            style_attributes: COMMON_STYLE_ATTRIBUTES,
275            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
276        },
277        WidgetKind::Tooltip => WidgetSchema {
278            required: &[],
279            optional: &["message", "position", "delay"],
280            events: COMMON_EVENTS,
281            style_attributes: COMMON_STYLE_ATTRIBUTES,
282            // Tooltip is a special case that typically wraps another widget but doesn't have layout itself in the same way?
283            // Data model says "no layout attributes".
284            layout_attributes: &[],
285        },
286        WidgetKind::Grid => WidgetSchema {
287            required: &[],
288            optional: &["columns"],
289            events: COMMON_EVENTS,
290            style_attributes: COMMON_STYLE_ATTRIBUTES,
291            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
292        },
293        WidgetKind::Canvas => WidgetSchema {
294            required: &[],
295            optional: &["width", "height", "program", "cache"],
296            events: &["on_click", "on_drag", "on_move", "on_release"],
297            style_attributes: COMMON_STYLE_ATTRIBUTES,
298            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
299        },
300        WidgetKind::CanvasRect => WidgetSchema {
301            required: &["x", "y", "width", "height"],
302            optional: &["fill", "stroke", "stroke_width", "radius"],
303            events: &[],
304            style_attributes: &[],
305            layout_attributes: &[],
306        },
307        WidgetKind::CanvasCircle => WidgetSchema {
308            required: &["cx", "cy", "radius"],
309            optional: &["fill", "stroke", "stroke_width"],
310            events: &[],
311            style_attributes: &[],
312            layout_attributes: &[],
313        },
314        WidgetKind::CanvasLine => WidgetSchema {
315            required: &["x1", "y1", "x2", "y2"],
316            optional: &["stroke", "stroke_width"],
317            events: &[],
318            style_attributes: &[],
319            layout_attributes: &[],
320        },
321        WidgetKind::CanvasText => WidgetSchema {
322            required: &["x", "y", "content"],
323            optional: &["size", "color"],
324            events: &[],
325            style_attributes: &[],
326            layout_attributes: &[],
327        },
328        WidgetKind::CanvasGroup => WidgetSchema {
329            required: &[],
330            optional: &["transform"],
331            events: &[],
332            style_attributes: &[],
333            layout_attributes: &[],
334        },
335        WidgetKind::DatePicker => WidgetSchema {
336            required: &[],
337            optional: &["value", "format", "show", "min_date", "max_date"],
338            events: &["on_submit", "on_cancel"],
339            style_attributes: COMMON_STYLE_ATTRIBUTES,
340            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
341        },
342        WidgetKind::TimePicker => WidgetSchema {
343            required: &[],
344            optional: &["value", "format", "show", "use_24h", "show_seconds"],
345            events: &["on_submit", "on_cancel"],
346            style_attributes: COMMON_STYLE_ATTRIBUTES,
347            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
348        },
349        WidgetKind::ColorPicker => WidgetSchema {
350            required: &[],
351            optional: &["value", "show", "show_alpha", "enabled"],
352            events: &["on_submit", "on_cancel", "on_change"],
353            style_attributes: COMMON_STYLE_ATTRIBUTES,
354            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
355        },
356        WidgetKind::Menu => WidgetSchema {
357            required: &[],
358            optional: &["position", "close_on_select", "width", "spacing", "class"],
359            events: &["on_open", "on_close"],
360            style_attributes: COMMON_STYLE_ATTRIBUTES,
361            layout_attributes: &["width", "padding"],
362        },
363        WidgetKind::MenuItem => WidgetSchema {
364            required: &["label"],
365            optional: &["icon", "shortcut", "disabled", "class"],
366            events: &["on_click"],
367            style_attributes: COMMON_STYLE_ATTRIBUTES,
368            layout_attributes: &["padding"],
369        },
370        WidgetKind::MenuSeparator => WidgetSchema {
371            required: &[],
372            optional: &[],
373            events: &[],
374            style_attributes: &["color", "opacity"],
375            layout_attributes: &["height"],
376        },
377        WidgetKind::ContextMenu => WidgetSchema {
378            required: &[],
379            optional: &["context"],
380            events: &["on_open", "on_close"],
381            style_attributes: COMMON_STYLE_ATTRIBUTES,
382            layout_attributes: &[],
383        },
384        WidgetKind::Float => WidgetSchema {
385            required: &[],
386            optional: &[],
387            events: COMMON_EVENTS,
388            style_attributes: COMMON_STYLE_ATTRIBUTES,
389            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
390        },
391        WidgetKind::DataTable => WidgetSchema {
392            required: &["data"],
393            optional: &[
394                "width",
395                "height",
396                "min_width",
397                "max_width",
398                "scrollbar_width",
399            ],
400            events: &["on_row_click"],
401            style_attributes: COMMON_STYLE_ATTRIBUTES,
402            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
403        },
404        WidgetKind::DataColumn => WidgetSchema {
405            required: &["header"],
406            optional: &["field", "width", "min_width", "max_width", "align"],
407            events: &[],
408            style_attributes: &[],
409            layout_attributes: &[],
410        },
411        WidgetKind::For => WidgetSchema {
412            required: &["each", "in"],
413            optional: &["template"],
414            events: &[],
415            style_attributes: COMMON_STYLE_ATTRIBUTES,
416            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
417        },
418        WidgetKind::If => WidgetSchema {
419            required: &["condition"],
420            optional: &[],
421            events: &[],
422            style_attributes: COMMON_STYLE_ATTRIBUTES,
423            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
424        },
425        WidgetKind::TreeView => WidgetSchema {
426            required: &[],
427            optional: &[
428                "nodes",
429                "expanded",
430                "selected",
431                "indent_size",
432                "node_height",
433                "icon_size",
434                "expand_icon",
435                "collapse_icon",
436                "leaf_icon",
437            ],
438            events: &["on_toggle", "on_select", "on_double_click"],
439            style_attributes: COMMON_STYLE_ATTRIBUTES,
440            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
441        },
442        WidgetKind::TreeNode => WidgetSchema {
443            required: &["id", "label"],
444            optional: &["icon", "expanded", "selected", "disabled", "class"],
445            events: &[],
446            style_attributes: &[],
447            layout_attributes: &[],
448        },
449        WidgetKind::TabBar => WidgetSchema {
450            required: &["selected"],
451            optional: &[
452                "spacing",
453                "padding",
454                "icon_size",
455                "text_size",
456                "width",
457                "height",
458                "class",
459            ],
460            events: &["on_select"],
461            style_attributes: COMMON_STYLE_ATTRIBUTES,
462            layout_attributes: &["width", "height"],
463        },
464        WidgetKind::Tab => WidgetSchema {
465            required: &[],
466            optional: &["label", "icon", "enabled"],
467            events: &["on_click"],
468            style_attributes: COMMON_STYLE_ATTRIBUTES,
469            layout_attributes: &[],
470        },
471        WidgetKind::Custom(_) => WidgetSchema {
472            required: &[],
473            optional: &[],
474            events: &[],
475            style_attributes: &[],
476            layout_attributes: &[],
477        },
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use crate::ir::WidgetKind;
485
486    #[test]
487    fn test_button_schema_contains_expected_attributes() {
488        let schema = WidgetKind::Button.schema();
489        let valid = schema.all_valid();
490
491        assert!(valid.contains("on_click"));
492        assert!(valid.contains("label"));
493        // Should also contain common attributes
494        assert!(valid.contains("width"));
495        assert!(valid.contains("background"));
496    }
497
498    #[test]
499    fn test_container_schema_includes_layout_attributes() {
500        let schema = WidgetKind::Container.schema();
501        let valid = schema.all_valid();
502
503        assert!(valid.contains("padding"));
504        assert!(valid.contains("align_x"));
505        assert!(valid.contains("width"));
506    }
507
508    #[test]
509    fn test_textinput_schema_includes_size() {
510        let schema = WidgetKind::TextInput.schema();
511        let valid = schema.all_valid();
512
513        assert!(valid.contains("size"));
514        assert!(valid.contains("placeholder"));
515    }
516
517    #[test]
518    fn test_custom_widget_returns_permissive_schema() {
519        let schema = WidgetKind::Custom("MyWidget".to_string()).schema();
520        assert!(schema.required.is_empty());
521    }
522
523    #[test]
524    fn test_all_widget_kinds_have_schema() {
525        let kinds = [
526            WidgetKind::Column,
527            WidgetKind::Row,
528            WidgetKind::Text,
529            WidgetKind::Button,
530            WidgetKind::Image,
531        ];
532
533        for kind in kinds {
534            let _ = kind.schema();
535        }
536    }
537}