Step-by-Step Solution
Step 1: Creating a library project with cargo
Create a new Cargo project, check the build and the test setup:
Solution
cargo new --lib simple-db
cd simple-db
cargo build
cargo test
Step 2: Appropriate data structures
Define two enums, one is called Command
and one is called Error
. Command
has 2 variants for the two possible commands. Publish
carries data (the message), Retrieve
does not. Error
is just a list of error kinds. Use #[derive(Eq,PartialEq,Debug)]
for both enums
.
Solution
#[derive(Eq, PartialEq, Debug)]
pub enum Command {
Publish(String),
Retrieve,
}
#[derive(Eq, PartialEq, Debug)]
pub enum Error {
TrailingData,
IncompleteMessage,
EmptyMessage,
UnknownCommand,
UnexpectedPayload,
MissingPayload,
}
// Tests go here!
Step 3: Read the documentation for str
, especially splitn()
, split_once()
to build your logic
tl;dr
split_once()
splits a str into 2 parts at the first occurrence of a delimiter.splitn()
splits a str into a max of n substrings at every occurrence of a delimiter.
The proposed logic
Split the input with split_once()
using \n
as delimiter, this allows to distinguish 3 cases:
- a command where
\n
is the last part, and the second substring is""
-> some kind of command - a command with trailing data (i.e. data after a newline) -> Error::TrailingData
- a command with no
\n
-> Error::IncompleteMessage
After that, split the input with splitn()
using ' '
as delimiter and 2 as the max number of substrings. The method an iterator over the substrings, and the iterator produces Some(...)
, or None
when there are no substrings. Note, that even an empty str ""
is a substring.
From here, the actual command cases need to be distinguished with pattern matching:
RETRIEVE
has no whitespace and no payloadPUBLISH <payload>
has always whitespace and an optional payload
Step 4: Implement fn parse()
Step 4a: Sorting out wrongly placed and absent newlines
Missing, wrongly placed and more than one \n
are errors that occur independent of other errors so it makes sense to handle these cases first. Split the incoming message at the first appearing \n
using split_once()
. This operation yields Some((&str, &str))
if at least one \n
is present, and None
if 0 are present. If the \n
is not the last item in the message, the second &str
in Some((&str, &str))
is not ""
.
Tip: Introduce a generic variant Command::Command
that temporarily stands for a valid command.
Handle the two cases with match, check if the second part is ""
. Return Err(Error::TrailingData)
or for wrongly placed \n
, Err(Error::IncompleteMessage)
for absent \n
and Ok(Command::Command)
if the \n
is placed correct.
Solution
pub fn parse(input: &str) -> Result<Command, Error> {
match input.split_once('\n') {
Some((_message, "")) => Ok(Command::Command),
Some(_) => return Err(Error::TrailingData),
None => return Err(Error::IncompleteMessage),
}
}
Step 4b: if let
: sorting Some()
from None
In 4a, we produce a Ok(Command::Command)
if the newlines all check out. Instead of doing that, we want to capture the message - that is the input, without the newline on the end, and we know it has no newlines within it.
Use .splitn()
to split the message
into 2 parts maximum, use a space as delimiter (' '
). This method yields an iterator over the substrings.
Use .next()
to access the first substring, which is the command keyword. You will always get Some(value)
- the splitn
method never returns None
the first time around. We can unwrap this first value because splitn
always returns at least one string - but add yourself a comment to remind yourself why this unwrap()
is never going to fail!
Solution
pub fn parse(input: &str) -> Result<Command, Error> {
let message = match input.split_once('\n') {
Some((message, "")) => message,
Some(_) => return Err(Error::TrailingData),
None => return Err(Error::IncompleteMessage),
};
let mut substrings = message.splitn(2, ' ');
let _command = substrings.next().unwrap();
Ok(Command::Command)
}
Step 4c: Pattern matching for the command keywords
Remove the Ok(Command::Command)
and the enum variant. Use match
to pattern match the command instead. Next, implement two necessary match arms: ""
for empty messages, _
for any other string, currently evaluated to be an unknown command.
Solution
pub fn parse(input: &str) -> Result<Command, Error> {
let message = match input.split_once('\n') {
Some((message, "")) => message,
Some(_) => return Err(Error::TrailingData),
None => return Err(Error::IncompleteMessage),
};
let mut substrings = message.splitn(2, ' ');
// Note: `splitn` *always* returns at least one value
let command = substrings.next().unwrap();
match command {
"" => Err(Error::EmptyMessage),
_ => Err(Error::UnknownCommand),
}
}
Step 4d: Add Retrieve Case
Add a match arm to check if the command substring is equal to "RETRIEVE"
. It’s not enough to return Ok(Command::Retrieve)
just yet. The Retrieve command cannot have a payload, this includes whitespace! To check for this, add an if else statement, that checks if the next iteration over the substrings returns None
. If this is true, return the Ok(Command::Retrieve)
, if it is false, return Err(Error::UnexpectedPayload)
.
Solution
pub fn parse(input: &str) -> Result<Command, Error> {
let message = match input.split_once('\n') {
Some((message, "")) => message,
Some(_) => return Err(Error::TrailingData),
None => return Err(Error::IncompleteMessage),
};
let mut substrings = message.splitn(2, ' ');
// Note: `splitn` *always* returns at least one value
let command = substrings.next().unwrap();
match command {
"RETRIEVE" => {
if substrings.next().is_none() {
Ok(Command::Retrieve)
} else {
Err(Error::UnexpectedPayload)
}
}
"" => Err(Error::EmptyMessage),
_ => Err(Error::UnknownCommand),
}
}
Step 4e: Add Publish Case and finish
Add a match
arm to check if the command substring is equal to "PUBLISH"
. Just like with the Retrieve command, we need to add a distinction, but the other way round: Publish needs a payload or whitespace for an empty payload to be valid.
Use if let
to check if the next iteration into the substrings returns Some()
. If it does, return Ok(Command::Publish(payload))
, where payload
is an owned version (a String
) of the trimmed payload. Otherwise return Err(Error::MissingPayload)
.
Solution
pub fn parse(input: &str) -> Result<Command, Error> {
let message = match input.split_once('\n') {
Some((message, "")) => message,
Some(_) => return Err(Error::TrailingData),
None => return Err(Error::IncompleteMessage),
};
let mut substrings = message.splitn(2, ' ');
// Note: `splitn` *always* returns at least one value
let command = substrings.next().unwrap();
match command {
"RETRIEVE" => {
if substrings.next().is_none() {
Ok(Command::Retrieve)
} else {
Err(Error::UnexpectedPayload)
}
}
"PUBLISH" => {
if let Some(payload) = substrings.next() {
Ok(Command::Publish(String::from(payload)))
} else {
Err(Error::MissingPayload)
}
}
"" => Err(Error::EmptyMessage),
_ => Err(Error::UnknownCommand),
}
}
Full source code
If all else fails, feel free to copy this solution to play around with it.
Solution
#![allow(unused)] fn main() { #[derive(Eq, PartialEq, Debug)] pub enum Command { Publish(String), Retrieve, } #[derive(Eq, PartialEq, Debug)] pub enum Error { TrailingData, IncompleteMessage, EmptyMessage, UnknownCommand, UnexpectedPayload, MissingPayload, } pub fn parse(input: &str) -> Result<Command, Error> { let message = match input.split_once('\n') { Some((message, "")) => message, Some(_) => return Err(Error::TrailingData), None => return Err(Error::IncompleteMessage), }; let mut substrings = message.splitn(2, ' '); // Note: `splitn` *always* returns at least one value let command = substrings.next().unwrap(); match command { "RETRIEVE" => { if substrings.next().is_none() { Ok(Command::Retrieve) } else { Err(Error::UnexpectedPayload) } } "PUBLISH" => { if let Some(payload) = substrings.next() { Ok(Command::Publish(String::from(payload))) } else { Err(Error::MissingPayload) } } "" => Err(Error::EmptyMessage), _ => Err(Error::UnknownCommand), } } #[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::TrailingData); assert_eq!(result, expected); } #[test] fn test_empty_string() { let line = ""; let result = 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); } } }