1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct AgentAction {
8 pub name: String,
10 pub description: String,
12 pub params: Vec<ActionParam>,
14 pub returns: Option<String>,
16 pub mutates: bool,
18 pub idempotent: bool,
20 pub shortcut: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ActionParam {
27 pub name: String,
29 pub description: String,
31 pub param_type: ActionParamType,
33 pub required: bool,
35 pub default_value: Option<serde_json::Value>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41pub enum ActionParamType {
42 String,
43 Integer,
44 Float,
45 Boolean,
46 Index,
47 Position { x: bool, y: bool },
48 Size { width: bool, height: bool },
49 Color,
50 Enum(Vec<String>),
51 Any,
52}
53
54impl AgentAction {
55 #[must_use]
57 pub fn simple(name: impl Into<String>, description: impl Into<String>, mutates: bool) -> Self {
58 Self {
59 name: name.into(),
60 description: description.into(),
61 params: vec![],
62 returns: None,
63 mutates,
64 idempotent: !mutates,
65 shortcut: None,
66 }
67 }
68
69 #[must_use]
71 pub fn with_params(
72 name: impl Into<String>,
73 description: impl Into<String>,
74 params: Vec<ActionParam>,
75 mutates: bool,
76 ) -> Self {
77 Self {
78 name: name.into(),
79 description: description.into(),
80 params,
81 returns: None,
82 mutates,
83 idempotent: !mutates,
84 shortcut: None,
85 }
86 }
87
88 pub fn validate_params(&self, params: &serde_json::Value) -> Result<(), String> {
90 for param in &self.params {
91 let val = params.get(¶m.name);
92 match val {
93 None | Some(serde_json::Value::Null) => {
94 if param.required {
95 return Err(format!("Missing required parameter '{}'", param.name));
96 }
97 }
98 Some(v) => {
99 param
100 .param_type
101 .check(v)
102 .map_err(|e| format!("Parameter '{}': {}", param.name, e))?;
103 }
104 }
105 }
106 Ok(())
107 }
108}
109
110impl ActionParamType {
111 fn check(&self, value: &serde_json::Value) -> Result<(), String> {
112 match self {
113 ActionParamType::String => {
114 if !value.is_string() {
115 return Err(format!("expected string, got {}", json_type_name(value)));
116 }
117 }
118 ActionParamType::Integer => {
119 if !value.is_i64() && !value.is_u64() {
120 return Err(format!("expected integer, got {}", json_type_name(value)));
121 }
122 }
123 ActionParamType::Float => {
124 if !value.is_number() {
125 return Err(format!("expected number, got {}", json_type_name(value)));
126 }
127 }
128 ActionParamType::Boolean => {
129 if !value.is_boolean() {
130 return Err(format!("expected boolean, got {}", json_type_name(value)));
131 }
132 }
133 ActionParamType::Index => {
134 if !value.is_u64() {
135 return Err(format!(
136 "expected index (uint), got {}",
137 json_type_name(value)
138 ));
139 }
140 }
141 ActionParamType::Position { .. } | ActionParamType::Size { .. } => {
142 if !value.is_object() {
143 return Err(format!("expected object, got {}", json_type_name(value)));
144 }
145 }
146 ActionParamType::Color => {
147 if !value.is_string() && !value.is_object() {
148 return Err(format!(
149 "expected color string or object, got {}",
150 json_type_name(value)
151 ));
152 }
153 }
154 ActionParamType::Enum(variants) => {
155 if let Some(s) = value.as_str() {
156 if !variants.iter().any(|v| v == s) {
157 return Err(format!("expected one of {:?}, got {:?}", variants, s));
158 }
159 } else {
160 return Err(format!(
161 "expected string enum, got {}",
162 json_type_name(value)
163 ));
164 }
165 }
166 ActionParamType::Any => {}
167 }
168 Ok(())
169 }
170}
171
172impl ActionParam {
173 #[must_use]
175 pub fn required(
176 name: impl Into<String>,
177 description: impl Into<String>,
178 param_type: ActionParamType,
179 ) -> Self {
180 Self {
181 name: name.into(),
182 description: description.into(),
183 param_type,
184 required: true,
185 default_value: None,
186 }
187 }
188
189 #[must_use]
191 pub fn optional(
192 name: impl Into<String>,
193 description: impl Into<String>,
194 param_type: ActionParamType,
195 default: serde_json::Value,
196 ) -> Self {
197 Self {
198 name: name.into(),
199 description: description.into(),
200 param_type,
201 required: false,
202 default_value: Some(default),
203 }
204 }
205}
206
207fn json_type_name(v: &serde_json::Value) -> &'static str {
208 match v {
209 serde_json::Value::Null => "null",
210 serde_json::Value::Bool(_) => "boolean",
211 serde_json::Value::Number(_) => "number",
212 serde_json::Value::String(_) => "string",
213 serde_json::Value::Array(_) => "array",
214 serde_json::Value::Object(_) => "object",
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn simple_action_defaults() {
224 let a = AgentAction::simple("click", "Click the button", true);
225 assert_eq!(a.name, "click");
226 assert!(a.mutates);
227 assert!(!a.idempotent);
228 assert!(a.params.is_empty());
229 assert!(a.shortcut.is_none());
230 }
231
232 #[test]
233 fn simple_action_readonly_is_idempotent() {
234 let a = AgentAction::simple("read", "Read value", false);
235 assert!(!a.mutates);
236 assert!(a.idempotent);
237 }
238
239 #[test]
240 fn validate_params_missing_required() {
241 let action = AgentAction::with_params(
242 "set_text",
243 "Set text content",
244 vec![ActionParam::required(
245 "text",
246 "The text",
247 ActionParamType::String,
248 )],
249 true,
250 );
251 let result = action.validate_params(&serde_json::json!({}));
252 assert!(result.is_err());
253 assert!(result.unwrap_err().contains("Missing required"));
254 }
255
256 #[test]
257 fn validate_params_wrong_type() {
258 let action = AgentAction::with_params(
259 "set_value",
260 "Set value",
261 vec![ActionParam::required(
262 "count",
263 "Count",
264 ActionParamType::Integer,
265 )],
266 true,
267 );
268 let result = action.validate_params(&serde_json::json!({"count": "not_a_number"}));
269 assert!(result.is_err());
270 }
271
272 #[test]
273 fn validate_params_correct() {
274 let action = AgentAction::with_params(
275 "set_value",
276 "Set value",
277 vec![ActionParam::required(
278 "count",
279 "Count",
280 ActionParamType::Integer,
281 )],
282 true,
283 );
284 assert!(
285 action
286 .validate_params(&serde_json::json!({"count": 42}))
287 .is_ok()
288 );
289 }
290
291 #[test]
292 fn validate_params_optional_missing_ok() {
293 let action = AgentAction::with_params(
294 "set",
295 "Set",
296 vec![ActionParam::optional(
297 "label",
298 "Label",
299 ActionParamType::String,
300 serde_json::json!("default"),
301 )],
302 true,
303 );
304 assert!(action.validate_params(&serde_json::json!({})).is_ok());
305 }
306
307 #[test]
308 fn validate_boolean_type() {
309 let action = AgentAction::with_params(
310 "toggle",
311 "Toggle",
312 vec![ActionParam::required(
313 "state",
314 "State",
315 ActionParamType::Boolean,
316 )],
317 true,
318 );
319 assert!(
320 action
321 .validate_params(&serde_json::json!({"state": true}))
322 .is_ok()
323 );
324 assert!(
325 action
326 .validate_params(&serde_json::json!({"state": "yes"}))
327 .is_err()
328 );
329 }
330
331 #[test]
332 fn validate_enum_type() {
333 let action = AgentAction::with_params(
334 "set_mode",
335 "Set mode",
336 vec![ActionParam::required(
337 "mode",
338 "Mode",
339 ActionParamType::Enum(vec!["light".into(), "dark".into()]),
340 )],
341 true,
342 );
343 assert!(
344 action
345 .validate_params(&serde_json::json!({"mode": "dark"}))
346 .is_ok()
347 );
348 assert!(
349 action
350 .validate_params(&serde_json::json!({"mode": "blue"}))
351 .is_err()
352 );
353 }
354
355 #[test]
356 fn validate_any_type_accepts_anything() {
357 let action = AgentAction::with_params(
358 "exec",
359 "Execute",
360 vec![ActionParam::required("data", "Data", ActionParamType::Any)],
361 true,
362 );
363 assert!(
364 action
365 .validate_params(&serde_json::json!({"data": [1,2,3]}))
366 .is_ok()
367 );
368 assert!(
369 action
370 .validate_params(&serde_json::json!({"data": "text"}))
371 .is_ok()
372 );
373 assert!(
374 action
375 .validate_params(&serde_json::json!({"data": 42}))
376 .is_ok()
377 );
378 }
379
380 #[test]
381 fn action_param_required_constructor() {
382 let p = ActionParam::required("name", "The name", ActionParamType::String);
383 assert!(p.required);
384 assert!(p.default_value.is_none());
385 }
386
387 #[test]
388 fn action_param_optional_constructor() {
389 let p = ActionParam::optional(
390 "name",
391 "The name",
392 ActionParamType::String,
393 serde_json::json!(""),
394 );
395 assert!(!p.required);
396 assert!(p.default_value.is_some());
397 }
398}