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 payload
  • PUBLISH <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);
    }
}
}