use anyhow;
use thiserror;
use std::fmt::Display;
use std::fmt::Formatter;
use std::future::Future;
use std::pin::Pin;
pub trait ExecuteCommand {
fn execute(
&mut self,
args: Vec<String>,
args_info: Vec<CommandArgInfo>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>>;
}
pub async fn lift_validation_err(
validation_err: Result<(), ArgsError>,
) -> Result<CommandStatus, anyhow::Error> {
validation_err
.map_err(|e| Err(e.into()))
.expect_err("Not a validation error, this is a bug.")
}
pub struct TrivialCommandHandler {}
impl TrivialCommandHandler {
pub fn new() -> Self {
Self {}
}
async fn handle_command(&mut self, _args: Vec<String>) -> anyhow::Result<CommandStatus> {
Ok(CommandStatus::Done)
}
}
impl ExecuteCommand for TrivialCommandHandler {
fn execute(
&mut self,
args: Vec<String>,
_args_info: Vec<CommandArgInfo>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
Box::pin(self.handle_command(args))
}
}
#[derive(Clone)]
pub struct CommandArgInfo {
pub arg_type: CommandArgType,
pub name: Option<String>,
}
impl CommandArgInfo {
pub fn new(arg_type: CommandArgType) -> Self {
CommandArgInfo {
arg_type,
name: None,
}
}
pub fn new_with_name(arg_type: CommandArgType, name: &str) -> Self {
CommandArgInfo {
arg_type,
name: Some(name.into()),
}
}
pub fn to_string(self) -> String {
format!("{}:{}", self.name.unwrap_or("".to_string()), self.arg_type)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommandArgType {
I32,
F32,
String,
Custom,
}
impl Display for CommandArgType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CommandArgType::I32 => write!(f, "i32"),
CommandArgType::F32 => write!(f, "f32"),
CommandArgType::String => write!(f, "String"),
CommandArgType::Custom => write!(f, "Custom"),
}
}
}
pub struct Command {
pub(crate) description: String,
pub(crate) args_info: Vec<CommandArgInfo>,
pub(crate) handler: Box<dyn ExecuteCommand>,
}
impl Command {
pub fn new(
desc: &str,
args_info: Vec<CommandArgInfo>,
handler: Box<dyn ExecuteCommand>,
) -> Self {
Self {
description: desc.into(),
args_info,
handler,
}
}
pub fn execute(
&mut self,
args: &[&str],
) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
self.handler.execute(
args.iter().map(|s| s.to_string()).collect(),
self.args_info.clone(),
)
}
pub fn arg_types(&self) -> Vec<String> {
self.args_info
.iter()
.map(|info| {
let info_string = info.clone().to_string();
let parts = info_string.split(':').collect::<Vec<_>>();
parts[1].to_string()
})
.collect()
}
}
pub fn validate(
args: Vec<String>,
arg_infos: Vec<CommandArgInfo>,
) -> std::result::Result<(), ArgsError> {
if args.len() != arg_infos.len() {
return Err(ArgsError::WrongNumberOfArguments {
got: args.len(),
expected: arg_infos.len(),
});
}
for (i, arg_value) in args.iter().enumerate() {
let arg_info = arg_infos[i].clone();
let arg_type: CommandArgType = arg_info.arg_type;
match arg_type {
CommandArgType::I32 => {
if let Err(err) = &arg_value.parse::<i32>() {
return Err(ArgsError::WrongArgumentValue {
argument: arg_value.to_string(),
error: err.to_string(),
});
}
}
CommandArgType::F32 => {
if let Err(err) = &arg_value.parse::<f32>() {
return Err(ArgsError::WrongArgumentValue {
argument: arg_value.to_string(),
error: err.to_string(),
});
}
}
CommandArgType::String => (),
CommandArgType::Custom => (),
}
}
Ok(())
}
#[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, error: String },
#[error("no command variant found for provided args")]
NoVariantFound,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validator_no_args() {
let arg_types = vec![];
assert!(validate(vec![], arg_types.clone()).is_ok());
assert!(validate(vec!["hello".into()], arg_types.clone()).is_err())
}
#[test]
fn validator_one_arg() {
let arg_types = vec![CommandArgInfo::new(CommandArgType::I32)];
assert!(validate(vec![], arg_types.clone()).is_err());
assert!(validate(vec!["hello".into()], arg_types.clone()).is_err());
assert!(validate(vec!["13".into()], arg_types.clone()).is_ok())
}
#[test]
fn validator_multiple_args() {
let arg_types = vec![
CommandArgInfo::new(CommandArgType::I32),
CommandArgInfo::new(CommandArgType::F32),
CommandArgInfo::new(CommandArgType::String),
];
assert!(validate(vec![], arg_types.clone()).is_err());
assert!(validate(
vec!["1".into(), "2.1".into(), "hello".into()],
arg_types.clone()
)
.is_ok());
assert!(validate(
vec!["1.2".into(), "2.1".into(), "hello".into()],
arg_types.clone()
)
.is_err());
assert!(validate(
vec!["1".into(), "a".into(), "hello".into()],
arg_types.clone()
)
.is_err());
assert!(validate(
vec!["1".into(), "2.1".into(), "hello".into(), "world".into()],
arg_types.clone()
)
.is_err());
}
#[tokio::test]
async fn manual_command() {
let mut cmd = Command::new(
"Test command",
vec![CommandArgInfo::new(CommandArgType::String)],
Box::new(TrivialCommandHandler::new()),
);
let result = cmd.execute(&["hello"]).await;
match result {
Ok(CommandStatus::Done) => {}
_ => panic!("Wrong variant"),
}
}
#[tokio::test]
async fn command_with_args() {
let mut cmd = Command::new(
"Example cmd",
vec![
CommandArgInfo::new(CommandArgType::I32),
CommandArgInfo::new(CommandArgType::F32),
],
Box::new(TrivialCommandHandler::new()),
);
let result = cmd.execute(&["13", "1.1"]).await;
match result {
Ok(CommandStatus::Done) => {}
Ok(v) => panic!("Wrong variant: {:?}", v),
Err(e) => panic!("Error: {:?}", e),
};
}
#[tokio::test]
async fn command_with_critical() {
struct WithCriticalCommandHandler {}
impl WithCriticalCommandHandler {
fn new() -> Self {
WithCriticalCommandHandler {}
}
async fn handle_command(
&mut self,
_args: Vec<String>,
) -> anyhow::Result<CommandStatus> {
let err = std::io::Error::new(std::io::ErrorKind::InvalidData, "example error");
Err(CriticalError::Critical(err.into()).into())
}
}
impl ExecuteCommand for WithCriticalCommandHandler {
fn execute(
&mut self,
args: Vec<String>,
_args_info: Vec<CommandArgInfo>,
) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
Box::pin(self.handle_command(args))
}
}
let mut cmd = Command::new(
"Example cmd",
vec![
CommandArgInfo::new(CommandArgType::I32),
CommandArgInfo::new(CommandArgType::F32),
],
Box::new(WithCriticalCommandHandler::new()),
);
let result = cmd.execute(&["13", "1.1"]).await;
match result {
Ok(v) => panic!("Wrong variant: {:?}", v),
Err(e) => {
if e.downcast_ref::<CriticalError>().is_none() {
panic!("Wrong error: {:?}", e)
}
}
};
}
}