Search code examples
rusttraits

Storing command structs with "generic" data


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.


Solution

  • 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)
    }