1use serde_json::{Value, json};
11
12fn is_ref(s: &str) -> bool {
14 s.strip_prefix('e').is_some_and(|d| !d.is_empty() && d.chars().all(|c| c.is_ascii_digit()))
15}
16
17const ROLES: &[&str] =
19 &["button", "link", "textbox", "checkbox", "radio", "label", "menuitem", "tab", "slider", "combobox", "listitem", "heading", "image", "window"];
20
21pub fn parse(s: &str) -> Result<Value, String> {
23 let s = s.trim();
24 if let Some(r) = s.strip_prefix('@') {
25 return if r.is_empty() { Err("`@` needs a ref, like @e3".into()) } else { Ok(json!({"ref": r})) };
26 }
27 if is_ref(s) {
28 return Ok(json!({"ref": s}));
29 }
30 if let Some(id) = s.strip_prefix("id=").or_else(|| s.strip_prefix('#')).filter(|i| !i.is_empty()) {
31 return Ok(json!({"id": id}));
32 }
33 if let Some(t) = s.strip_prefix("text=") {
34 return Ok(json!({"text": t}));
35 }
36 if let Some((role, text)) = s.split_once('=') {
37 let role = role.strip_prefix("role:").unwrap_or(role);
38 if ROLES.contains(&role) || s.starts_with("role:") {
39 return Ok(json!({"text": text, "role": role}));
40 }
41 }
42 if let Some((x, y)) = s.split_once(',')
43 && let (Ok(x), Ok(y)) = (x.trim().parse::<f64>(), y.trim().parse::<f64>())
44 && x.is_finite()
45 && y.is_finite()
46 {
47 return Ok(json!({"x": x, "y": y}));
48 }
49 Err(format!("`{s}` is not a target; write a ref (@e3), id=<automation id>, text=<label>, <role>=<label> or x,y"))
50}
51
52pub fn show(t: &Value) -> String {
54 let s = |k: &str| t.get(k).and_then(Value::as_str);
55 if let Some(r) = s("ref") {
56 return format!("@{r}");
57 }
58 if let Some(i) = s("id") {
59 return format!("id={i}");
60 }
61 if let Some(x) = s("text") {
62 return match s("role") {
63 Some(r) => format!("{r}={x}"),
64 None => format!("text={x}"),
65 };
66 }
67 match (t.get("x").and_then(Value::as_f64), t.get("y").and_then(Value::as_f64)) {
68 (Some(x), Some(y)) => format!("{x},{y}"),
69 _ => t.to_string(),
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn every_form() {
79 assert_eq!(parse("@e3").unwrap(), json!({"ref": "e3"}));
80 assert_eq!(parse("e12").unwrap(), json!({"ref": "e12"}));
81 assert_eq!(parse("@custom").unwrap(), json!({"ref": "custom"}));
82 assert_eq!(parse("id=file.save").unwrap(), json!({"id": "file.save"}));
83 assert_eq!(parse("#file.save").unwrap(), json!({"id": "file.save"}));
84 assert_eq!(parse("text=Save as…").unwrap(), json!({"text": "Save as…"}));
85 assert_eq!(parse("button=Save").unwrap(), json!({"text": "Save", "role": "button"}));
86 assert_eq!(parse("role:toolbar=Main").unwrap(), json!({"text": "Main", "role": "toolbar"}));
87 assert_eq!(parse("120,40.5").unwrap(), json!({"x": 120.0, "y": 40.5}));
88 for bad in ["", "@", "e", "Save", "nan,1", "inf,2", "a=b", "#"] {
89 assert!(parse(bad).is_err(), "{bad}");
90 }
91 }
92
93 #[test]
94 fn show_inverts_parse() {
95 for s in ["@e3", "id=file.save", "text=Save", "button=Save", "1,2"] {
96 assert_eq!(show(&parse(s).unwrap()), s);
97 }
98 }
99}