Skip to main content

greentic_operator/demo/
commands.rs

1use std::fmt;
2
3/// Commands that are recognized inside the interactive demo REPL.
4#[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/// Error returned when the input line cannot be parsed.
17#[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
36/// Parse the trimmed input line into a REPL command.
37pub 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}