dampen_cli/commands/check/
attributes.rs1use dampen_core::ir::WidgetKind;
2use std::collections::HashSet;
3
4#[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 pub fn for_widget(kind: &WidgetKind) -> Self {
20 let core_schema = kind.schema();
21
22 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 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 pub fn all_valid_names(&self) -> Vec<&'static str> {
49 self.all_valid().into_iter().collect()
50 }
51}
52
53pub 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 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
81pub 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
87pub fn validate_required_attributes(
91 widget_kind: &WidgetKind,
92 attributes: &[String],
93) -> Vec<String> {
94 let schema = WidgetAttributeSchema::for_widget(widget_kind);
95
96 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 assert!(all.contains("value"));
147
148 assert!(all.contains("size"));
150
151 assert!(all.contains("background"));
153
154 assert!(all.contains("width"));
156
157 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 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 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}