SimpleDB Exercise
In this exercise, we will implement a toy protocol parser for a simple protocol for databank queries. We call it simpleDB. The protocol has two commands, one of them can be sent with a payload of additional data. Your parser parses the incoming data strings, makes sure the commands are formatted correctly and returns errors for the different ways the formatting can go wrong.
After completing this exercise you will be able to
-
write a simple Rust library from scratch
-
interact with borrowed and owned memory, especially how to take ownership
-
handle complex cases using the
match
andif let
syntax -
create a safe protocol parser in Rust manually
Prerequisites
-
basic pattern matching with
match
-
control flow with if/else
-
familiarity with
Result<T, E>
,Option<T>
Tasks
- Create a library project called
simple-db
. - Implement appropriate data structures for
Command
andError
. - Read the documentation for
str
, especiallystrip_prefix()
andstrip_suffix()
. Pay attention to their return type. - Implement the following function so that it implements the protocol specifications to parse the messages. Use the provided tests to help you with the case handling.
pub fn parse(input: &str) -> Result<Command, Error> {
todo!()
}
The Step-by-Step-Solution contains steps 4a-c that explain a possible way to handle the cases in detail.
Optional Tasks:
- Run
clippy
on your codebase. - Run
rustfmt
on your codebase.
If you need it, we have provided solutions for every step for this exercise.
Protocol Specification
The protocol has two commands that are sent as messages in the following form:
-
PUBLISH <payload>\n
-
RETRIEVE\n
With the additional properties:
-
The payload cannot contain newlines.
-
A missing newline at the end of the command is an error.
-
A newline other than at the end of the command is an error.
-
An empty payload is allowed. The command to publish an empty payload is
PUBLISH \n
.
Issues with the format (or other properties) of the messages are handled with the following error codes:
-
UnexpectedNewline
(a newline not at the end of the line) -
IncompleteMessage
(no newline at the end) -
EmptyMessage
(empty string instead of a command) -
UnknownCommand
(string is not empty, but neitherPUBLISH
norRECEIVE
) -
UnexpectedPayload
(message contains a payload, when it should not) -
MissingPayload
(message is missing a payload)
Testing
Below are the tests your protocol parser needs to pass. You can copy them to the bottom of your lib.rs
.
#[cfg(test)]
mod tests {
use super::*;
// Tests placement of \n
#[test]
fn test_missing_nl() {
let line = "RETRIEVE";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::IncompleteMessage);
assert_eq!(result, expected);
}
#[test]
fn test_trailing_data() {
let line = "PUBLISH The message\n is wrong \n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::UnexpectedNewline);
assert_eq!(result, expected);
}
#[test]
fn test_empty_string() {
let line = "";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::IncompleteMessage);
assert_eq!(result, expected);
}
// Tests for empty messages and unknown commands
#[test]
fn test_only_nl() {
let line = "\n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::EmptyMessage);
assert_eq!(result, expected);
}
#[test]
fn test_unknown_command() {
let line = "SERVE \n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::UnknownCommand);
assert_eq!(result, expected);
}
// Tests correct formatting of RETRIEVE command
#[test]
fn test_retrieve_w_whitespace() {
let line = "RETRIEVE \n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::UnexpectedPayload);
assert_eq!(result, expected);
}
#[test]
fn test_retrieve_payload() {
let line = "RETRIEVE this has a payload\n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::UnexpectedPayload);
assert_eq!(result, expected);
}
#[test]
fn test_retrieve() {
let line = "RETRIEVE\n";
let result: Result<Command, Error> = parse(line);
let expected = Ok(Command::Retrieve);
assert_eq!(result, expected);
}
// Tests correct formatting of PUBLISH command
#[test]
fn test_publish() {
let line = "PUBLISH TestMessage\n";
let result: Result<Command, Error> = parse(line);
let expected = Ok(Command::Publish("TestMessage".into()));
assert_eq!(result, expected);
}
#[test]
fn test_empty_publish() {
let line = "PUBLISH \n";
let result: Result<Command, Error> = parse(line);
let expected = Ok(Command::Publish("".into()));
assert_eq!(result, expected);
}
#[test]
fn test_missing_payload() {
let line = "PUBLISH\n";
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::MissingPayload);
assert_eq!(result, expected);
}
}