1use crate::schema::*;
6
7#[derive(Debug, Clone)]
9pub struct ValidationError {
10 pub path: String,
11 pub message: String,
12}
13
14impl std::fmt::Display for ValidationError {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 write!(f, "{}: {}", self.path, self.message)
17 }
18}
19
20impl std::error::Error for ValidationError {}
21
22pub trait Validate {
24 fn validate(&self, path: &str) -> Vec<ValidationError>;
25}
26
27impl Validate for UiResponse {
28 fn validate(&self, path: &str) -> Vec<ValidationError> {
29 let mut errors = Vec::new();
30
31 if self.components.is_empty() {
32 errors.push(ValidationError {
33 path: path.to_string(),
34 message: "UiResponse must have at least one component".to_string(),
35 });
36 }
37
38 for (i, component) in self.components.iter().enumerate() {
39 errors.extend(component.validate(&format!("{}.components[{}]", path, i)));
40 }
41
42 errors
43 }
44}
45
46impl Validate for Component {
47 fn validate(&self, path: &str) -> Vec<ValidationError> {
48 let mut errors = Vec::new();
49
50 match self {
51 Component::Text(t) => {
52 if t.content.is_empty() {
53 errors.push(ValidationError {
54 path: format!("{}.content", path),
55 message: "Text content cannot be empty".to_string(),
56 });
57 }
58 }
59 Component::Button(b) => {
60 if b.label.is_empty() {
61 errors.push(ValidationError {
62 path: format!("{}.label", path),
63 message: "Button label cannot be empty".to_string(),
64 });
65 }
66 if b.action_id.is_empty() {
67 errors.push(ValidationError {
68 path: format!("{}.action_id", path),
69 message: "Button action_id cannot be empty".to_string(),
70 });
71 }
72 }
73 Component::TextInput(t) => {
74 if t.name.is_empty() {
75 errors.push(ValidationError {
76 path: format!("{}.name", path),
77 message: "TextInput name cannot be empty".to_string(),
78 });
79 }
80 if let (Some(min), Some(max)) = (t.min_length, t.max_length) {
81 if min > max {
82 errors.push(ValidationError {
83 path: format!("{}.min_length", path),
84 message: "min_length cannot be greater than max_length".to_string(),
85 });
86 }
87 }
88 }
89 Component::NumberInput(n) => {
90 if n.name.is_empty() {
91 errors.push(ValidationError {
92 path: format!("{}.name", path),
93 message: "NumberInput name cannot be empty".to_string(),
94 });
95 }
96 if n.min > n.max {
97 errors.push(ValidationError {
98 path: format!("{}.min", path),
99 message: "min cannot be greater than max".to_string(),
100 });
101 }
102 }
103 Component::Select(s) => {
104 if s.name.is_empty() {
105 errors.push(ValidationError {
106 path: format!("{}.name", path),
107 message: "Select name cannot be empty".to_string(),
108 });
109 }
110 if s.options.is_empty() {
111 errors.push(ValidationError {
112 path: format!("{}.options", path),
113 message: "Select must have at least one option".to_string(),
114 });
115 }
116 }
117 Component::Table(t) => {
118 if t.columns.is_empty() {
119 errors.push(ValidationError {
120 path: format!("{}.columns", path),
121 message: "Table must have at least one column".to_string(),
122 });
123 }
124 }
125 Component::Chart(c) => {
126 if c.data.is_empty() {
127 errors.push(ValidationError {
128 path: format!("{}.data", path),
129 message: "Chart must have data".to_string(),
130 });
131 }
132 if c.y_keys.is_empty() {
133 errors.push(ValidationError {
134 path: format!("{}.y_keys", path),
135 message: "Chart must have at least one y_key".to_string(),
136 });
137 }
138 }
139 Component::Card(c) => {
140 for (i, child) in c.content.iter().enumerate() {
141 errors.extend(child.validate(&format!("{}.content[{}]", path, i)));
142 }
143 if let Some(footer) = &c.footer {
144 for (i, child) in footer.iter().enumerate() {
145 errors.extend(child.validate(&format!("{}.footer[{}]", path, i)));
146 }
147 }
148 }
149 Component::Modal(m) => {
150 for (i, child) in m.content.iter().enumerate() {
151 errors.extend(child.validate(&format!("{}.content[{}]", path, i)));
152 }
153 }
154 Component::Stack(s) => {
155 for (i, child) in s.children.iter().enumerate() {
156 errors.extend(child.validate(&format!("{}.children[{}]", path, i)));
157 }
158 }
159 Component::Grid(g) => {
160 for (i, child) in g.children.iter().enumerate() {
161 errors.extend(child.validate(&format!("{}.children[{}]", path, i)));
162 }
163 }
164 Component::Tabs(t) => {
165 if t.tabs.is_empty() {
166 errors.push(ValidationError {
167 path: format!("{}.tabs", path),
168 message: "Tabs must have at least one tab".to_string(),
169 });
170 }
171 }
172 _ => {}
174 }
175
176 errors
177 }
178}
179
180pub fn validate_ui_response(ui: &UiResponse) -> Result<(), Vec<ValidationError>> {
182 let errors = ui.validate("UiResponse");
183 if errors.is_empty() {
184 Ok(())
185 } else {
186 Err(errors)
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_empty_response_fails() {
196 let ui = UiResponse::new(vec![]);
197 let result = validate_ui_response(&ui);
198 assert!(result.is_err());
199 }
200
201 #[test]
202 fn test_valid_text_passes() {
203 let ui = UiResponse::new(vec![Component::Text(Text {
204 id: None,
205 content: "Hello".to_string(),
206 variant: TextVariant::Body,
207 })]);
208 let result = validate_ui_response(&ui);
209 assert!(result.is_ok());
210 }
211
212 #[test]
213 fn test_empty_button_label_fails() {
214 let ui = UiResponse::new(vec![Component::Button(Button {
215 id: None,
216 label: "".to_string(),
217 action_id: "click".to_string(),
218 variant: ButtonVariant::Primary,
219 disabled: false,
220 icon: None,
221 })]);
222 let result = validate_ui_response(&ui);
223 assert!(result.is_err());
224 }
225}