Message
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 ofToDo
s, 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 singleToDo
Update(ToDo)
- This case will replace an existing version in the data store with the includedToDo
Remove(i32)
- This case will remove an item from the data store, it will include a theid
of theToDo
that should be removed-
Error(String)
- This case will represent a server error, it will include a message describing the error
All(Vec<ToDo>)
case we have the associated value of a Vec
(one of rust's array types) of ToDo
s, 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 String
s can be a little tricky to work with, to make our lives easier here we are using something called a trait
. trait
s 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 impl
ements 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.