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