orcs_runtime/io/
parser.rs1use super::InputCommand;
9
10const MAX_INPUT_PARTS: usize = 3;
13
14#[derive(Debug, Clone, Copy, Default)]
19pub struct InputParser;
20
21impl InputParser {
22 #[must_use]
24 pub fn new() -> Self {
25 Self
26 }
27
28 #[must_use]
54 pub fn parse(&self, line: &str) -> InputCommand {
55 let line = line.trim();
56
57 if line.is_empty() {
58 return InputCommand::Empty;
59 }
60
61 if let Some(rest) = line.strip_prefix('@') {
63 let mut parts = rest.splitn(2, ' ');
64 let target = parts.next().unwrap_or("").to_lowercase();
65 if target.is_empty() {
66 return InputCommand::Unknown {
67 input: line.to_string(),
68 };
69 }
70 let message = parts.next().unwrap_or("").to_string();
71 return InputCommand::ComponentMessage { target, message };
72 }
73
74 let parts: Vec<&str> = line.splitn(MAX_INPUT_PARTS, ' ').collect();
75 let cmd = parts[0].to_lowercase();
76
77 match cmd.as_str() {
78 "y" | "yes" | "approve" => {
80 let approval_id = parts.get(1).map(|s| (*s).to_string());
81 InputCommand::Approve { approval_id }
82 }
83
84 "n" | "no" | "reject" => {
86 let approval_id = parts.get(1).map(|s| (*s).to_string());
87 let reason = parts.get(2).map(|s| (*s).to_string());
88 InputCommand::Reject {
89 approval_id,
90 reason,
91 }
92 }
93
94 "p" | "pause" => InputCommand::Pause,
96
97 "r" | "resume" => InputCommand::Resume,
99
100 "s" | "steer" => {
102 let message = if parts.len() > 1 {
103 parts[1..].join(" ")
104 } else {
105 return InputCommand::Unknown {
107 input: line.to_string(),
108 };
109 };
110 InputCommand::Steer { message }
111 }
112
113 "q" | "quit" | "exit" => InputCommand::Quit,
115
116 "veto" | "stop" | "abort" => InputCommand::Veto,
118
119 _ => InputCommand::Unknown {
121 input: line.to_string(),
122 },
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn parse_approve_simple() {
133 let cmd = InputParser.parse("y");
134 assert!(matches!(cmd, InputCommand::Approve { approval_id: None }));
135
136 let cmd = InputParser.parse("yes");
137 assert!(matches!(cmd, InputCommand::Approve { approval_id: None }));
138
139 let cmd = InputParser.parse("approve");
140 assert!(matches!(cmd, InputCommand::Approve { approval_id: None }));
141 }
142
143 #[test]
144 fn parse_approve_with_id() {
145 let cmd = InputParser.parse("y req-123");
146 assert!(matches!(
147 cmd,
148 InputCommand::Approve {
149 approval_id: Some(ref id)
150 } if id == "req-123"
151 ));
152 }
153
154 #[test]
155 fn parse_reject_simple() {
156 let cmd = InputParser.parse("n");
157 assert!(matches!(
158 cmd,
159 InputCommand::Reject {
160 approval_id: None,
161 reason: None
162 }
163 ));
164 }
165
166 #[test]
167 fn parse_reject_with_id() {
168 let cmd = InputParser.parse("n req-456");
169 assert!(matches!(
170 cmd,
171 InputCommand::Reject {
172 approval_id: Some(ref id),
173 reason: None
174 } if id == "req-456"
175 ));
176 }
177
178 #[test]
179 fn parse_reject_with_reason() {
180 let cmd = InputParser.parse("n req-789 too dangerous");
181 assert!(matches!(
182 cmd,
183 InputCommand::Reject {
184 approval_id: Some(ref id),
185 reason: Some(ref r)
186 } if id == "req-789" && r == "too dangerous"
187 ));
188 }
189
190 #[test]
191 fn parse_pause_resume() {
192 assert!(matches!(InputParser.parse("p"), InputCommand::Pause));
193 assert!(matches!(InputParser.parse("pause"), InputCommand::Pause));
194 assert!(matches!(InputParser.parse("r"), InputCommand::Resume));
195 assert!(matches!(InputParser.parse("resume"), InputCommand::Resume));
196 }
197
198 #[test]
199 fn parse_steer() {
200 let cmd = InputParser.parse("s focus on error handling");
201 assert!(matches!(
202 cmd,
203 InputCommand::Steer { ref message } if message == "focus on error handling"
204 ));
205 }
206
207 #[test]
208 fn parse_steer_empty_is_unknown() {
209 let cmd = InputParser.parse("s");
210 assert!(matches!(cmd, InputCommand::Unknown { .. }));
211
212 let cmd = InputParser.parse("steer");
213 assert!(matches!(cmd, InputCommand::Unknown { .. }));
214 }
215
216 #[test]
217 fn parse_quit() {
218 assert!(matches!(InputParser.parse("q"), InputCommand::Quit));
219 assert!(matches!(InputParser.parse("quit"), InputCommand::Quit));
220 assert!(matches!(InputParser.parse("exit"), InputCommand::Quit));
221 }
222
223 #[test]
224 fn parse_veto() {
225 assert!(matches!(InputParser.parse("veto"), InputCommand::Veto));
226 assert!(matches!(InputParser.parse("stop"), InputCommand::Veto));
227 assert!(matches!(InputParser.parse("abort"), InputCommand::Veto));
228 }
229
230 #[test]
231 fn parse_unknown() {
232 let cmd = InputParser.parse("foobar");
233 assert!(matches!(cmd, InputCommand::Unknown { .. }));
234 }
235
236 #[test]
237 fn parse_empty() {
238 let cmd = InputParser.parse("");
239 assert!(matches!(cmd, InputCommand::Empty));
240
241 let cmd = InputParser.parse(" ");
242 assert!(matches!(cmd, InputCommand::Empty));
243 }
244
245 #[test]
246 fn parse_component_message() {
247 let cmd = InputParser.parse("@shell ls -la");
248 assert!(matches!(
249 cmd,
250 InputCommand::ComponentMessage {
251 ref target,
252 ref message,
253 } if target == "shell" && message == "ls -la"
254 ));
255 }
256
257 #[test]
258 fn parse_component_message_uppercase() {
259 let cmd = InputParser.parse("@Shell echo hello");
260 assert!(matches!(
261 cmd,
262 InputCommand::ComponentMessage {
263 ref target,
264 ref message,
265 } if target == "shell" && message == "echo hello"
266 ));
267 }
268
269 #[test]
270 fn parse_component_message_no_message() {
271 let cmd = InputParser.parse("@shell");
272 assert!(matches!(
273 cmd,
274 InputCommand::ComponentMessage {
275 ref target,
276 ref message,
277 } if target == "shell" && message.is_empty()
278 ));
279 }
280
281 #[test]
282 fn parse_at_only_is_unknown() {
283 let cmd = InputParser.parse("@");
284 assert!(matches!(cmd, InputCommand::Unknown { .. }));
285 }
286
287 #[test]
288 fn case_insensitive() {
289 assert!(matches!(
290 InputParser.parse("Y"),
291 InputCommand::Approve { .. }
292 ));
293 assert!(matches!(
294 InputParser.parse("YES"),
295 InputCommand::Approve { .. }
296 ));
297 assert!(matches!(
298 InputParser.parse("N"),
299 InputCommand::Reject { .. }
300 ));
301 assert!(matches!(InputParser.parse("QUIT"), InputCommand::Quit));
302 }
303}