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    "width",
70    "height",
71    "min_width",
72    "max_width",
73    "min_height",
74    "max_height",
75    "padding",
76    "spacing",
77    "align_items",
78    "justify_content",
79    "align",
80    "align_x",
81    "align_y",
82    "align_self",
83    "direction",
84    "position",
85    "top",
86    "right",
87    "bottom",
88    "left",
89    "z_index",
90    "class",
91    "theme",
92    "theme_ref",
93];
94
95/// Common event attributes shared by most interactive widgets.
96pub const COMMON_EVENTS: &[&str] = &[
97    "on_click",
98    "on_press",
99    "on_release",
100    "on_change",
101    "on_input",
102    "on_submit",
103    "on_select",
104    "on_toggle",
105    "on_scroll",
106];
107
108impl WidgetSchema {
109    /// Returns a `HashSet` containing all valid attributes for this schema.
110    ///
111    /// This combines required, optional, events, style, and layout attributes.
112    pub fn all_valid(&self) -> HashSet<&'static str> {
113        let mut set = HashSet::new();
114        set.extend(self.required.iter().cloned());
115        set.extend(self.optional.iter().cloned());
116        set.extend(self.events.iter().cloned());
117        set.extend(self.style_attributes.iter().cloned());
118        set.extend(self.layout_attributes.iter().cloned());
119        set
120    }
121
122    /// Returns a `Vec` containing all valid attribute names.
123    pub fn all_valid_names(&self) -> Vec<&'static str> {
124        let mut names = Vec::new();
125        names.extend_from_slice(self.required);
126        names.extend_from_slice(self.optional);
127        names.extend_from_slice(self.events);
128        names.extend_from_slice(self.style_attributes);
129        names.extend_from_slice(self.layout_attributes);
130        names
131    }
132}
133
134/// Returns the validation schema for a given widget kind.
135///
136/// # Examples
137///
138/// ```
139/// use dampen_core::ir::WidgetKind;
140/// use dampen_core::schema::get_widget_schema;
141///
142/// let schema = get_widget_schema(&WidgetKind::Text);
143/// assert!(schema.required.contains(&"value"));
144/// ```
145pub fn get_widget_schema(kind: &WidgetKind) -> WidgetSchema {
146    match kind {
147        WidgetKind::Text => WidgetSchema {
148            required: &["value"],
149            optional: &["size", "weight", "color"],
150            events: COMMON_EVENTS,
151            style_attributes: COMMON_STYLE_ATTRIBUTES,
152            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
153        },
154        WidgetKind::Image => WidgetSchema {
155            required: &["src"],
156            optional: &["width", "height", "fit", "filter_method", "path"],
157            events: COMMON_EVENTS,
158            style_attributes: COMMON_STYLE_ATTRIBUTES,
159            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
160        },
161        WidgetKind::Button => WidgetSchema {
162            required: &[],
163            optional: &["label", "enabled"],
164            events: &["on_click", "on_press", "on_release"],
165            style_attributes: COMMON_STYLE_ATTRIBUTES,
166            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
167        },
168        WidgetKind::TextInput => WidgetSchema {
169            required: &[],
170            optional: &["placeholder", "value", "password", "icon", "size"],
171            events: &["on_input", "on_submit", "on_change", "on_paste"],
172            style_attributes: COMMON_STYLE_ATTRIBUTES,
173            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
174        },
175        WidgetKind::Checkbox => WidgetSchema {
176            required: &[],
177            optional: &["checked", "label", "icon", "size"],
178            events: &["on_toggle"],
179            style_attributes: COMMON_STYLE_ATTRIBUTES,
180            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
181        },
182        WidgetKind::Radio => WidgetSchema {
183            required: &["label", "value"],
184            optional: &[
185                "selected",
186                "disabled",
187                "size",
188                "text_size",
189                "text_line_height",
190                "text_shaping",
191            ],
192            events: &["on_select"],
193            style_attributes: COMMON_STYLE_ATTRIBUTES,
194            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
195        },
196        WidgetKind::Slider => WidgetSchema {
197            required: &[],
198            optional: &["min", "max", "value", "step"],
199            events: &["on_change", "on_release"],
200            style_attributes: COMMON_STYLE_ATTRIBUTES,
201            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
202        },
203        WidgetKind::Column | WidgetKind::Row | WidgetKind::Container => WidgetSchema {
204            required: &[],
205            optional: &[],
206            events: COMMON_EVENTS,
207            style_attributes: COMMON_STYLE_ATTRIBUTES,
208            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
209        },
210        WidgetKind::Scrollable => WidgetSchema {
211            required: &[],
212            optional: &[],
213            events: &["on_scroll"],
214            style_attributes: COMMON_STYLE_ATTRIBUTES,
215            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
216        },
217        WidgetKind::Stack => WidgetSchema {
218            required: &[],
219            optional: &[],
220            events: COMMON_EVENTS,
221            style_attributes: COMMON_STYLE_ATTRIBUTES,
222            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
223        },
224        WidgetKind::Svg => WidgetSchema {
225            required: &["src"],
226            optional: &["width", "height", "path"],
227            events: COMMON_EVENTS,
228            style_attributes: COMMON_STYLE_ATTRIBUTES,
229            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
230        },
231        WidgetKind::PickList => WidgetSchema {
232            required: &[],
233            optional: &["placeholder", "selected", "options"],
234            events: &["on_select"],
235            style_attributes: COMMON_STYLE_ATTRIBUTES,
236            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
237        },
238        WidgetKind::Toggler => WidgetSchema {
239            required: &[],
240            optional: &["checked", "active", "label"],
241            events: &["on_toggle"],
242            style_attributes: COMMON_STYLE_ATTRIBUTES,
243            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
244        },
245        WidgetKind::Space | WidgetKind::Rule => WidgetSchema {
246            required: &[],
247            optional: &[],
248            events: &[],
249            style_attributes: COMMON_STYLE_ATTRIBUTES,
250            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
251        },
252        WidgetKind::ComboBox => WidgetSchema {
253            required: &[],
254            optional: &["placeholder", "value", "options"],
255            events: &["on_input", "on_select"],
256            style_attributes: COMMON_STYLE_ATTRIBUTES,
257            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
258        },
259        WidgetKind::ProgressBar => WidgetSchema {
260            required: &[],
261            optional: &["value", "min", "max", "style"],
262            events: &[],
263            style_attributes: COMMON_STYLE_ATTRIBUTES,
264            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
265        },
266        WidgetKind::Tooltip => WidgetSchema {
267            required: &[],
268            optional: &["message", "position", "delay"],
269            events: COMMON_EVENTS,
270            style_attributes: COMMON_STYLE_ATTRIBUTES,
271            // Tooltip is a special case that typically wraps another widget but doesn't have layout itself in the same way?
272            // Data model says "no layout attributes".
273            layout_attributes: &[],
274        },
275        WidgetKind::Grid => WidgetSchema {
276            required: &[],
277            optional: &["columns"],
278            events: COMMON_EVENTS,
279            style_attributes: COMMON_STYLE_ATTRIBUTES,
280            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
281        },
282        WidgetKind::Canvas => WidgetSchema {
283            required: &[],
284            optional: &["program"],
285            events: &["on_draw"],
286            style_attributes: COMMON_STYLE_ATTRIBUTES,
287            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
288        },
289        WidgetKind::Float => WidgetSchema {
290            required: &[],
291            optional: &[],
292            events: COMMON_EVENTS,
293            style_attributes: COMMON_STYLE_ATTRIBUTES,
294            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
295        },
296        WidgetKind::For => WidgetSchema {
297            required: &["each", "in"],
298            optional: &["template"],
299            events: &[],
300            style_attributes: COMMON_STYLE_ATTRIBUTES,
301            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
302        },
303        WidgetKind::If => WidgetSchema {
304            required: &["condition"],
305            optional: &[],
306            events: &[],
307            style_attributes: COMMON_STYLE_ATTRIBUTES,
308            layout_attributes: COMMON_LAYOUT_ATTRIBUTES,
309        },
310        WidgetKind::Custom(_) => WidgetSchema {
311            required: &[],
312            optional: &[],
313            events: &[],
314            style_attributes: &[],
315            layout_attributes: &[],
316        },
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::ir::WidgetKind;
324
325    #[test]
326    fn test_button_schema_contains_expected_attributes() {
327        let schema = WidgetKind::Button.schema();
328        let valid = schema.all_valid();
329
330        assert!(valid.contains("on_click"));
331        assert!(valid.contains("label"));
332        // Should also contain common attributes
333        assert!(valid.contains("width"));
334        assert!(valid.contains("background"));
335    }
336
337    #[test]
338    fn test_container_schema_includes_layout_attributes() {
339        let schema = WidgetKind::Container.schema();
340        let valid = schema.all_valid();
341
342        assert!(valid.contains("padding"));
343        assert!(valid.contains("align_x"));
344        assert!(valid.contains("width"));
345    }
346
347    #[test]
348    fn test_textinput_schema_includes_size() {
349        let schema = WidgetKind::TextInput.schema();
350        let valid = schema.all_valid();
351
352        assert!(valid.contains("size"));
353        assert!(valid.contains("placeholder"));
354    }
355
356    #[test]
357    fn test_custom_widget_returns_permissive_schema() {
358        let schema = WidgetKind::Custom("MyWidget".to_string()).schema();
359        assert!(schema.required.is_empty());
360    }
361
362    #[test]
363    fn test_all_widget_kinds_have_schema() {
364        let kinds = [
365            WidgetKind::Column,
366            WidgetKind::Row,
367            WidgetKind::Text,
368            WidgetKind::Button,
369            WidgetKind::Image,
370        ];
371
372        for kind in kinds {
373            let _ = kind.schema();
374        }
375    }
376}