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}