diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 3cb74aa..9134397 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -2,6 +2,49 @@ use serde_json::{json, Value}; use crate::flags::Flags; +/// Error type for command parsing with contextual information +#[derive(Debug)] +pub enum ParseError { + /// Command does not exist + UnknownCommand { command: String }, + /// Command exists but subcommand is invalid + UnknownSubcommand { + subcommand: String, + valid_options: &'static [&'static str], + }, + /// Command/subcommand exists but required arguments are missing + MissingArguments { + context: String, + usage: &'static str, + }, +} + +impl ParseError { + pub fn format(&self) -> String { + match self { + ParseError::UnknownCommand { command } => { + format!("Unknown command: {}", command) + } + ParseError::UnknownSubcommand { + subcommand, + valid_options, + } => { + format!( + "Unknown subcommand: {}\nValid options: {}", + subcommand, + valid_options.join(", ") + ) + } + ParseError::MissingArguments { context, usage } => { + format!( + "Missing arguments for: {}\nUsage: agent-browser {}", + context, usage + ) + } + } + } +} + pub fn gen_id() -> String { format!( "r{}", @@ -13,9 +56,12 @@ pub fn gen_id() -> String { ) } -pub fn parse_command(args: &[String], flags: &Flags) -> Option { +pub fn parse_command(args: &[String], flags: &Flags) -> Result { if args.is_empty() { - return None; + return Err(ParseError::MissingArguments { + context: "".to_string(), + usage: " [args...]", + }); } let cmd = args[0].as_str(); @@ -25,64 +71,172 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Option { match cmd { // === Navigation === "open" | "goto" | "navigate" => { - let url = rest.get(0)?; + let url = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: cmd.to_string(), + usage: "open ", + })?; let url = if url.starts_with("http") { url.to_string() } else { format!("https://{}", url) }; - Some(json!({ "id": id, "action": "navigate", "url": url })) + Ok(json!({ "id": id, "action": "navigate", "url": url })) } - "back" => Some(json!({ "id": id, "action": "back" })), - "forward" => Some(json!({ "id": id, "action": "forward" })), - "reload" => Some(json!({ "id": id, "action": "reload" })), + "back" => Ok(json!({ "id": id, "action": "back" })), + "forward" => Ok(json!({ "id": id, "action": "forward" })), + "reload" => Ok(json!({ "id": id, "action": "reload" })), // === Core Actions === - "click" => Some(json!({ "id": id, "action": "click", "selector": rest.get(0)? })), - "dblclick" => Some(json!({ "id": id, "action": "dblclick", "selector": rest.get(0)? })), - "fill" => Some(json!({ "id": id, "action": "fill", "selector": rest.get(0)?, "value": rest[1..].join(" ") })), - "type" => Some(json!({ "id": id, "action": "type", "selector": rest.get(0)?, "text": rest[1..].join(" ") })), - "hover" => Some(json!({ "id": id, "action": "hover", "selector": rest.get(0)? })), - "focus" => Some(json!({ "id": id, "action": "focus", "selector": rest.get(0)? })), - "check" => Some(json!({ "id": id, "action": "check", "selector": rest.get(0)? })), - "uncheck" => Some(json!({ "id": id, "action": "uncheck", "selector": rest.get(0)? })), - "select" => Some(json!({ "id": id, "action": "select", "selector": rest.get(0)?, "value": rest.get(1)? })), - "drag" => Some(json!({ "id": id, "action": "drag", "source": rest.get(0)?, "target": rest.get(1)? })), - "upload" => Some(json!({ "id": id, "action": "upload", "selector": rest.get(0)?, "files": &rest[1..] })), + "click" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "click".to_string(), + usage: "click ", + })?; + Ok(json!({ "id": id, "action": "click", "selector": sel })) + } + "dblclick" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "dblclick".to_string(), + usage: "dblclick ", + })?; + Ok(json!({ "id": id, "action": "dblclick", "selector": sel })) + } + "fill" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "fill".to_string(), + usage: "fill ", + })?; + Ok(json!({ "id": id, "action": "fill", "selector": sel, "value": rest[1..].join(" ") })) + } + "type" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "type".to_string(), + usage: "type ", + })?; + Ok(json!({ "id": id, "action": "type", "selector": sel, "text": rest[1..].join(" ") })) + } + "hover" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "hover".to_string(), + usage: "hover ", + })?; + Ok(json!({ "id": id, "action": "hover", "selector": sel })) + } + "focus" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "focus".to_string(), + usage: "focus ", + })?; + Ok(json!({ "id": id, "action": "focus", "selector": sel })) + } + "check" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "check".to_string(), + usage: "check ", + })?; + Ok(json!({ "id": id, "action": "check", "selector": sel })) + } + "uncheck" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "uncheck".to_string(), + usage: "uncheck ", + })?; + Ok(json!({ "id": id, "action": "uncheck", "selector": sel })) + } + "select" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "select".to_string(), + usage: "select ", + })?; + let val = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "select".to_string(), + usage: "select ", + })?; + Ok(json!({ "id": id, "action": "select", "selector": sel, "value": val })) + } + "drag" => { + let src = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "drag".to_string(), + usage: "drag ", + })?; + let tgt = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "drag".to_string(), + usage: "drag ", + })?; + Ok(json!({ "id": id, "action": "drag", "source": src, "target": tgt })) + } + "upload" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "upload".to_string(), + usage: "upload ", + })?; + Ok(json!({ "id": id, "action": "upload", "selector": sel, "files": &rest[1..] })) + } // === Keyboard === - "press" | "key" => Some(json!({ "id": id, "action": "press", "key": rest.get(0)? })), - "keydown" => Some(json!({ "id": id, "action": "keydown", "key": rest.get(0)? })), - "keyup" => Some(json!({ "id": id, "action": "keyup", "key": rest.get(0)? })), + "press" | "key" => { + let key = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "press".to_string(), + usage: "press ", + })?; + Ok(json!({ "id": id, "action": "press", "key": key })) + } + "keydown" => { + let key = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "keydown".to_string(), + usage: "keydown ", + })?; + Ok(json!({ "id": id, "action": "keydown", "key": key })) + } + "keyup" => { + let key = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "keyup".to_string(), + usage: "keyup ", + })?; + Ok(json!({ "id": id, "action": "keyup", "key": key })) + } // === Scroll === "scroll" => { let dir = rest.get(0).unwrap_or(&"down"); let amount = rest.get(1).and_then(|s| s.parse::().ok()).unwrap_or(300); - Some(json!({ "id": id, "action": "scroll", "direction": dir, "amount": amount })) + Ok(json!({ "id": id, "action": "scroll", "direction": dir, "amount": amount })) } "scrollintoview" | "scrollinto" => { - Some(json!({ "id": id, "action": "scrollintoview", "selector": rest.get(0)? })) + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "scrollintoview".to_string(), + usage: "scrollintoview ", + })?; + Ok(json!({ "id": id, "action": "scrollintoview", "selector": sel })) } // === Wait === "wait" => { if let Some(arg) = rest.get(0) { if arg.parse::().is_ok() { - Some(json!({ "id": id, "action": "wait", "timeout": arg.parse::().unwrap() })) + Ok(json!({ "id": id, "action": "wait", "timeout": arg.parse::().unwrap() })) } else { - Some(json!({ "id": id, "action": "wait", "selector": arg })) + Ok(json!({ "id": id, "action": "wait", "selector": arg })) } } else { - None + Err(ParseError::MissingArguments { + context: "wait".to_string(), + usage: "wait ", + }) } } // === Screenshot/PDF === "screenshot" => { - Some(json!({ "id": id, "action": "screenshot", "path": rest.get(0), "fullPage": flags.full })) + Ok(json!({ "id": id, "action": "screenshot", "path": rest.get(0), "fullPage": flags.full })) + } + "pdf" => { + let path = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "pdf".to_string(), + usage: "pdf ", + })?; + Ok(json!({ "id": id, "action": "pdf", "path": path })) } - "pdf" => Some(json!({ "id": id, "action": "pdf", "path": rest.get(0)? })), // === Snapshot === "snapshot" => { @@ -115,244 +269,477 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Option { } i += 1; } - Some(cmd) + Ok(cmd) } // === Eval === - "eval" => Some(json!({ "id": id, "action": "evaluate", "script": rest.join(" ") })), + "eval" => Ok(json!({ "id": id, "action": "evaluate", "script": rest.join(" ") })), // === Close === - "close" | "quit" | "exit" => Some(json!({ "id": id, "action": "close" })), + "close" | "quit" | "exit" => Ok(json!({ "id": id, "action": "close" })), // === Get === - "get" => match rest.get(0).map(|s| *s) { - Some("text") => Some(json!({ "id": id, "action": "gettext", "selector": rest.get(1)? })), - Some("html") => Some(json!({ "id": id, "action": "innerhtml", "selector": rest.get(1)? })), - Some("value") => Some(json!({ "id": id, "action": "inputvalue", "selector": rest.get(1)? })), - Some("attr") => Some(json!({ "id": id, "action": "getattribute", "selector": rest.get(1)?, "attribute": rest.get(2)? })), - Some("url") => Some(json!({ "id": id, "action": "url" })), - Some("title") => Some(json!({ "id": id, "action": "title" })), - Some("count") => Some(json!({ "id": id, "action": "count", "selector": rest.get(1)? })), - Some("box") => Some(json!({ "id": id, "action": "boundingbox", "selector": rest.get(1)? })), - _ => None, - }, + "get" => parse_get(&rest, &id), // === Is (state checks) === - "is" => match rest.get(0).map(|s| *s) { - Some("visible") => Some(json!({ "id": id, "action": "isvisible", "selector": rest.get(1)? })), - Some("enabled") => Some(json!({ "id": id, "action": "isenabled", "selector": rest.get(1)? })), - Some("checked") => Some(json!({ "id": id, "action": "ischecked", "selector": rest.get(1)? })), - _ => None, - }, + "is" => parse_is(&rest, &id), // === Find (locators) === "find" => parse_find(&rest, &id), // === Mouse === - "mouse" => match rest.get(0).map(|s| *s) { - Some("move") => { - let x = rest.get(1)?.parse::().ok()?; - let y = rest.get(2)?.parse::().ok()?; - Some(json!({ "id": id, "action": "mousemove", "x": x, "y": y })) - } - Some("down") => { - Some(json!({ "id": id, "action": "mousedown", "button": rest.get(1).unwrap_or(&"left") })) - } - Some("up") => { - Some(json!({ "id": id, "action": "mouseup", "button": rest.get(1).unwrap_or(&"left") })) - } - Some("wheel") => { - let dy = rest.get(1).and_then(|s| s.parse::().ok()).unwrap_or(100); - let dx = rest.get(2).and_then(|s| s.parse::().ok()).unwrap_or(0); - Some(json!({ "id": id, "action": "mousewheel", "deltaX": dx, "deltaY": dy })) - } - _ => None, - }, + "mouse" => parse_mouse(&rest, &id), // === Set (browser settings) === "set" => parse_set(&rest, &id), // === Network === - "network" => match rest.get(0).map(|s| *s) { - Some("route") => { - let url = rest.get(1)?; - let abort = rest.iter().any(|&s| s == "--abort"); - let body_idx = rest.iter().position(|&s| s == "--body"); - let body = body_idx.and_then(|i| rest.get(i + 1).map(|s| *s)); - Some(json!({ "id": id, "action": "route", "url": url, "abort": abort, "body": body })) - } - Some("unroute") => Some(json!({ "id": id, "action": "unroute", "url": rest.get(1) })), - Some("requests") => { - let clear = rest.iter().any(|&s| s == "--clear"); - let filter_idx = rest.iter().position(|&s| s == "--filter"); - let filter = filter_idx.and_then(|i| rest.get(i + 1).map(|s| *s)); - Some(json!({ "id": id, "action": "requests", "clear": clear, "filter": filter })) - } - _ => None, - }, + "network" => parse_network(&rest, &id), // === Storage === - "storage" => match rest.get(0).map(|s| *s) { - Some("local") | Some("session") => { - let storage_type = rest.get(0)?; - let op = rest.get(1).unwrap_or(&"get"); - let key = rest.get(2); - let value = rest.get(3); - match *op { - "set" => Some(json!({ "id": id, "action": "storage_set", "type": storage_type, "key": key?, "value": value? })), - "clear" => Some(json!({ "id": id, "action": "storage_clear", "type": storage_type })), - _ => { - let mut cmd = json!({ "id": id, "action": "storage_get", "type": storage_type }); - if let Some(k) = key { - cmd.as_object_mut().unwrap().insert("key".to_string(), json!(k)); - } - Some(cmd) - } - } - } - _ => None, - }, + "storage" => parse_storage(&rest, &id), // === Cookies === "cookies" => { let op = rest.get(0).unwrap_or(&"get"); match *op { "set" => { - let name = rest.get(1)?; - let value = rest.get(2)?; - Some(json!({ "id": id, "action": "cookies_set", "cookies": [{ "name": name, "value": value }] })) + let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "cookies set".to_string(), + usage: "cookies set ", + })?; + let value = rest.get(2).ok_or_else(|| ParseError::MissingArguments { + context: "cookies set".to_string(), + usage: "cookies set ", + })?; + Ok(json!({ "id": id, "action": "cookies_set", "cookies": [{ "name": name, "value": value }] })) } - "clear" => Some(json!({ "id": id, "action": "cookies_clear" })), - _ => Some(json!({ "id": id, "action": "cookies_get" })), + "clear" => Ok(json!({ "id": id, "action": "cookies_clear" })), + _ => Ok(json!({ "id": id, "action": "cookies_get" })), } } // === Tabs === - "tab" => match rest.get(0).map(|s| *s) { - Some("new") => Some(json!({ "id": id, "action": "tab_new", "url": rest.get(1) })), - Some("list") => Some(json!({ "id": id, "action": "tab_list" })), - Some("close") => { - Some(json!({ "id": id, "action": "tab_close", "index": rest.get(1).and_then(|s| s.parse::().ok()) })) + "tab" => { + match rest.get(0).map(|s| *s) { + Some("new") => Ok(json!({ "id": id, "action": "tab_new", "url": rest.get(1) })), + Some("list") => Ok(json!({ "id": id, "action": "tab_list" })), + Some("close") => { + Ok(json!({ "id": id, "action": "tab_close", "index": rest.get(1).and_then(|s| s.parse::().ok()) })) + } + Some(n) if n.parse::().is_ok() => { + Ok(json!({ "id": id, "action": "tab_switch", "index": n.parse::().unwrap() })) + } + _ => Ok(json!({ "id": id, "action": "tab_list" })), } - Some(n) if n.parse::().is_ok() => { - Some(json!({ "id": id, "action": "tab_switch", "index": n.parse::().unwrap() })) - } - _ => Some(json!({ "id": id, "action": "tab_list" })), - }, + } // === Window === - "window" => match rest.get(0).map(|s| *s) { - Some("new") => Some(json!({ "id": id, "action": "window_new" })), - _ => None, - }, + "window" => { + const VALID: &[&str] = &["new"]; + match rest.get(0).map(|s| *s) { + Some("new") => Ok(json!({ "id": id, "action": "window_new" })), + Some(sub) => Err(ParseError::UnknownSubcommand { + subcommand: sub.to_string(), + valid_options: VALID, + }), + None => Err(ParseError::MissingArguments { + context: "window".to_string(), + usage: "window ", + }), + } + } // === Frame === "frame" => { if rest.get(0).map(|s| *s) == Some("main") { - Some(json!({ "id": id, "action": "frame_main" })) + Ok(json!({ "id": id, "action": "frame_main" })) } else { - Some(json!({ "id": id, "action": "frame", "selector": rest.get(0)? })) + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "frame".to_string(), + usage: "frame ", + })?; + Ok(json!({ "id": id, "action": "frame", "selector": sel })) } } // === Dialog === - "dialog" => match rest.get(0).map(|s| *s) { - Some("accept") => { - Some(json!({ "id": id, "action": "dialog", "response": "accept", "promptText": rest.get(1) })) + "dialog" => { + const VALID: &[&str] = &["accept", "dismiss"]; + match rest.get(0).map(|s| *s) { + Some("accept") => { + Ok(json!({ "id": id, "action": "dialog", "response": "accept", "promptText": rest.get(1) })) + } + Some("dismiss") => Ok(json!({ "id": id, "action": "dialog", "response": "dismiss" })), + Some(sub) => Err(ParseError::UnknownSubcommand { + subcommand: sub.to_string(), + valid_options: VALID, + }), + None => Err(ParseError::MissingArguments { + context: "dialog".to_string(), + usage: "dialog [text]", + }), } - Some("dismiss") => Some(json!({ "id": id, "action": "dialog", "response": "dismiss" })), - _ => None, - }, + } // === Debug === - "trace" => match rest.get(0).map(|s| *s) { - Some("start") => Some(json!({ "id": id, "action": "trace_start", "path": rest.get(1) })), - Some("stop") => Some(json!({ "id": id, "action": "trace_stop", "path": rest.get(1) })), - _ => None, - }, + "trace" => { + const VALID: &[&str] = &["start", "stop"]; + match rest.get(0).map(|s| *s) { + Some("start") => Ok(json!({ "id": id, "action": "trace_start", "path": rest.get(1) })), + Some("stop") => Ok(json!({ "id": id, "action": "trace_stop", "path": rest.get(1) })), + Some(sub) => Err(ParseError::UnknownSubcommand { + subcommand: sub.to_string(), + valid_options: VALID, + }), + None => Err(ParseError::MissingArguments { + context: "trace".to_string(), + usage: "trace [path]", + }), + } + } "console" => { let clear = rest.iter().any(|&s| s == "--clear"); - Some(json!({ "id": id, "action": "console", "clear": clear })) + Ok(json!({ "id": id, "action": "console", "clear": clear })) } "errors" => { let clear = rest.iter().any(|&s| s == "--clear"); - Some(json!({ "id": id, "action": "errors", "clear": clear })) + Ok(json!({ "id": id, "action": "errors", "clear": clear })) + } + "highlight" => { + let sel = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "highlight".to_string(), + usage: "highlight ", + })?; + Ok(json!({ "id": id, "action": "highlight", "selector": sel })) } - "highlight" => Some(json!({ "id": id, "action": "highlight", "selector": rest.get(0)? })), // === State === - "state" => match rest.get(0).map(|s| *s) { - Some("save") => Some(json!({ "id": id, "action": "state_save", "path": rest.get(1)? })), - Some("load") => Some(json!({ "id": id, "action": "state_load", "path": rest.get(1)? })), - _ => None, - }, + "state" => { + const VALID: &[&str] = &["save", "load"]; + match rest.get(0).map(|s| *s) { + Some("save") => { + let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "state save".to_string(), + usage: "state save ", + })?; + Ok(json!({ "id": id, "action": "state_save", "path": path })) + } + Some("load") => { + let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "state load".to_string(), + usage: "state load ", + })?; + Ok(json!({ "id": id, "action": "state_load", "path": path })) + } + Some(sub) => Err(ParseError::UnknownSubcommand { + subcommand: sub.to_string(), + valid_options: VALID, + }), + None => Err(ParseError::MissingArguments { + context: "state".to_string(), + usage: "state ", + }), + } + } - _ => None, + _ => Err(ParseError::UnknownCommand { + command: cmd.to_string(), + }), } } -fn parse_find(rest: &[&str], id: &str) -> Option { - let locator = rest.get(0)?; - let value = rest.get(1)?; - let subaction = rest.get(2).unwrap_or(&"click"); - let fill_value = if rest.len() > 3 { - Some(rest[3..].join(" ")) - } else { - None - }; +fn parse_get(rest: &[&str], id: &str) -> Result { + const VALID: &[&str] = &["text", "html", "value", "attr", "url", "title", "count", "box"]; + + match rest.get(0).map(|s| *s) { + Some("text") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "get text".to_string(), + usage: "get text ", + })?; + Ok(json!({ "id": id, "action": "gettext", "selector": sel })) + } + Some("html") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "get html".to_string(), + usage: "get html ", + })?; + Ok(json!({ "id": id, "action": "innerhtml", "selector": sel })) + } + Some("value") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "get value".to_string(), + usage: "get value ", + })?; + Ok(json!({ "id": id, "action": "inputvalue", "selector": sel })) + } + Some("attr") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "get attr".to_string(), + usage: "get attr ", + })?; + let attr = rest.get(2).ok_or_else(|| ParseError::MissingArguments { + context: "get attr".to_string(), + usage: "get attr ", + })?; + Ok(json!({ "id": id, "action": "getattribute", "selector": sel, "attribute": attr })) + } + Some("url") => Ok(json!({ "id": id, "action": "url" })), + Some("title") => Ok(json!({ "id": id, "action": "title" })), + Some("count") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "get count".to_string(), + usage: "get count ", + })?; + Ok(json!({ "id": id, "action": "count", "selector": sel })) + } + Some("box") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "get box".to_string(), + usage: "get box ", + })?; + Ok(json!({ "id": id, "action": "boundingbox", "selector": sel })) + } + Some(sub) => Err(ParseError::UnknownSubcommand { + subcommand: sub.to_string(), + valid_options: VALID, + }), + None => Err(ParseError::MissingArguments { + context: "get".to_string(), + usage: "get [args...]", + }), + } +} +fn parse_is(rest: &[&str], id: &str) -> Result { + const VALID: &[&str] = &["visible", "enabled", "checked"]; + + match rest.get(0).map(|s| *s) { + Some("visible") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "is visible".to_string(), + usage: "is visible ", + })?; + Ok(json!({ "id": id, "action": "isvisible", "selector": sel })) + } + Some("enabled") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "is enabled".to_string(), + usage: "is enabled ", + })?; + Ok(json!({ "id": id, "action": "isenabled", "selector": sel })) + } + Some("checked") => { + let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: "is checked".to_string(), + usage: "is checked ", + })?; + Ok(json!({ "id": id, "action": "ischecked", "selector": sel })) + } + Some(sub) => Err(ParseError::UnknownSubcommand { + subcommand: sub.to_string(), + valid_options: VALID, + }), + None => Err(ParseError::MissingArguments { + context: "is".to_string(), + usage: "is ", + }), + } +} + +fn parse_find(rest: &[&str], id: &str) -> Result { + const VALID: &[&str] = &["role", "text", "label", "placeholder", "alt", "title", "testid", "first", "last", "nth"]; + + let locator = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + context: "find".to_string(), + usage: "find [action] [text]", + })?; + let name_idx = rest.iter().position(|&s| s == "--name"); let name = name_idx.and_then(|i| rest.get(i + 1).map(|s| *s)); let exact = rest.iter().any(|&s| s == "--exact"); match *locator { - "role" => Some(json!({ "id": id, "action": "getbyrole", "role": value, "subaction": subaction, "value": fill_value, "name": name, "exact": exact })), - "text" => Some(json!({ "id": id, "action": "getbytext", "text": value, "subaction": subaction, "exact": exact })), - "label" => Some(json!({ "id": id, "action": "getbylabel", "label": value, "subaction": subaction, "value": fill_value, "exact": exact })), - "placeholder" => Some(json!({ "id": id, "action": "getbyplaceholder", "placeholder": value, "subaction": subaction, "value": fill_value, "exact": exact })), - "alt" => Some(json!({ "id": id, "action": "getbyalttext", "text": value, "subaction": subaction, "exact": exact })), - "title" => Some(json!({ "id": id, "action": "getbytitle", "text": value, "subaction": subaction, "exact": exact })), - "testid" => Some(json!({ "id": id, "action": "getbytestid", "testId": value, "subaction": subaction, "value": fill_value })), - "first" => Some(json!({ "id": id, "action": "nth", "selector": value, "index": 0, "subaction": subaction, "value": fill_value })), - "last" => Some(json!({ "id": id, "action": "nth", "selector": value, "index": -1, "subaction": subaction, "value": fill_value })), + "role" | "text" | "label" | "placeholder" | "alt" | "title" | "testid" | "first" | "last" => { + let value = rest.get(1).ok_or_else(|| ParseError::MissingArguments { + context: format!("find {}", locator), + usage: match *locator { + "role" => "find role [action] [--name ] [--exact]", + "text" => "find text [action] [--exact]", + "label" => "find label