๐ŸŽ“ A1Learner
๐Ÿ”

Rust Tokio Commands

A command is one of a small set of named requests, and each request has named fields. Parsing turns a frame into a command.

Frames arrive as words

An Array frame of bulk strings is, in plain terms, a list of words:

// the client sent:  set name Ferris
let words = ["set", "name", "Ferris"];

The commands are an enum

enum Command {
    Get { key: String },
    Set { key: String, value: String },
    Unknown(String),
}

Name the command

from_frame in the tutorial does this job on a real Frame. Here the same match works on words:

match words[0].to_ascii_lowercase().as_str() {
    "get" if words.len() == 2 => Command::Get { key: words[1].into() },
    "set" if words.len() == 3 => Command::Set {
        key: words[1].into(),
        value: words[2].into(),
    },
    _ => Command::Unknown(words[0].into()),
}

Run a Set

Command::Set { key, value } => {
    db.insert(key.clone(), value.clone());
    "OK".to_string()
}

Run a Get โ€” missing is not an error

Command::Get { key } => match db.get(key) {
    Some(value) => value.clone(),
    None => "(nil)".to_string(), // Redis answers with a Null frame
},

Commands can carry a deadline

A SET that expires adds one field โ€” and the server keeps a background task to enforce it:

Set { key: String, value: Bytes, expire: Option<Duration> }
// The background task sleeps until the earliest deadline,
// then removes every key whose time is up.

Example

use std::collections::HashMap;

#[derive(Debug)]
enum Command {
    Get { key: String },
    Set { key: String, value: String },
    Unknown(String),
}

impl Command {
    fn from_words(words: &[&str]) -> Command {
        if words.is_empty() {
            return Command::Unknown(String::new());
        }
        match words[0].to_ascii_lowercase().as_str() {
            "get" if words.len() == 2 => Command::Get { key: words[1].to_string() },
            "set" if words.len() == 3 => Command::Set {
                key: words[1].to_string(),
                value: words[2].to_string(),
            },
            _ => Command::Unknown(words[0].to_string()),
        }
    }

    fn apply(&self, db: &mut HashMap<String, String>) -> String {
        match self {
            Command::Set { key, value } => {
                db.insert(key.clone(), value.clone());
                "OK".to_string()
            }
            Command::Get { key } => match db.get(key) {
                Some(value) => value.clone(),
                None => "(nil)".to_string(),
            },
            Command::Unknown(name) => format!("error: unknown command '{}'", name),
        }
    }
}

fn main() {
    let mut db: HashMap<String, String> = HashMap::new();
    let script: Vec<Vec<&str>> = vec![
        vec!["set", "name", "Ferris"],
        vec!["get", "name"],
        vec!["get", "city"],
        vec!["dance"],
    ];
    for words in &script {
        let cmd = Command::from_words(words);
        println!("{}", cmd.apply(&mut db));
    }
}

Key points

  • Command parsing and command running are separate steps.
  • from_frame only names the request.
  • apply does the work.
  • So parsing can be tested without a network.
  • Each command carries its own fields.
  • Get holds a key.
  • Set holds a key, a value, and an optional expiry.
  • Matching is forgiving about case โ€” "GET" and "get" are the same command โ€” but strict about the number of words
  • An unrecognised first word is not a crash: it becomes an Unknown command, answered with an error frame
  • A missing key is a normal answer, not an error.
  • Redis sends a Null frame for it.
  • Errors are for bad requests, not empty results.
  • SET with an expiry stores a deadline beside the value.
  • One background task removes expired keys.
  • A Notify wakes it when a new earliest deadline appears.
  • In the tutorial, apply writes its own response frame to the connection.
  • A command is a named request with named fields, parsed from a frame.
  • A frame carries data only, with no meaning yet.

In this example, a small script of word lists stands in for frames arriving from a client. from_words names each one. set with three words becomes a Set command. get with two words becomes a Get. dance becomes Unknown instead of crashing anything. The server does not know that word. apply then runs each command against the map. Set stores the pair and answers OK. The first Get finds Ferris. The second Get finds nothing and answers (nil), a normal reply. Unknown answers with an error line. On the real server, each answer string would be written back as a response frame โ€” here println! shows it instead.

Note

Parsing, running, and replying stay three separate decisions.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

A client sends the words ["get"] with no key after it. What should the parser produce?