Skip to main content

dampen_cli/commands/check/
attributes.rs

1use dampen_core::ir::WidgetKind;
2use std::collections::HashSet;
3
4/// Schema defining valid attributes for a widget type.
5///
6/// This is a wrapper around `dampen_core::schema::WidgetSchema` to maintain
7/// backward compatibility with existing CLI tests and logic.
8#[derive(Debug, Clone)]
9pub struct WidgetAttributeSchema {
10    pub required: HashSet<&'static str>,
11    pub optional: HashSet<&'static str>,
12    pub events: HashSet<&'static str>,
13    pub style_attributes: HashSet<&'static str>,
14    pub layout_attributes: HashSet<&'static str>,
15}
16
17impl WidgetAttributeSchema {
18    /// Returns the schema for a specific widget kind.
19    pub fn for_widget(kind: &WidgetKind) -> Self {
20        let core_schema = kind.schema();
21
22        // Helper to convert slice to HashSet
23        let to_set = |slice: &'static [&'static str]| -> HashSet<&'static str> {
24            slice.iter().copied().collect()
25        };
26
27        Self {
28            required: to_set(core_schema.required),
29            optional: to_set(core_schema.optional),
30            events: to_set(core_schema.events),
31            style_attributes: to_set(core_schema.style_attributes),
32            layout_attributes: to_set(core_schema.layout_attributes),
33        }
34    }
35
36    /// Returns all valid attributes (required + optional + events + style + layout).
37    pub fn all_valid(&self) -> HashSet<&'static str> {
38        let mut all = HashSet::new();
39        all.extend(self.required.iter().copied());
40        all.extend(self.optional.iter().copied());
41        all.extend(self.events.iter().copied());
42        all.extend(self.style_attributes.iter().copied());
43        all.extend(self.layout_attributes.iter().copied());
44        all
45    }
46
47    /// Returns a list of all valid attribute names as a vector.
48    pub fn all_valid_names(&self) -> Vec<&'static str> {
49        self.all_valid().into_iter().collect()
50    }
51}
52
53/// Validates widget attributes and detects unknown attributes.
54///
55/// Returns a list of unknown attributes with suggestions.
56pub fn validate_widget_attributes(
57    widget_kind: &WidgetKind,
58    attributes: &[String],
59) -> Vec<(String, Option<String>)> {
60    use crate::commands::check::suggestions;
61
62    let schema = WidgetAttributeSchema::for_widget(widget_kind);
63    let valid_attrs = schema.all_valid();
64    let valid_names = schema.all_valid_names();
65
66    let mut unknown_attrs = Vec::new();
67
68    for attr in attributes {
69        if !valid_attrs.contains(attr.as_str()) {
70            // Generate suggestion using Levenshtein distance
71            let suggestion = suggestions::find_closest_match(attr, &valid_names, 3)
72                .map(|(matched, _)| matched.to_string());
73
74            unknown_attrs.push((attr.clone(), suggestion));
75        }
76    }
77
78    unknown_attrs
79}
80
81/// Checks if an attribute is valid for a widget.
82pub fn is_valid_attribute(widget_kind: &WidgetKind, attribute: &str) -> bool {
83    let schema = WidgetAttributeSchema::for_widget(widget_kind);
84    schema.all_valid().contains(attribute)
85}
86
87/// Validates that all required attributes are present for a widget.
88///
89/// Returns a list of missing required attributes.
90pub fn validate_required_attributes(
91    widget_kind: &WidgetKind,
92    attributes: &[String],
93) -> Vec<String> {
94    let schema = WidgetAttributeSchema::for_widget(widget_kind);
95
96    // Find all required attributes that are not present in the provided attributes
97    schema
98        .required
99        .iter()
100        .filter(|&&req| !attributes.iter().any(|attr| attr == req))
101        .map(|&s| s.to_string())
102        .collect()
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_text_widget_schema() {
111        let schema = WidgetAttributeSchema::for_widget(&WidgetKind::Text);
112        assert!(schema.required.contains("value"));
113        assert!(schema.optional.contains("size"));
114        assert!(!schema.required.contains("size"));
115    }
116
117    #[test]
118    fn test_image_widget_schema() {
119        let schema = WidgetAttributeSchema::for_widget(&WidgetKind::Image);
120        assert!(schema.required.contains("src"));
121        assert!(schema.optional.contains("width"));
122    }
123
124    #[test]
125    fn test_button_widget_schema() {
126        let schema = WidgetAttributeSchema::for_widget(&WidgetKind::Button);
127        assert!(schema.events.contains("on_click"));
128        assert!(schema.optional.contains("label"));
129    }
130
131    #[test]
132    fn test_radio_widget_schema() {
133        let schema = WidgetAttributeSchema::for_widget(&WidgetKind::Radio);
134        assert!(schema.required.contains("label"));
135        assert!(schema.required.contains("value"));
136        assert!(schema.optional.contains("selected"));
137        assert!(schema.events.contains("on_select"));
138    }
139
140    #[test]
141    fn test_all_valid_includes_all_categories() {
142        let schema = WidgetAttributeSchema::for_widget(&WidgetKind::Text);
143        let all = schema.all_valid();
144
145        // Should include required
146        assert!(all.contains("value"));
147
148        // Should include optional
149        assert!(all.contains("size"));
150
151        // Should include style
152        assert!(all.contains("background"));
153
154        // Should include layout
155        assert!(all.contains("width"));
156
157        // Should include events
158        assert!(all.contains("on_click"));
159    }
160
161    #[test]
162    fn test_all_valid_names_returns_vec() {
163        let schema = WidgetAttributeSchema::for_widget(&WidgetKind::Button);
164        let names = schema.all_valid_names();
165
166        assert!(!names.is_empty());
167        assert!(names.contains(&"on_click"));
168    }
169
170    #[test]
171    fn test_validate_widget_attributes_valid() {
172        let attrs = vec!["on_click".to_string(), "label".to_string()];
173        let unknown = validate_widget_attributes(&WidgetKind::Button, &attrs);
174        assert!(unknown.is_empty());
175    }
176
177    #[test]
178    fn test_validate_widget_attributes_unknown() {
179        let attrs = vec!["on_clik".to_string(), "unknown".to_string()];
180        let unknown = validate_widget_attributes(&WidgetKind::Button, &attrs);
181        assert_eq!(unknown.len(), 2);
182
183        // First should have suggestion for "on_click"
184        assert_eq!(unknown[0].0, "on_clik");
185        assert!(unknown[0].1.is_some());
186        assert_eq!(unknown[0].1.as_ref().unwrap(), "on_click");
187
188        // Second might not have a good suggestion
189        assert_eq!(unknown[1].0, "unknown");
190    }
191
192    #[test]
193    fn test_is_valid_attribute() {
194        assert!(is_valid_attribute(&WidgetKind::Button, "on_click"));
195        assert!(is_valid_attribute(&WidgetKind::Button, "label"));
196        assert!(!is_valid_attribute(&WidgetKind::Button, "on_clik"));
197    }
198
199    #[test]
200    fn test_validate_required_attributes_all_present() {
201        let attrs = vec!["value".to_string(), "size".to_string()];
202        let missing = validate_required_attributes(&WidgetKind::Text, &attrs);
203        assert!(missing.is_empty());
204    }
205
206    #[test]
207    fn test_validate_required_attributes_missing_value() {
208        let attrs = vec!["size".to_string(), "color".to_string()];
209        let missing = validate_required_attributes(&WidgetKind::Text, &attrs);
210        assert_eq!(missing.len(), 1);
211        assert_eq!(missing[0], "value");
212    }
213
214    #[test]
215    fn test_validate_required_attributes_image_missing_src() {
216        let attrs = vec!["width".to_string(), "height".to_string()];
217        let missing = validate_required_attributes(&WidgetKind::Image, &attrs);
218        assert_eq!(missing.len(), 1);
219        assert_eq!(missing[0], "src");
220    }
221
222    #[test]
223    fn test_validate_required_attributes_radio_missing_both() {
224        let attrs = vec!["selected".to_string()];
225        let missing = validate_required_attributes(&WidgetKind::Radio, &attrs);
226        assert_eq!(missing.len(), 2);
227        assert!(missing.contains(&"label".to_string()));
228        assert!(missing.contains(&"value".to_string()));
229    }
230
231    #[test]
232    fn test_validate_required_attributes_button_no_required() {
233        let attrs = vec!["on_click".to_string()];
234        let missing = validate_required_attributes(&WidgetKind::Button, &attrs);
235        assert!(missing.is_empty());
236    }
237}