I have the following trait:
pub trait Command {
/// The expected arguments.
type Payload;
/// Returns the command ID.
fn id(&self) -> &'static str {
"0"
}
/// Returns the expected number of arguments.
fn args_count(&self) -> usize {
1
}
/// Parses the arguments.
fn parse(&self, args: Vec<&str>) -> Result<Self::Payload, CommandError>;
/// The function to run.
fn run(&self, args: Self::Payload) -> Result<Option<Vec<String>>, CommandError>;
}
and it can be used like this
/// Sets the clipboard's content.
pub struct SetClipboard;
impl Command for SetClipboard {
type Payload = String;
fn id(&self) -> &'static str {
"2"
}
fn args_count(&self) -> usize {
1
}
fn parse(&self, args: Vec<&str>) -> Result<Self::Payload, CommandError> {
Ok(args.join(""))
}
fn run(&self, payload: Self::Payload) -> Result<Option<Vec<String>>, CommandError> {
// Set clipboard
if let Err(e) = set_clipboard(formats::Unicode, payload) {
return Err(CommandError::SystemError(e.raw_code()))
};
// Return
Ok(None)
}
}
However, I need to reference these "commands" of course. So I'll store them in an array but what do I set the Payload
type as?
pub type Commands = Vec<Box<dyn Command<Payload = dyn std::any::Any>>>;
...
let commands: Commands = vec![
Box::new(MouseMoveRel),
Box::new(MouseMoveAbs),
Box::new(SetClipboard)
];
(this doesn't work due to a type-mismatch).
For each command, I would check if it something matches the same command id to figure out which one to use. This is done within a WebSocket message from a client (as the server).
// Split the text
// Format: `COMMAND ID|JOB ID|ARGUMENTS
let split: Vec<&str> = text.split("|").collect();
// Check the length of the arguments
if split.len() < 2 {
return ctx.text(CommandError::BadlyFormattedCommand)
}
// Find which command
let command_id = split[0];
let Some(command) = self.commands.iter().find(|x| x.id() == command_id) else {
return ctx.text(CommandError::CouldNotFindCommand)
};
// Check the length of the arguments
if split.len() - 2 < command.args_count() {
return ctx.text(CommandError::NotEnoughArguments)
}
// Parse the arguments
let parsed = match command.parse(split[2..].to_vec()) {
Ok(x) => x,
Err(e) => return ctx.text(e)
};
// Run the command
match command.run(parsed) {
Ok(x) => match x {
Some(data) => ctx.text(CommandResponse {
id: split[1].to_owned(),
data
}),
None => ()
},
Err(e) => ctx.text(e)
}
Is there a better way of doing this instead?
Note: Payload can be any type, either a String or a tuple of arbitrary length e.g. (i32, i32) or (String, i32, i32) etc.
Consider approaching this from the type level. A Command
can be parsed from some string input and knows how to execute some action and return its result.
use std::str::FromStr;
pub enum CommandError {
BadlyFormattedCommand,
CouldNotFindCommand,
NotEnoughArguments,
}
pub enum Commands {
SetClipboard(String),
MouseMoveRel(i32, i32),
MouseMoveAbs(i32, i32),
}
impl FromStr for Commands {
type Err = CommandError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split("|").collect::<Vec<&str>>().as_slice() {
["1", value] => Ok(Commands::SetClipboard(value.to_string())),
["1", ..] => Err(CommandError::NotEnoughArguments),
["2", x, y] => {
let x: i32 = x.parse().map_err(|_| CommandError::BadlyFormattedCommand)?;
let y: i32 = y.parse().map_err(|_| CommandError::BadlyFormattedCommand)?;
Ok(Commands::MouseMoveRel(x, y))
}
["2", ..] => Err(CommandError::NotEnoughArguments),
["3", x, y] => {
let x: i32 = x.parse().map_err(|_| CommandError::BadlyFormattedCommand)?;
let y: i32 = y.parse().map_err(|_| CommandError::BadlyFormattedCommand)?;
Ok(Commands::MouseMoveAbs(x, y))
}
["3", ..] => Err(CommandError::NotEnoughArguments),
_ => Err(CommandError::CouldNotFindCommand),
}
}
}
pub struct CommandResponse; // I won't define this, but maybe this is actually an enum?
impl Commands {
pub fn execute(&self) -> Result<Option<CommandResponse>, CommandError> {
match self {
Self::SetClipboard(s) => {
// do that thing
Ok(None)
}
Self::MouseMoveRel(x, y) => {
// do that thing
Ok(Some(CommandResponse)) // Maybe this is the new location?
}
Self::MouseMoveAbs(x, y) => {
// do THAT thing
Ok(Some(CommandResponse))
}
}
}
}
Then using the code is:
def run_command(s: &str) -> Result<Option<CommandResult>, ApplicationError> {
// Parse the command
command: Command = s.parse().map_err(|_| ApplicationError)?;
// and return the result of its execution
command.execute().map_err(|_| ApplicationError)
}