greentic_operator/demo/
commands.rs1use std::fmt;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
5pub enum DemoCommand {
6 Show,
7 Json,
8 Input { field: String, value: String },
9 Click { action_id: String },
10 Setup { provider: Option<String> },
11 Back,
12 Quit,
13 Help,
14}
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum CommandParseError {
19 Unknown(String),
20 InvalidFormat(String),
21}
22
23impl fmt::Display for CommandParseError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 CommandParseError::Unknown(command) => {
27 write!(f, "unknown command '{}'; try @show or @help", command)
28 }
29 CommandParseError::InvalidFormat(message) => write!(f, "{message}"),
30 }
31 }
32}
33
34impl std::error::Error for CommandParseError {}
35
36pub fn parse_command(line: &str) -> Result<DemoCommand, CommandParseError> {
38 let trimmed = line.trim();
39 if !trimmed.starts_with('@') {
40 return Err(CommandParseError::InvalidFormat(
41 "commands must start with '@'".to_string(),
42 ));
43 }
44 let mut parts = trimmed[1..].trim().splitn(2, char::is_whitespace);
45 let name = parts.next().unwrap_or_default().to_ascii_lowercase();
46 let rest = parts.next().unwrap_or("").trim();
47
48 match name.as_str() {
49 "show" => Ok(DemoCommand::Show),
50 "json" => Ok(DemoCommand::Json),
51 "back" => Ok(DemoCommand::Back),
52 "help" => Ok(DemoCommand::Help),
53 "quit" => Ok(DemoCommand::Quit),
54 "setup" => Ok(DemoCommand::Setup {
55 provider: if rest.is_empty() {
56 None
57 } else {
58 Some(rest.to_string())
59 },
60 }),
61 "click" => {
62 if rest.is_empty() {
63 Err(CommandParseError::InvalidFormat(
64 "@click requires an action id".to_string(),
65 ))
66 } else {
67 Ok(DemoCommand::Click {
68 action_id: rest.to_string(),
69 })
70 }
71 }
72 "input" => {
73 if rest.is_empty() {
74 return Err(CommandParseError::InvalidFormat(
75 "@input requires <field>=<value>".to_string(),
76 ));
77 }
78 let mut kv = rest.splitn(2, '=');
79 let field = kv.next().unwrap_or("").trim();
80 let value = kv.next().unwrap_or("").trim();
81 if field.is_empty() || value.is_empty() {
82 return Err(CommandParseError::InvalidFormat(
83 "@input requires a field and a value separated by '='".to_string(),
84 ));
85 }
86 Ok(DemoCommand::Input {
87 field: field.to_string(),
88 value: value.to_string(),
89 })
90 }
91 unknown => Err(CommandParseError::Unknown(unknown.to_string())),
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn parses_input_command() {
101 let command = parse_command("@input foo=bar").unwrap();
102 assert_eq!(
103 command,
104 DemoCommand::Input {
105 field: "foo".into(),
106 value: "bar".into()
107 }
108 );
109 }
110
111 #[test]
112 fn parses_click_command() {
113 let command = parse_command("@click submit").unwrap();
114 assert_eq!(
115 command,
116 DemoCommand::Click {
117 action_id: "submit".into()
118 }
119 );
120 }
121
122 #[test]
123 fn handles_unknown_command() {
124 let err = parse_command("@foo").unwrap_err();
125 assert!(matches!(err, CommandParseError::Unknown(_)));
126 }
127
128 #[test]
129 fn rejects_invalid_input_format() {
130 let err = parse_command("@input missing").unwrap_err();
131 assert!(matches!(err, CommandParseError::InvalidFormat(_)));
132 }
133
134 #[test]
135 fn parses_back_command() {
136 let command = parse_command("@back").unwrap();
137 assert_eq!(command, DemoCommand::Back);
138 }
139}