use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::fmt::{Debug, Formatter};
#[allow(unused)]
#[derive(Clone, Debug, PartialEq)]
pub enum ParamCountCheck {
Equal(u32),
AtLeast(u32),
AtMost(u32),
Range{ min: u32, max: u32 },
OneOf(Vec<Self>),
}
impl fmt::Display for ParamCountCheck {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ParamCountCheck::Equal(target) =>
write!(f, "x == {target}"),
ParamCountCheck::AtLeast(target) =>
write!(f, "{target} <= x"),
ParamCountCheck::AtMost(target) =>
write!(f, "x <= {target}"),
ParamCountCheck::Range {min, max} =>
write!(f, "{min} <= x <= {max}"),
ParamCountCheck::OneOf(sub_conditions) => {
for (idx, condition) in sub_conditions.iter().enumerate() {
if idx != 0 {
write!(f, " | ")?;
}
write!(f, "{condition}")?;
}
Ok(())
}
}
}
}
impl ParamCountCheck {
pub fn matches(&self, count: u32) -> bool {
match self {
ParamCountCheck::Equal(target) => &count == target,
ParamCountCheck::AtLeast(target) => target <= &count,
ParamCountCheck::AtMost(target) => &count <= target,
ParamCountCheck::Range {min, max} => min <= &count && &count <= max,
ParamCountCheck::OneOf(conditions) => conditions.iter()
.any(|condition| condition.matches(count)),
}
}
pub fn check(self, count: u32) -> Result<(), CommandError> {
if !self.matches(count) {
return Err(CommandError::InvalidParameterCount {actual: count, compare_op: self});
}
Ok(())
}
}
#[derive(Debug)]
pub enum CommandError {
InvalidParameter(String),
InvalidParameterCount{ actual: u32, compare_op: ParamCountCheck },
CommandFailure(Box<dyn Error + Send>),
}
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
CommandError::InvalidParameter(parameter) =>
write!(f, "Invalid command parameter: {parameter}"),
CommandError::InvalidParameterCount {actual, compare_op } =>
write!(f, "Parameter count ({actual}) does not match constraint(s): {compare_op}"),
CommandError::CommandFailure(e) =>
write!(f, "Command failed: {}", e.to_string()),
}
}
}
impl Error for CommandError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
CommandError::CommandFailure(e) => e.source(),
_ => None,
}
}
}
pub trait Command: Debug + Send + Sync + 'static {
fn handle(&self, parameters: &[String]) -> Result<(), CommandError>;
#[allow(unused)]
fn options(&self, parameters: &[String]) -> Vec<String> {
vec![]
}
}
#[derive(Debug)]
pub enum CommandRegisterError {
CommandExists(String),
AliasTargetMissing(String),
}
impl fmt::Display for CommandRegisterError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
CommandRegisterError::CommandExists(command) =>
write!(f, "Command '{command}' already exists"),
CommandRegisterError::AliasTargetMissing(command) =>
write!(f, "No alias target '{command}' exists"),
}
}
}
impl Error for CommandRegisterError {}
pub trait CommandRegistry {
fn register_command<S: Into<String>>(
&mut self,
key: S,
command: Box<dyn Command>,
) -> Result<&mut Self, CommandRegisterError>;
fn register_alias<S: Into<String>, T: Into<String>>(
&mut self,
alias: S,
key: T,
) -> Result<&mut Self, CommandRegisterError>;
}
#[derive(Debug)]
enum CommandEntry {
COMMAND(Box<dyn Command>),
ALIAS(String),
}
#[derive(Debug, Default)]
pub struct CommandTree {
commands: HashMap<String, CommandEntry>,
}
impl CommandTree {
fn get(&self, key: &String) -> Option<&Box<dyn Command>> {
let entry = self.commands.get(key)?;
match entry {
CommandEntry::COMMAND(command) => Some(command),
CommandEntry::ALIAS(alias) => self.get(alias),
}
}
}
impl CommandRegistry for CommandTree {
fn register_command<S: Into<String>>(
&mut self,
key: S,
command: Box<dyn Command>,
) -> Result<&mut Self, CommandRegisterError> {
let key: String = key.into();
if self.commands.contains_key(&key) {
return Err(CommandRegisterError::CommandExists(key))
}
self.commands.insert(key, CommandEntry::COMMAND(command));
Ok(self)
}
fn register_alias<S: Into<String>, T: Into<String>>(
&mut self,
alias: S,
key: T,
) -> Result<&mut Self, CommandRegisterError> {
let alias: String = alias.into();
let key: String = key.into();
if self.commands.contains_key(&alias) {
return Err(CommandRegisterError::CommandExists(alias))
} else if !self.commands.contains_key(&key) {
return Err(CommandRegisterError::AliasTargetMissing(key))
}
self.commands.insert(alias, CommandEntry::ALIAS(key));
Ok(self)
}
}
impl Command for CommandTree {
fn handle(&self, parameters: &[String]) -> Result<(), CommandError> {
if parameters.len() < 1 {
return Err(CommandError::InvalidParameter("".into()))
}
match self.get(¶meters[0]) {
Some(command) => command.handle(¶meters[1..]),
None => Err(CommandError::InvalidParameter(parameters[0].clone())),
}
}
fn options(&self, parameters: &[String]) -> Vec<String> {
if parameters.len() < 1 {
return Vec::new()
} else if parameters.len() == 1 {
let mut options = self.commands.keys()
.filter(|key| key.starts_with(¶meters[0]))
.map(|key| key.clone())
.collect::<Vec<_>>();
options.sort();
return options;
}
match self.get(¶meters[0]) {
Some(command) => command.options(¶meters[1..]),
None => Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use std::fmt::{Display};
use std::ptr;
use parking_lot::RwLock;
use super::*;
#[derive(Default, Debug)]
struct StoreCommand {
value: RwLock<Option<String>>,
}
impl Command for StoreCommand {
fn handle(&self, parameters: &[String]) -> Result<(), CommandError> {
ParamCountCheck::Range{min: 1, max: 2}.check(parameters.len() as u32)?;
match parameters[0].as_str() {
"set" => {
ParamCountCheck::Equal(2).check(parameters.len() as u32)?;
*self.value.write() = Some(parameters[1].clone());
Ok(())
},
"clear" => {
ParamCountCheck::Equal(1).check(parameters.len() as u32)?;
*self.value.write() = None;
Ok(())
},
_ => Err(CommandError::InvalidParameter(parameters[0].clone())),
}
}
fn options(&self, parameters: &[String]) -> Vec<String> {
match parameters.len() {
1 => ["set".into(), "clear".into()].into_iter()
.filter(|val: &String| val.starts_with(¶meters[0]))
.collect(),
_ => vec![],
}
}
}
#[test]
fn test_command_options() {
let command = StoreCommand::default();
assert_eq!(
command.options(&["".into()]),
vec!["set".to_owned(), "clear".to_owned()],
);
assert_eq!(
command.options(&["set".into()]),
vec!["set".to_owned()],
);
assert_eq!(
command.options(&["cle".into()]),
vec!["clear".to_owned()],
);
assert_eq!(
command.options(&["invalid".into()]),
Vec::<String>::new(),
);
assert_eq!(
command.options(&["clear".into(), "extra".into()]),
Vec::<String>::new(),
)
}
#[test]
fn test_command_success() {
let command = StoreCommand::default();
assert_eq!(command.value.read().clone(), None);
command.handle(&["set".into(), "a".into()])
.expect("Command should succeed");
assert_eq!(command.value.read().clone(), Some("a".to_owned()));
command.handle(&["set".into(), "b".into()])
.expect("Command should succeed");
assert_eq!(command.value.read().clone(), Some("b".to_owned()));
command.handle(&["clear".into()])
.expect("Command should succeed");
assert_eq!(command.value.read().clone(), None);
}
#[test]
fn test_command_invalid_params() {
let command = StoreCommand::default();
assert_matches!(
command.handle(&[])
.expect_err("Command should fail"),
CommandError::InvalidParameterCount {actual: 0, compare_op: ParamCountCheck::Range{min: 1, max: 2}}
);
assert_matches!(
command.handle(&["invalid".into()])
.expect_err("Command should fail"),
CommandError::InvalidParameter(param) => {
assert_eq!(param, "invalid");
}
);
assert_matches!(
command.handle(&["set".into()])
.expect_err("Command should fail"),
CommandError::InvalidParameterCount {actual: 1, compare_op: ParamCountCheck::Equal(2)}
);
assert_matches!(
command.handle(&["set".into(), "val".into(), "extra".into()])
.expect_err("Command should fail"),
CommandError::InvalidParameterCount {actual: 3, compare_op: ParamCountCheck::Range{min: 1, max: 2}}
);
assert_matches!(
command.handle(&["clear".into(), "extra".into()])
.expect_err("Command should fail"),
CommandError::InvalidParameterCount {actual: 2, compare_op: ParamCountCheck::Equal(1)}
);
}
#[derive(Debug)]
struct CommandFailure;
impl Display for CommandFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Command failed")
}
}
impl Error for CommandFailure {}
#[derive(Default, Debug)]
struct FailCommand;
impl Command for FailCommand {
fn handle(&self, _parts: &[String]) -> Result<(), CommandError> {
Err(CommandError::CommandFailure(Box::new(CommandFailure {})))
}
}
#[test]
fn test_command_failure() {
let command = FailCommand::default();
assert_matches!(
command.handle(&[])
.expect_err("Command should fail"),
CommandError::CommandFailure(error) => {
assert_eq!(error.to_string(), "Command failed".to_owned());
}
)
}
#[derive(Default, Debug)]
struct NoOpCommand;
impl Command for NoOpCommand {
fn handle(&self, _parts: &[String]) -> Result<(), CommandError> {
Ok(())
}
}
fn create_command_map() -> CommandTree {
let mut map = CommandTree::default();
map.register_command("noop", Box::new(NoOpCommand::default()))
.expect("Command should register")
.register_alias("noop2", "noop")
.expect("Alias to command should register")
.register_alias("noop3", "noop2")
.expect("Alias to alias should register");
map
}
fn create_nested_command_map() -> CommandTree {
let sub_map = create_command_map();
let mut map = CommandTree::default();
map.register_command("sub", Box::new(sub_map))
.expect("Subcommand map should register");
map.register_command("fail", Box::new(FailCommand::default()))
.expect("Command should register");
map
}
#[test]
fn test_command_map_register_failure() {
let mut commands = create_command_map();
assert_matches!(
commands.register_command("noop", Box::new(NoOpCommand::default()))
.expect_err("Register should fail"),
CommandRegisterError::CommandExists(command) => {
assert_eq!(command, "noop");
}
);
assert_matches!(
commands.register_command("noop2", Box::new(NoOpCommand::default()))
.expect_err("Register should fail"),
CommandRegisterError::CommandExists(command) => {
assert_eq!(command, "noop2");
}
);
assert_matches!(
commands.register_alias("new-alias", "non-existent")
.expect_err("Register should fail"),
CommandRegisterError::AliasTargetMissing(command) => {
assert_eq!(command, "non-existent");
}
);
}
#[test]
fn test_command_map_alias() {
let commands = create_command_map();
let command = commands.get(&"noop".into())
.expect("Command should exist");
let alias = commands.get(&"noop2".into())
.expect("Alias should exist");
let alias2 = commands.get(&"noop3".into())
.expect("Alias to alias should exist");
assert!(ptr::eq(command, alias));
assert!(ptr::eq(command, alias2));
}
#[test]
fn test_command_map_options() {
let commands = create_nested_command_map();
assert_eq!(
commands.options(&["".into()]),
vec!["fail".to_owned(), "sub".to_owned()],
);
assert_eq!(
commands.options(&["sub".into()]),
vec!["sub".to_owned()],
);
assert_eq!(
commands.options(&["fa".into()]),
vec!["fail".to_owned()],
);
assert_eq!(
commands.options(&["invalid".into()]),
Vec::<String>::new(),
);
assert_eq!(
commands.options(&["sub".into(), "".into()]),
vec!["noop".to_owned(), "noop2".to_owned(), "noop3".to_owned()],
);
assert_eq!(
commands.options(&["sub".into(), "noo".into()]),
vec!["noop".to_owned(), "noop2".to_owned(), "noop3".to_owned()],
);
assert_eq!(
commands.options(&["sub".into(), "noop2".into()]),
vec!["noop2".to_owned()],
);
assert_eq!(
commands.options(&["sub".into(), "invalid".into()]),
Vec::<String>::new(),
);
assert_eq!(
commands.options(&["invalid".into(), "invalid".into()]),
Vec::<String>::new(),
);
}
#[test]
fn test_command_map_handle() {
let commands = create_nested_command_map();
commands.handle(&["sub".into(), "noop".into()])
.expect("Command should succeed");
commands.handle(&["sub".into(), "noop2".into()])
.expect("Command should succeed");
commands.handle(&["sub".into(), "noop3".into()])
.expect("Command should succeed");
assert_matches!(
commands.handle(&["sub".into(), "invalid".into()])
.expect_err("Command should fail"),
CommandError::InvalidParameter(param) => {
assert_eq!(param, "invalid");
}
);
assert_matches!(
commands.handle(&["fail".into()])
.expect_err("Command should fail"),
CommandError::CommandFailure(error) => {
assert_eq!(error.to_string(), "Command failed".to_owned());
}
);
assert_matches!(
commands.handle(&["invalid".into()])
.expect_err("Command should fail"),
CommandError::InvalidParameter(param) => {
assert_eq!(param, "invalid");
}
);
}
}