easy_repl/
command.rs

1//! Implementation of [`Command`]s with utilities that help to crate them.
2
3use anyhow;
4use thiserror;
5
6/// Command handler.
7///
8/// It should return the status in case of correct execution. In case of
9/// errors, all the errors will be handled by the REPL, except for
10/// [`CriticalError`], which will be passed up from the REPL.
11///
12/// The handler should validate command arguments and can return [`ArgsError`]
13/// to indicate that arguments were wrong.
14pub type Handler<'a> = dyn 'a + FnMut(&[&str]) -> anyhow::Result<CommandStatus>;
15
16/// Single command that can be called in the REPL.
17///
18/// Though it is possible to construct it by manually, it is not advised.
19/// One should rather use the provided [`command!`] macro which will generate
20/// appropriate arguments validation and args_info based on passed specification.
21pub struct Command<'a> {
22    /// Command desctiption that will be displayed in the help message
23    pub description: String,
24    /// Names and types of arguments to the command
25    pub args_info: Vec<String>,
26    /// Command handler which should validate arguments and perform command logic
27    pub handler: Box<Handler<'a>>,
28}
29
30/// Return status of a command.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum CommandStatus {
33    /// Indicates that REPL should continue execution
34    Done,
35    /// Indicates that REPL should quit
36    Quit,
37}
38
39/// Special error wrapper used to indicate that a critical error occured.
40///
41/// [`Handler`] can return [`CriticalError`] to indicate that this error
42/// should not be handled by the REPL (which just prints error message
43/// and continues for all other errors).
44///
45/// This is most conveniently used via the [`Critical`] extension trait.
46#[derive(Debug, thiserror::Error)]
47pub enum CriticalError {
48    /// The contained error is critical and should be returned back from REPL.
49    #[error(transparent)]
50    Critical(#[from] anyhow::Error),
51}
52
53/// Extension trait to easily wrap errors in [`CriticalError`].
54///
55/// This is implemented for [`std::result::Result`] so can be used to coveniently
56/// wrap errors that implement [`std::error::Error`] to indicate that they are
57/// critical and should be returned by the REPL, for example:
58/// ```rust
59/// # use easy_repl::{CriticalError, Critical};
60/// let result: Result<(), std::fmt::Error> = Err(std::fmt::Error);
61/// let critical = result.into_critical();
62/// assert!(matches!(critical, Err(CriticalError::Critical(_))));
63/// ```
64///
65/// See `examples/errors.rs` for a concrete usage example.
66pub trait Critical<T, E> {
67    /// Wrap the contained [`Err`] in [`CriticalError`] or leave [`Ok`] untouched
68    fn into_critical(self) -> Result<T, CriticalError>;
69}
70
71impl<T, E> Critical<T, E> for Result<T, E>
72where
73    E: std::error::Error + Send + Sync + 'static,
74{
75    fn into_critical(self) -> Result<T, CriticalError> {
76        self.map_err(|e| CriticalError::Critical(e.into()))
77    }
78}
79
80/// Wrong command arguments.
81#[allow(missing_docs)]
82#[derive(Debug, thiserror::Error)]
83pub enum ArgsError {
84    #[error("wrong number of arguments: got {got}, expected {expected}")]
85    WrongNumberOfArguments { got: usize, expected: usize },
86    #[error("failed to parse argument value '{argument}': {error}")]
87    WrongArgumentValue {
88        argument: String,
89        #[source]
90        error: anyhow::Error,
91    },
92}
93
94impl<'a> Command<'a> {
95    /// Validate the arguments and invoke the handler if arguments are correct.
96    pub fn run(&mut self, args: &[&str]) -> anyhow::Result<CommandStatus> {
97        (self.handler)(args)
98    }
99
100    /// Returns the string description of the argument types
101    pub fn arg_types(&self) -> Vec<&str> {
102        self.args_info
103            .iter()
104            .map(|info| info.split(":").collect::<Vec<_>>()[1])
105            .collect()
106    }
107}
108
109impl<'a> std::fmt::Debug for Command<'a> {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("Command")
112            .field("description", &self.description)
113            .finish()
114    }
115}
116
117/// Generate argument validator based on a list of types (used by [`command!`]).
118///
119/// This macro can be used to generate a closure that takes arguments as `&[&str]`
120/// and makes sure that the nubmer of arguments is correct and all can be parsed
121/// to appropriate types. This macro should generally not be used. Prefer to use
122/// [`command!`] which will use this macro appropriately.
123///
124/// Example usage:
125/// ```rust
126/// # use easy_repl::validator;
127/// let validator = validator!(i32, f32, String);
128/// assert!(validator(&["10", "3.14", "hello"]).is_ok());
129/// ```
130///
131/// # Note
132///
133/// For string arguments use [`String`] instead of [`&str`].
134#[macro_export]
135macro_rules! validator {
136    ($($type:ty),*) => {
137        |args: &[&str]| -> std::result::Result<(), $crate::command::ArgsError> {
138            // check the number of arguments
139            let n_args: usize = <[()]>::len(&[ $( $crate::validator!(@replace $type ()) ),* ]);
140            if args.len() != n_args {
141                return Err($crate::command::ArgsError::WrongNumberOfArguments {
142                    got: args.len(),
143                    expected: n_args,
144            });
145            }
146            #[allow(unused_variables, unused_mut)]
147            let mut i = 0;
148            #[allow(unused_assignments)]
149            {
150                $(
151                    if let Err(err) = args[i].parse::<$type>() {
152                        return Err($crate::command::ArgsError::WrongArgumentValue {
153                            argument: args[i].into(),
154                            error: err.into()
155                    });
156                    }
157                    i += 1;
158                )*
159            }
160
161            Ok(())
162        }
163    };
164    // Helper that allows to replace one expression with another (possibly "noop" one)
165    (@replace $_old:tt $new:expr) => { $new };
166}
167
168// TODO: avoid parsing arguments 2 times by generating validation logic in the function
169/// Generate [`Command`] based on desctiption, list of arg types and a closure used in handler.
170///
171/// This macro should be used when creating [`Command`]s. It takes a string description,
172/// a list of argument types with optional names (in the form `name: type`) and a closure.
173/// The closure should have the same number of arguments as provided in the argument list.
174/// The generated command handler will parse all the arguments and call the closure.
175/// The closure used for handler is `move`.
176///
177/// The following command description:
178/// ```rust
179/// # use easy_repl::{CommandStatus, command};
180/// let cmd = command! {
181///     "Example command",
182///     (arg1: i32, arg2: String) => |arg1, arg2| {
183///         Ok(CommandStatus::Done)
184///     }
185/// };
186/// ```
187///
188/// will roughly be translated into something like (code here is slightly simplified):
189/// ```rust
190/// # use anyhow;
191/// # use easy_repl::{Command, CommandStatus, command, validator};
192/// let cmd = Command {
193///     description: "Example command".into(),
194///     args_info: vec!["arg1:i32".into(), "arg2:String".into()],
195///     handler: Box::new(move |args| -> anyhow::Result<CommandStatus> {
196///         let validator = validator!(i32, String);
197///         validator(args)?;
198///         let mut handler = |arg1, arg2| {
199///             Ok(CommandStatus::Done)
200///         };
201///         handler(args[0].parse::<i32>().unwrap(), args[1].parse::<String>().unwrap())
202///     }),
203/// };
204/// ```
205#[macro_export]
206macro_rules! command {
207    ($description:expr, ( $($( $name:ident )? : $type:ty),* ) => $handler:expr $(,)?) => {
208        $crate::command::Command {
209            description: $description.into(),
210            args_info: vec![ $(
211                concat!($(stringify!($name), )? ":", stringify!($type)).into()
212            ),* ], // TODO
213            handler: command!(@handler $($type)*, $handler),
214        }
215    };
216    (@handler $($type:ty)*, $handler:expr) => {
217        Box::new( move |#[allow(unused_variables)] args| -> $crate::anyhow::Result<CommandStatus> {
218            let validator = $crate::validator!($($type),*);
219            validator(args)?;
220            #[allow(unused_mut)]
221            let mut handler = $handler;
222            command!(@handler_call handler; args; $($type;)*)
223        })
224    };
225    // transform element of $args into parsed function argument by calling .parse::<$type>().unwrap()
226    // on each, this starts a recursive muncher that constructs following argument getters args[i]
227    (@handler_call $handler:ident; $args:ident; $($types:ty;)*) => {
228        command!(@handler_call $handler, $args, 0; $($types;)* =>)
229    };
230    // $num is used to index $args; pop $type from beginning of list, add new parsed at the endo of $parsed
231    (@handler_call $handler:ident, $args:ident, $num:expr; $type:ty; $($types:ty;)* => $($parsed:expr;)*) => {
232        command!(@handler_call $handler, $args, $num + 1;
233            $($types;)* =>
234            $($parsed;)* $args[$num].parse::<$type>().unwrap();
235        )
236    };
237    // finally when there are no more types emit code that calls the handler with all arguments parsed
238    (@handler_call $handler:ident, $args:ident, $num:expr; => $($parsed:expr;)*) => {
239        $handler( $($parsed),* )
240    };
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn manual_command() {
249        let mut cmd = Command {
250            description: "Test command".into(),
251            args_info: vec![],
252            handler: Box::new(|_args| Ok(CommandStatus::Done)),
253        };
254        match (cmd.handler)(&[]) {
255            Ok(CommandStatus::Done) => {}
256            _ => panic!("Wrong variant"),
257        };
258    }
259
260    #[test]
261    fn validator_no_args() {
262        let validator = validator!();
263        assert!(validator(&[]).is_ok());
264        assert!(validator(&["hello"]).is_err());
265    }
266
267    #[test]
268    fn validator_one_arg() {
269        let validator = validator!(i32);
270        assert!(validator(&[]).is_err());
271        assert!(validator(&["hello"]).is_err());
272        assert!(validator(&["13"]).is_ok());
273    }
274
275    #[test]
276    fn validator_multiple_args() {
277        let validator = validator!(i32, f32, String);
278        assert!(validator(&[]).is_err());
279        assert!(validator(&["1", "2.1", "hello"]).is_ok());
280        assert!(validator(&["1.2", "2.1", "hello"]).is_err());
281        assert!(validator(&["1", "a", "hello"]).is_err());
282        assert!(validator(&["1", "2.1", "hello", "world"]).is_err());
283    }
284
285    #[test]
286    fn command_auto_no_args() {
287        let mut cmd = command! {
288            "Example cmd",
289            () => || {
290                Ok(CommandStatus::Done)
291            }
292        };
293        match cmd.run(&[]) {
294            Ok(CommandStatus::Done) => {}
295            Ok(v) => panic!("Wrong variant: {:?}", v),
296            Err(e) => panic!("Error: {:?}", e),
297        };
298    }
299
300    #[test]
301    fn command_auto_with_args() {
302        let mut cmd = command! {
303            "Example cmd",
304            (:i32, :f32) => |_x, _y| {
305                Ok(CommandStatus::Done)
306            }
307        };
308        match cmd.run(&["13", "1.1"]) {
309            Ok(CommandStatus::Done) => {}
310            Ok(v) => panic!("Wrong variant: {:?}", v),
311            Err(e) => panic!("Error: {:?}", e),
312        };
313    }
314
315    #[test]
316    fn command_auto_with_critical() {
317        let mut cmd = command! {
318            "Example cmd",
319            (:i32, :f32) => |_x, _y| {
320                let err = std::io::Error::new(std::io::ErrorKind::InvalidData, "example error");
321                Err(CriticalError::Critical(err.into()).into())
322            }
323        };
324        match cmd.run(&["13", "1.1"]) {
325            Ok(v) => panic!("Wrong variant: {:?}", v),
326            Err(e) => {
327                if e.downcast_ref::<CriticalError>().is_none() {
328                    panic!("Wrong error: {:?}", e)
329                }
330            }
331        };
332    }
333
334    #[test]
335    fn command_auto_args_info() {
336        let cmd = command!("Example cmd", (:i32, :String, :f32) => |_x, _s, _y| { Ok(CommandStatus::Done) });
337        assert_eq!(cmd.args_info, &[":i32", ":String", ":f32"]);
338        let cmd = command!("Example cmd", (:i32, :f32) => |_x, _y| { Ok(CommandStatus::Done) });
339        assert_eq!(cmd.args_info, &[":i32", ":f32"]);
340        let cmd = command!("Example cmd", (:f32) => |_x| { Ok(CommandStatus::Done) });
341        assert_eq!(cmd.args_info, &[":f32"]);
342        let cmd = command!("Example cmd", () => || { Ok(CommandStatus::Done) });
343        let res: &[&str] = &[];
344        assert_eq!(cmd.args_info, res);
345    }
346
347    #[test]
348    fn command_auto_args_info_with_names() {
349        let cmd = command! {
350            "Example cmd",
351            (number:i32, name : String, :f32) => |_x, _s, _y| Ok(CommandStatus::Done)
352        };
353        assert_eq!(cmd.args_info, &["number:i32", "name:String", ":f32"]);
354    }
355}