rusty_cmd/
cmd.rs

1use std::collections::HashMap;
2use std::io;
3
4use crate::command_handler::{CommandHandler, CommandResult};
5
6/// Command interpreter implemented as struct that contains
7/// boxed CommandHandlers in a hashmap with Strings as the keys
8pub struct Cmd<R, W>
9where
10    W: io::Write + 'static,
11    R: io::BufRead + 'static,
12{
13    handles: HashMap<String, Box<dyn CommandHandler<W>>>,
14    stdin: R,
15    stdout: W,
16}
17
18impl<R, W> Cmd<R, W>
19where
20    W: io::Write + 'static,
21    R: io::BufRead + 'static,
22{
23    /// Create new Cmd instance
24    pub fn new(reader: R, writer: W) -> Cmd<R, W>
25    where
26        W: io::Write,
27        R: io::Read,
28    {
29        Cmd {
30            handles: HashMap::new(),
31            stdin: reader,
32            stdout: writer,
33        }
34    }
35
36    /// Start the command interpreter
37    ///
38    /// Handlers with return code 0 will break the loop
39    pub fn run(&mut self) -> Result<(), io::Error> {
40        loop {
41            // print promt at every iteration and flush stdout to ensure user
42            // can type on same line as promt
43            write!(self.stdout, "(cmd) ")?;
44            self.stdout.flush()?;
45
46            // get user input from stdin
47            let mut inputs = String::new();
48            self.stdin.read_line(&mut inputs)?;
49            let inputs = inputs.trim();
50
51            // separate user input into a command and optional args
52            if !inputs.is_empty() {
53                let (command, args) = parse_cmd(inputs);
54                let args = split_args(args);
55
56                // attempt to execute command
57                if let Some(handler) = self.handles.get(command) {
58                    if matches!(
59                        handler.execute(&mut self.stdout, &args),
60                        CommandResult::Break
61                    ) {
62                        break;
63                    }
64                } else {
65                    writeln!(self.stdout, "No command {}", command)?;
66                }
67            }
68        }
69        Ok(())
70    }
71
72    /// Insert new handler into the Cmd handles HashMap defined by a function or closure
73    ///
74    /// ## Note: Will not overwrite existing handler names
75    pub fn add_cmd_fn(
76        &mut self,
77        name: String,
78        handler: impl Fn(&mut W, &[&str]) -> CommandResult + 'static,
79    ) -> Result<(), io::Error> {
80        self.add_cmd(name, handler)
81    }
82
83    /// Insert new handler into the Cmd handles HashMap
84    ///
85    /// ## Note: Will not overwrite existing handler names
86    pub fn add_cmd(
87        &mut self,
88        name: String,
89        handler: impl CommandHandler<W> + 'static,
90    ) -> Result<(), io::Error> {
91        match self.handles.get(&name) {
92            Some(_) => write!(
93                self.stdout,
94                "Warning: Command with handle {} already exists.",
95                name
96            )?,
97            None => {
98                self.handles.insert(name, Box::new(handler));
99            }
100        }
101
102        Ok(())
103    }
104
105    #[cfg(test)]
106    fn get_cmd(&self, key: String) -> Option<&Box<dyn CommandHandler<W>>> {
107        self.handles.get(&key)
108    }
109}
110
111// Parse command string into command, and args Strings
112fn parse_cmd(line: &str) -> (&str, &str) {
113    let line = line.trim();
114    let first_space = line.find(' ').unwrap_or(line.len());
115    let command = &line[..first_space];
116
117    let args = line[command.len()..].trim();
118    (command, args)
119}
120
121fn split_args(args: &str) -> Vec<&str> {
122    args.split_whitespace().map(|arg| arg.trim()).collect()
123}
124
125#[cfg(test)]
126mod tests {
127    use std::io::BufRead;
128    use std::io::{self, BufReader, Write};
129
130    use super::*;
131    use crate::command_handler::CommandResult;
132    use crate::handlers::Quit;
133
134    #[derive(Default)]
135    pub struct Greeting {}
136
137    impl<W: io::Write> CommandHandler<W> for Greeting {
138        fn execute(&self, stdout: &mut W, _args: &[&str]) -> CommandResult {
139            write!(stdout, "Hello there!").unwrap();
140            CommandResult::Continue
141        }
142    }
143
144    // Mock object for stdin that always errs on stdin.read()
145    struct StdinAlwaysErr;
146
147    impl io::Read for StdinAlwaysErr {
148        fn read(&mut self, _: &mut [u8]) -> Result<usize, std::io::Error> {
149            Err(io::Error::new(io::ErrorKind::Other, "failed on read"))
150        }
151    }
152
153    // Mock object for stdout that always errs on stdout.write()
154    struct StdoutWriteErr;
155
156    impl io::Write for StdoutWriteErr {
157        fn write(&mut self, _: &[u8]) -> Result<usize, std::io::Error> {
158            Err(io::Error::new(io::ErrorKind::Other, "failed on write"))
159        }
160        fn flush(&mut self) -> Result<(), std::io::Error> {
161            Ok(())
162        }
163    }
164    // Mock object for stdout that always errs on stdout.flush()
165    struct StdoutFlushErr;
166
167    impl io::Write for StdoutFlushErr {
168        fn write(&mut self, _: &[u8]) -> Result<usize, std::io::Error> {
169            Ok(1)
170        }
171        fn flush(&mut self) -> Result<(), std::io::Error> {
172            Err(io::Error::new(io::ErrorKind::Other, "failed on flush"))
173        }
174    }
175
176    fn setup() -> Cmd<io::BufReader<std::fs::File>, Vec<u8>> {
177        let f = std::fs::File::open("test_files/test_in.txt").unwrap();
178        let stdin = io::BufReader::new(f);
179
180        let stdout: Vec<u8> = Vec::new();
181        let mut app: Cmd<io::BufReader<std::fs::File>, Vec<u8>> = Cmd::new(stdin, stdout);
182        let greet_handler = Greeting::default();
183
184        // Add the trait object to the HashMap
185        app.add_cmd(String::from("greet"), greet_handler).unwrap();
186        app.add_cmd(String::from("quit"), Quit::default()).unwrap();
187        app
188    }
189
190    #[test]
191    fn test_add_cmd() {
192        let app = setup();
193        let mut stdout = vec![];
194
195        // Verify that the key-value pair exists in the HashMap
196        let h = app.get_cmd(String::from("greet"));
197        assert!(h.is_some());
198
199        // Verify right handler was added to hashmap
200        h.unwrap().execute(&mut stdout, &[]);
201        assert_eq!(String::from_utf8(stdout).unwrap(), "Hello there!");
202    }
203
204    #[test]
205    fn test_add_existing_cmd() {
206        let mut app = setup();
207
208        // Verify message is printed out when a handle with existing name is added
209        app.add_cmd("greet".to_string(), Greeting {}).unwrap();
210
211        let mut std_out_lines = app.stdout.lines();
212        let line1 = std_out_lines.next().unwrap().unwrap();
213
214        assert_eq!(line1, "Warning: Command with handle greet already exists.");
215    }
216
217    #[test]
218    fn test_add_cmd_always_error() {
219        let f = std::fs::File::open("test_files/test_in.txt").unwrap();
220        let stdin = io::BufReader::new(f);
221        let stdout = StdoutWriteErr;
222        let mut app = Cmd::new(stdin, stdout);
223
224        // add same command twice, which will cause the self.stdout.write() path to output error
225        let _ok = app.add_cmd("greet".to_string(), Greeting {}).unwrap();
226        let e = app.add_cmd("greet".to_string(), Greeting {}).unwrap_err();
227
228        assert_eq!(e.to_string(), "failed on write");
229        assert_eq!(e.kind(), io::ErrorKind::Other);
230    }
231
232    #[test]
233    fn test_parse_cmd() {
234        let line = "command arg1 arg2";
235        assert_eq!(parse_cmd(line), ("command", "arg1 arg2"))
236    }
237    #[test]
238
239    fn test_parse_cmd_empty_line() {
240        assert_eq!(parse_cmd(""), ("", ""));
241        assert_eq!(parse_cmd("    "), ("", ""));
242    }
243
244    #[test]
245    fn test_parse_cmd_remove_extra_spaces() {
246        let line = "     command arg1 arg2";
247        assert_eq!(parse_cmd(line), ("command", "arg1 arg2"))
248    }
249
250    #[test]
251    fn test_parse_cmd_empty_args() {
252        let line = "command";
253        assert_eq!(parse_cmd(line), ("command", ""));
254
255        let line = "     command";
256        assert_eq!(parse_cmd(line), ("command", ""));
257    }
258
259    #[test]
260    fn test_run() {
261        let mut app = setup();
262
263        app.run().unwrap();
264
265        // let std_out_lines = ;
266        let line1 = String::from_utf8(app.stdout).unwrap();
267
268        assert_eq!(
269            line1,
270            "(cmd) Hello there!(cmd) (cmd) No command non\n(cmd) "
271        );
272    }
273
274    #[test]
275    fn test_run_stdout_write_err() {
276        let f = std::fs::File::open("test_files/test_in.txt").unwrap();
277        let stdin = io::BufReader::new(f);
278        let stdout = StdoutWriteErr;
279        let mut app = Cmd::new(stdin, stdout);
280
281        app.stdout.flush().unwrap(); // this line is here to ensure all statements are run during testing
282
283        let e = app.run().unwrap_err();
284
285        assert_eq!(e.kind(), io::ErrorKind::Other);
286        assert_eq!(e.to_string(), "failed on write");
287    }
288
289    #[test]
290    fn test_run_stdout_flush_err() {
291        let f = std::fs::File::open("test_files/test_in.txt").unwrap();
292        let stdin = io::BufReader::new(f);
293        let stdout = StdoutFlushErr;
294        let mut app = Cmd::new(stdin, stdout);
295
296        let e = app.run().unwrap_err();
297
298        assert_eq!(e.kind(), io::ErrorKind::Other);
299        assert_eq!(e.to_string(), "failed on flush");
300    }
301
302    #[test]
303    fn test_run_stdin_read_err() {
304        let stdin = BufReader::new(StdinAlwaysErr);
305        let stdout = io::stdout();
306        let mut app = Cmd::new(stdin, stdout);
307
308        let e = app.run().unwrap_err();
309
310        assert_eq!(e.kind(), io::ErrorKind::Other);
311        assert_eq!(e.to_string(), "failed on read");
312    }
313
314    #[test]
315    fn test_split_args() {
316        let args = "arg1 arg2 arg3";
317        let expected = vec!["arg1", "arg2", "arg3"];
318        assert_eq!(split_args(args), expected);
319    }
320
321    #[test]
322    fn split_empty_args() {
323        let args = "";
324        let expected: Vec<&str> = vec![];
325        assert_eq!(split_args(args), expected);
326    }
327}