Skip to main content

adk_ui/
validation.rs

1//! Validation for UI components
2//!
3//! Server-side validation to catch malformed UiResponse before sending to client.
4
5use crate::schema::*;
6
7/// Validation error for UI components
8#[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
22/// Trait for validatable UI components
23pub 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 Text {
47    fn validate(&self, path: &str) -> Vec<ValidationError> {
48        let mut errors = Vec::new();
49        if self.content.is_empty() {
50            errors.push(ValidationError {
51                path: format!("{}.content", path),
52                message: "Text content cannot be empty".to_string(),
53            });
54        }
55        errors
56    }
57}
58
59impl Validate for Button {
60    fn validate(&self, path: &str) -> Vec<ValidationError> {
61        let mut errors = Vec::new();
62        if self.label.is_empty() {
63            errors.push(ValidationError {
64                path: format!("{}.label", path),
65                message: "Button label cannot be empty".to_string(),
66            });
67        }
68        if self.action_id.is_empty() {
69            errors.push(ValidationError {
70                path: format!("{}.action_id", path),
71                message: "Button action_id cannot be empty".to_string(),
72            });
73        }
74        errors
75    }
76}
77
78impl Validate for TextInput {
79    fn validate(&self, path: &str) -> Vec<ValidationError> {
80        let mut errors = Vec::new();
81        if self.name.is_empty() {
82            errors.push(ValidationError {
83                path: format!("{}.name", path),
84                message: "TextInput name cannot be empty".to_string(),
85            });
86        }
87        if let (Some(min), Some(max)) = (self.min_length, self.max_length) {
88            if min > max {
89                errors.push(ValidationError {
90                    path: format!("{}.min_length", path),
91                    message: "min_length cannot be greater than max_length".to_string(),
92                });
93            }
94        }
95        errors
96    }
97}
98
99impl Validate for NumberInput {
100    fn validate(&self, path: &str) -> Vec<ValidationError> {
101        let mut errors = Vec::new();
102        if self.name.is_empty() {
103            errors.push(ValidationError {
104                path: format!("{}.name", path),
105                message: "NumberInput name cannot be empty".to_string(),
106            });
107        }
108        if let (Some(min), Some(max)) = (self.min, self.max) {
109            if min > max {
110                errors.push(ValidationError {
111                    path: format!("{}.min", path),
112                    message: "min cannot be greater than max".to_string(),
113                });
114            }
115        }
116        errors
117    }
118}
119
120impl Validate for Select {
121    fn validate(&self, path: &str) -> Vec<ValidationError> {
122        let mut errors = Vec::new();
123        if self.name.is_empty() {
124            errors.push(ValidationError {
125                path: format!("{}.name", path),
126                message: "Select name cannot be empty".to_string(),
127            });
128        }
129        if self.options.is_empty() {
130            errors.push(ValidationError {
131                path: format!("{}.options", path),
132                message: "Select must have at least one option".to_string(),
133            });
134        }
135        errors
136    }
137}
138
139impl Validate for Table {
140    fn validate(&self, path: &str) -> Vec<ValidationError> {
141        let mut errors = Vec::new();
142        if self.columns.is_empty() {
143            errors.push(ValidationError {
144                path: format!("{}.columns", path),
145                message: "Table must have at least one column".to_string(),
146            });
147        }
148        errors
149    }
150}
151
152impl Validate for Chart {
153    fn validate(&self, path: &str) -> Vec<ValidationError> {
154        let mut errors = Vec::new();
155        if self.data.is_empty() {
156            errors.push(ValidationError {
157                path: format!("{}.data", path),
158                message: "Chart must have data".to_string(),
159            });
160        }
161        if self.y_keys.is_empty() {
162            errors.push(ValidationError {
163                path: format!("{}.y_keys", path),
164                message: "Chart must have at least one y_key".to_string(),
165            });
166        }
167        errors
168    }
169}
170
171impl Validate for Card {
172    fn validate(&self, path: &str) -> Vec<ValidationError> {
173        let mut errors = Vec::new();
174        for (i, child) in self.content.iter().enumerate() {
175            errors.extend(child.validate(&format!("{}.content[{}]", path, i)));
176        }
177        if let Some(footer) = &self.footer {
178            for (i, child) in footer.iter().enumerate() {
179                errors.extend(child.validate(&format!("{}.footer[{}]", path, i)));
180            }
181        }
182        errors
183    }
184}
185
186impl Validate for Modal {
187    fn validate(&self, path: &str) -> Vec<ValidationError> {
188        let mut errors = Vec::new();
189        for (i, child) in self.content.iter().enumerate() {
190            errors.extend(child.validate(&format!("{}.content[{}]", path, i)));
191        }
192        errors
193    }
194}
195
196impl Validate for Stack {
197    fn validate(&self, path: &str) -> Vec<ValidationError> {
198        let mut errors = Vec::new();
199        for (i, child) in self.children.iter().enumerate() {
200            errors.extend(child.validate(&format!("{}.children[{}]", path, i)));
201        }
202        errors
203    }
204}
205
206impl Validate for Grid {
207    fn validate(&self, path: &str) -> Vec<ValidationError> {
208        let mut errors = Vec::new();
209        for (i, child) in self.children.iter().enumerate() {
210            errors.extend(child.validate(&format!("{}.children[{}]", path, i)));
211        }
212        errors
213    }
214}
215
216impl Validate for Tabs {
217    fn validate(&self, path: &str) -> Vec<ValidationError> {
218        let mut errors = Vec::new();
219        if self.tabs.is_empty() {
220            errors.push(ValidationError {
221                path: format!("{}.tabs", path),
222                message: "Tabs must have at least one tab".to_string(),
223            });
224        }
225        errors
226    }
227}
228
229impl Validate for Component {
230    fn validate(&self, path: &str) -> Vec<ValidationError> {
231        match self {
232            Component::Text(t) => t.validate(path),
233            Component::Button(b) => b.validate(path),
234            Component::TextInput(t) => t.validate(path),
235            Component::NumberInput(n) => n.validate(path),
236            Component::Select(s) => s.validate(path),
237            Component::Table(t) => t.validate(path),
238            Component::Chart(c) => c.validate(path),
239            Component::Card(c) => c.validate(path),
240            Component::Modal(m) => m.validate(path),
241            Component::Stack(s) => s.validate(path),
242            Component::Grid(g) => g.validate(path),
243            Component::Tabs(t) => t.validate(path),
244            // Components with no additional validation constraints
245            _ => Vec::new(),
246        }
247    }
248}
249
250/// Validate a UiResponse and return Result
251pub fn validate_ui_response(ui: &UiResponse) -> Result<(), Vec<ValidationError>> {
252    let errors = ui.validate("UiResponse");
253    if errors.is_empty() { Ok(()) } else { Err(errors) }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_empty_response_fails() {
262        let ui = UiResponse::new(vec![]);
263        let result = validate_ui_response(&ui);
264        assert!(result.is_err());
265    }
266
267    #[test]
268    fn test_valid_text_passes() {
269        let ui = UiResponse::new(vec![Component::Text(Text {
270            id: None,
271            content: "Hello".to_string(),
272            variant: TextVariant::Body,
273        })]);
274        let result = validate_ui_response(&ui);
275        assert!(result.is_ok());
276    }
277
278    #[test]
279    fn test_empty_button_label_fails() {
280        let ui = UiResponse::new(vec![Component::Button(Button {
281            id: None,
282            label: "".to_string(),
283            action_id: "click".to_string(),
284            variant: ButtonVariant::Primary,
285            disabled: false,
286            icon: None,
287        })]);
288        let result = validate_ui_response(&ui);
289        assert!(result.is_err());
290    }
291}