use anyhow;
use thiserror;
pub type Handler<'a> = dyn 'a + FnMut(&[&str]) -> anyhow::Result<CommandStatus>;
pub struct Command<'a> {
pub description: String,
pub args_info: Vec<String>,
pub handler: Box<Handler<'a>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommandStatus {
Done,
Quit,
}
#[derive(Debug, thiserror::Error)]
pub enum CriticalError {
#[error(transparent)]
Critical(#[from] anyhow::Error),
}
pub trait Critical<T, E> {
fn into_critical(self) -> Result<T, CriticalError>;
}
impl<T, E> Critical<T, E> for Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn into_critical(self) -> Result<T, CriticalError> {
self.map_err(|e| CriticalError::Critical(e.into()))
}
}
#[allow(missing_docs)]
#[derive(Debug, thiserror::Error)]
pub enum ArgsError {
#[error("wrong number of arguments: got {got}, expected {expected}")]
WrongNumberOfArguments { got: usize, expected: usize },
#[error("failed to parse argument value '{argument}': {error}")]
WrongArgumentValue {
argument: String,
#[source]
error: anyhow::Error,
},
}
impl<'a> Command<'a> {
pub fn run(&mut self, args: &[&str]) -> anyhow::Result<CommandStatus> {
(self.handler)(args)
}
pub fn arg_types(&self) -> Vec<&str> {
self.args_info
.iter()
.map(|info| info.split(":").collect::<Vec<_>>()[1])
.collect()
}
}
impl<'a> std::fmt::Debug for Command<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command")
.field("description", &self.description)
.finish()
}
}
#[macro_export]
macro_rules! validator {
($($type:ty),*) => {
|args: &[&str]| -> std::result::Result<(), $crate::command::ArgsError> {
let n_args: usize = <[()]>::len(&[ $( $crate::validator!(@replace $type ()) ),* ]);
if args.len() != n_args {
return Err($crate::command::ArgsError::WrongNumberOfArguments {
got: args.len(),
expected: n_args,
});
}
#[allow(unused_variables, unused_mut)]
let mut i = 0;
#[allow(unused_assignments)]
{
$(
if let Err(err) = args[i].parse::<$type>() {
return Err($crate::command::ArgsError::WrongArgumentValue {
argument: args[i].into(),
error: err.into()
});
}
i += 1;
)*
}
Ok(())
}
};
(@replace $_old:tt $new:expr) => { $new };
}
#[macro_export]
macro_rules! command {
($description:expr, ( $($( $name:ident )? : $type:ty),* ) => $handler:expr $(,)?) => {
$crate::command::Command {
description: $description.into(),
args_info: vec![ $(
concat!($(stringify!($name), )? ":", stringify!($type)).into()
),* ], handler: command!(@handler $($type)*, $handler),
}
};
(@handler $($type:ty)*, $handler:expr) => {
Box::new( move |#[allow(unused_variables)] args| -> $crate::anyhow::Result<CommandStatus> {
let validator = $crate::validator!($($type),*);
validator(args)?;
#[allow(unused_mut)]
let mut handler = $handler;
command!(@handler_call handler; args; $($type;)*)
})
};
(@handler_call $handler:ident; $args:ident; $($types:ty;)*) => {
command!(@handler_call $handler, $args, 0; $($types;)* =>)
};
(@handler_call $handler:ident, $args:ident, $num:expr; $type:ty; $($types:ty;)* => $($parsed:expr;)*) => {
command!(@handler_call $handler, $args, $num + 1;
$($types;)* =>
$($parsed;)* $args[$num].parse::<$type>().unwrap();
)
};
(@handler_call $handler:ident, $args:ident, $num:expr; => $($parsed:expr;)*) => {
$handler( $($parsed),* )
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manual_command() {
let mut cmd = Command {
description: "Test command".into(),
args_info: vec![],
handler: Box::new(|_args| Ok(CommandStatus::Done)),
};
match (cmd.handler)(&[]) {
Ok(CommandStatus::Done) => {}
_ => panic!("Wrong variant"),
};
}
#[test]
fn validator_no_args() {
let validator = validator!();
assert!(validator(&[]).is_ok());
assert!(validator(&["hello"]).is_err());
}
#[test]
fn validator_one_arg() {
let validator = validator!(i32);
assert!(validator(&[]).is_err());
assert!(validator(&["hello"]).is_err());
assert!(validator(&["13"]).is_ok());
}
#[test]
fn validator_multiple_args() {
let validator = validator!(i32, f32, String);
assert!(validator(&[]).is_err());
assert!(validator(&["1", "2.1", "hello"]).is_ok());
assert!(validator(&["1.2", "2.1", "hello"]).is_err());
assert!(validator(&["1", "a", "hello"]).is_err());
assert!(validator(&["1", "2.1", "hello", "world"]).is_err());
}
#[test]
fn command_auto_no_args() {
let mut cmd = command! {
"Example cmd",
() => || {
Ok(CommandStatus::Done)
}
};
match cmd.run(&[]) {
Ok(CommandStatus::Done) => {}
Ok(v) => panic!("Wrong variant: {:?}", v),
Err(e) => panic!("Error: {:?}", e),
};
}
#[test]
fn command_auto_with_args() {
let mut cmd = command! {
"Example cmd",
(:i32, :f32) => |_x, _y| {
Ok(CommandStatus::Done)
}
};
match cmd.run(&["13", "1.1"]) {
Ok(CommandStatus::Done) => {}
Ok(v) => panic!("Wrong variant: {:?}", v),
Err(e) => panic!("Error: {:?}", e),
};
}
#[test]
fn command_auto_with_critical() {
let mut cmd = command! {
"Example cmd",
(:i32, :f32) => |_x, _y| {
let err = std::io::Error::new(std::io::ErrorKind::InvalidData, "example error");
Err(CriticalError::Critical(err.into()).into())
}
};
match cmd.run(&["13", "1.1"]) {
Ok(v) => panic!("Wrong variant: {:?}", v),
Err(e) => {
if e.downcast_ref::<CriticalError>().is_none() {
panic!("Wrong error: {:?}", e)
}
}
};
}
#[test]
fn command_auto_args_info() {
let cmd = command!("Example cmd", (:i32, :String, :f32) => |_x, _s, _y| { Ok(CommandStatus::Done) });
assert_eq!(cmd.args_info, &[":i32", ":String", ":f32"]);
let cmd = command!("Example cmd", (:i32, :f32) => |_x, _y| { Ok(CommandStatus::Done) });
assert_eq!(cmd.args_info, &[":i32", ":f32"]);
let cmd = command!("Example cmd", (:f32) => |_x| { Ok(CommandStatus::Done) });
assert_eq!(cmd.args_info, &[":f32"]);
let cmd = command!("Example cmd", () => || { Ok(CommandStatus::Done) });
let res: &[&str] = &[];
assert_eq!(cmd.args_info, res);
}
#[test]
fn command_auto_args_info_with_names() {
let cmd = command! {
"Example cmd",
(number:i32, name : String, :f32) => |_x, _s, _y| Ok(CommandStatus::Done)
};
assert_eq!(cmd.args_info, &["number:i32", "name:String", ":f32"]);
}
}