Message is an enum, describing the 6 different kinds of messages that we will be sending to/from the client.
pub enum Message {
    GetAll,
    All(Vec<ToDo>),
    Add(ToDo),
    Update(ToDo),
    Remove(i32),
    Error(String),
}
  • GetAll - This case will be the client requesting all of the items in a to do list.
  • All(Vec<ToDo>) - This case will be the server's reply with a list of ToDos, all server responses will be either this or the Error case
  • Add(ToDo) - This case will be used to add a new item to the data store, it will include a single ToDo
  • Update(ToDo) - This case will replace an existing version in the data store with the included ToDo
  • Remove(i32) - This case will remove an item from the data store, it will include a the id of the ToDo that should be removed
  • Error(String) - This case will represent a server error, it will include a message describing the error
Here you can see how we have defined the associated values that I referred to on a previous page. In our All(Vec<ToDo>) case we have the associated value of a Vec (one of rust's array types) of ToDos, you can define any number of these values by just putting them in parentheses. This pairing of information becomes super useful when you are evaluating the data later, below is an example using rust's version of a switch statement, called match. In a match each case will be a new line with a possible option followed by a fat arrow (=>) and then a code block wrapped in curly braces followed by a comma.

Enum example

fn todo_route(message: Message) {
    match message {
        Message::GetAll => {
            //get all of the messages from the data store
            DataStore::get_all();
        },
        Message::All(todos) => {
            //todos is now a variable that we can use
            println!("ToDos: {:?}", todos);
        },
        Message::Add(todo) => {
            //add the variable todo to data store
            DataStore::add(todo);
        },
        Message::Update(todo) => {
            //use the variable todo to update the data store
            DataStore::Update(todo);
        },
        Message::Remove(id) => {
            //use the variable id to remove a ToDo from the data store
            DataStore::Remove(id);
        },
        Message::Error(msg) => {
            //print the error to the console
            println!("Error in todo_route. {}", msg);
        }
    }
}

Just like in a switch statement, each case will have its own block inside of that block we have access to the variable we named in the parentheses. The variable's type will be what was defined in the enum block. As you can see this can be a very powerful tool. One thing that Rust requires is that match statements are always "exhaustive", if you only care about some of the possible options you can use the _ case as a catch all, you will see this in action later.

We also have an impl block for Message.

Message’s functions

impl Message {
    pub fn for_error(data: impl Into<String>) -> Message {
        Message::Error(data.into())
    }

    pub fn to_bytes(self) -> Vec<u8> {
        serialize(&self).unwrap_or(vec!())
    }

    pub fn from_bytes(bytes: Vec<u8>) -> Result<Message, String> {
        match deserialize(&bytes) {
            Ok(msg) => Ok(msg),
            Err(e) => Err(format!("{:?}", e))
        }
    }
}

Here we have 3 functions, the first is a special constructor to make building the Error case a little easier. Rust's Strings can be a little tricky to work with, to make our lives easier here we are using something called a trait. traits are a way to define a specific behavior, we can use them to allow for more flexibility in the type system. If you are familiar with Interfaces or Protocols in another language, Traits as a similar concept. The argument to our constructor is one that implements a trait called Into, the goal of this trait is to convert the current type into another type, the target type is whatever we put in the angle brackets which would be String in this situation. In the body of the constructor you can see we are calling the method that this trait defines, into(), this means that the argument can accept anything that can be converted into a String.

Next we have our first instance method, the only argument to this function is self, this is a special value representing the current instance, kind of like self in Python and Swift or this in javascript, C#, and many other languages. to_bytes is a convenience method for serializing a Message into Bincode.

lastly we have another special constructor, this one takes in some bytes and attempts to deserialize them as Bincode into a Message, notice that this function returns Result<Message, String>. Result is an enum provided by rust's standard library that looks like this.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

This is the primary way to indicate that a function might fail. Any Result could be one of two options, if everything went well it will be Ok(T), if it failed it will be Err(E). If you are not familiar with "generic" notation like this, we are using the letters T and E as place holders for actual types which will be defined when used, this is a very useful tool when working in a strongly typed language like Rust. Since deserialization can fail we need to tell anyone using our function about this possible failure and Result does just that. Anyone using this special constructor is going to need to use a match statement to get either the Message if everything is ok or an explanation of why it failed to deserialize. We will see this in action shortly.