1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct WidgetSchema {
8 pub name: String,
10 pub description: String,
12 pub default_role: SemanticRole,
14 pub properties: Vec<PropertySchema>,
16 #[serde(default, skip_serializing_if = "Vec::is_empty")]
18 pub actions: Vec<super::AgentAction>,
19 pub usage_hint: Option<String>,
21 pub tags: Vec<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub struct PropertySchema {
28 pub name: String,
30 pub description: String,
32 pub property_type: PropertyType,
34 pub required: bool,
36 pub default_value: Option<serde_json::Value>,
38 pub constraints: Vec<PropertyConstraint>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44pub enum PropertyType {
45 String,
46 Integer,
47 Float,
48 Boolean,
49 Color,
50 Style,
51 Rect,
52 Size,
53 Position,
54 Enum(Vec<String>),
55 Array(Box<PropertyType>),
56 Object(Vec<PropertySchema>),
57 Widget,
58 Any,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub enum PropertyConstraint {
64 Min(f64),
65 Max(f64),
66 MinLength(usize),
67 MaxLength(usize),
68 Pattern(String),
69 OneOf(Vec<serde_json::Value>),
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub enum SemanticRole {
75 Display,
77 Input,
79 Navigation,
81 Container,
83 Progress,
85 Selection,
87 DataVisualization,
89 Decoration,
91 Action,
93 Scrollable,
95 Modal,
97 Menu,
99 Toolbar,
101 Tab,
103 TreeNode,
105 Canvas,
107 Media,
109 System,
111 Diagnostic,
113 Configuration,
115 Custom,
117}
118
119impl WidgetSchema {
120 pub fn new(
122 name: impl Into<String>,
123 description: impl Into<String>,
124 role: SemanticRole,
125 ) -> Self {
126 Self {
127 name: name.into(),
128 description: description.into(),
129 default_role: role,
130 properties: vec![],
131 actions: vec![],
132 usage_hint: None,
133 tags: vec![],
134 }
135 }
136}
137
138impl PropertySchema {
139 pub fn new(
141 name: impl Into<String>,
142 description: impl Into<String>,
143 property_type: PropertyType,
144 required: bool,
145 ) -> Self {
146 Self {
147 name: name.into(),
148 description: description.into(),
149 property_type,
150 required,
151 default_value: None,
152 constraints: vec![],
153 }
154 }
155}
156
157impl std::fmt::Display for SemanticRole {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 let s = match self {
160 Self::Display => "display",
161 Self::Input => "input",
162 Self::Navigation => "navigation",
163 Self::Container => "container",
164 Self::Progress => "progress",
165 Self::Selection => "selection",
166 Self::DataVisualization => "data-visualization",
167 Self::Decoration => "decoration",
168 Self::Action => "action",
169 Self::Scrollable => "scrollable",
170 Self::Modal => "modal",
171 Self::Menu => "menu",
172 Self::Toolbar => "toolbar",
173 Self::Tab => "tab",
174 Self::TreeNode => "tree-node",
175 Self::Canvas => "canvas",
176 Self::Media => "media",
177 Self::System => "system",
178 Self::Diagnostic => "diagnostic",
179 Self::Configuration => "configuration",
180 Self::Custom => "custom",
181 };
182 f.write_str(s)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn widget_schema_new() {
192 let ws = WidgetSchema::new("Button", "A clickable button", SemanticRole::Action);
193 assert_eq!(ws.name, "Button");
194 assert_eq!(ws.default_role, SemanticRole::Action);
195 assert!(ws.properties.is_empty());
196 assert!(ws.actions.is_empty());
197 assert!(ws.tags.is_empty());
198 }
199
200 #[test]
201 fn property_schema_new() {
202 let ps = PropertySchema::new("text", "The text content", PropertyType::String, true);
203 assert_eq!(ps.name, "text");
204 assert!(ps.required);
205 assert!(ps.default_value.is_none());
206 assert!(ps.constraints.is_empty());
207 }
208
209 #[test]
210 fn semantic_role_display() {
211 assert_eq!(format!("{}", SemanticRole::Display), "display");
212 assert_eq!(format!("{}", SemanticRole::Input), "input");
213 assert_eq!(format!("{}", SemanticRole::Action), "action");
214 assert_eq!(format!("{}", SemanticRole::Container), "container");
215 assert_eq!(
216 format!("{}", SemanticRole::DataVisualization),
217 "data-visualization"
218 );
219 assert_eq!(format!("{}", SemanticRole::Custom), "custom");
220 }
221
222 #[test]
223 fn widget_schema_serialize_roundtrip() {
224 let mut ws = WidgetSchema::new("Slider", "A range slider", SemanticRole::Input);
225 ws.properties.push(PropertySchema::new(
226 "value",
227 "Current value",
228 PropertyType::Float,
229 true,
230 ));
231 ws.tags = vec!["slider".into(), "range".into()];
232
233 let json = serde_json::to_string(&ws).unwrap();
234 let ws2: WidgetSchema = serde_json::from_str(&json).unwrap();
235 assert_eq!(ws2.name, "Slider");
236 assert_eq!(ws2.properties.len(), 1);
237 assert_eq!(ws2.tags.len(), 2);
238 }
239
240 #[test]
241 fn property_type_nested() {
242 let arr = PropertyType::Array(Box::new(PropertyType::String));
243 let json = serde_json::to_string(&arr).unwrap();
244 let arr2: PropertyType = serde_json::from_str(&json).unwrap();
245 assert_eq!(arr, arr2);
246 }
247
248 #[test]
249 fn property_constraint_serialize() {
250 let constraints = vec![
251 PropertyConstraint::Min(0.0),
252 PropertyConstraint::Max(100.0),
253 PropertyConstraint::MinLength(1),
254 PropertyConstraint::MaxLength(255),
255 PropertyConstraint::Pattern(r"^\d+$".into()),
256 ];
257 for c in constraints {
258 let json = serde_json::to_string(&c).unwrap();
259 let c2: PropertyConstraint = serde_json::from_str(&json).unwrap();
260 assert_eq!(c, c2);
261 }
262 }
263}