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: Define 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 {
    UnexpectedNewline,
    IncompleteMessage,
    EmptyMessage,
    UnknownCommand,
    UnexpectedPayload,
    MissingPayload,
}

// Tests go here!

Step 3: Read the documentation for str, especially strip_prefix(), strip_suffix()

tl;dr:

  • message.strip_prefix("FOO ") returns Some(remainder) if the string slice message starts with "FOO ", otherwise you get None
  • message.strip_suffix('\n') returns Some(remainder) if the string slice message ends with '\n', otherwise you get None.

Note that both functions will take either a string slice, or a character, or will actually even take a function that returns a boolean to tell you whether a character matches or not (we won't use that though).

The proposed logic
  1. Check if the string ends with the char '\n' - if so, keep the rest of it, otherwise return an error.

  2. Check if the remainder still contains a '\n' - if so, return an error.

  3. Check if the remainder is empty - if so, return an error.

  4. Check if the remainder begins with "PUBLISH " - if so, return Ok(Command::Publish(...)) with the payload upconverted to a String

  5. Check if the remainder is "PUBLISH" - if so, return an error because the mandatory payload is missing.

  6. Check if the remainder begins with "RETRIEVE " - if so, return an error because that command should not have anything after it.

  7. Check if the remainder is "RETRIEVE" - if so, return Ok(Command::Retrieve)

  8. Otherwise, return an unknown command error.

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. Check the string has a newline at the end with strip_suffix. If not, that's an Error::IncompleteMessage. We can assume the pattern will match (that strip_suffix will return Some(...), which is our so-called sunny day scenario) so a let - else makes most sense here - although a match will also work.

Now look for newlines within the remainder using the contains() method and if you find any, that's an error.

Tip: Introduce a generic variant Command::Command that temporarily stands for a valid command.

Solution
pub fn parse(input: &str) -> Result<Command, Error> {
    let Some(message) = input.strip_suffix('\n') else {
        return Err(Error::IncompleteMessage);
    };

    if message.contains('\n') {
        return Err(Error::UnexpectedNewline);
    }

    Ok(Command::Command)

Step 4b: Looking for "RETRIEVE"

In 4a, we produce a Ok(Command::Command) if the newlines all check out. Now we want to look for a RETRIEVE command.

If the string is empty, that's an error. If the string is exactly "RETRIEVE", that's our command. Otherwise the string starts with "RETRIEVE ", then that's an UnexpectedPayload error.

Solution
    let Some(message) = input.strip_suffix('\n') else {
        return Err(Error::IncompleteMessage);
    };

    if message.contains('\n') {
        return Err(Error::UnexpectedNewline);
    }

    if message == "RETRIEVE" {
        Ok(Command::Retrieve)
    } else if let Some(_payload) = message.strip_prefix("RETRIEVE ") {
        Err(Error::UnexpectedPayload)
    } else if message == "" {
        Err(Error::EmptyMessage)
    } else {
        Err(Error::UnknownCommand)
    }

Step 4c: Looking for "PUBLISH"

Now we want to see if the message starts with "PUBLISH ", and if so, return a Command::Publish containing the payload, but converted to a heap-allocted String so that ownership is passed back to the caller. If not, and the message is equal to "PUBLISH", then that's a MissingPayload error.

Solution
    let Some(message) = input.strip_suffix('\n') else {
        return Err(Error::IncompleteMessage);
    };

    if message.contains('\n') {
        return Err(Error::UnexpectedNewline);
    }

    if let Some(payload) = message.strip_prefix("PUBLISH ") {
        Ok(Command::Publish(String::from(payload)))
    } else if message == "PUBLISH" {
        Err(Error::MissingPayload)
    } else if message == "RETRIEVE" {
        Ok(Command::Retrieve)
    } else if let Some(_payload) = message.strip_prefix("RETRIEVE ") {
        Err(Error::UnexpectedPayload)
    } else if message == "" {
        Err(Error::EmptyMessage)
    } else {
        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 {
    UnexpectedNewline,
    IncompleteMessage,
    EmptyMessage,
    UnknownCommand,
    UnexpectedPayload,
    MissingPayload,
}

pub fn parse(input: &str) -> Result<Command, Error> {
    let Some(message) = input.strip_suffix('\n') else {
        return Err(Error::IncompleteMessage);
    };

    if message.contains('\n') {
        return Err(Error::UnexpectedNewline);
    }

    if let Some(payload) = message.strip_prefix("PUBLISH ") {
        Ok(Command::Publish(String::from(payload)))
    } else if message == "PUBLISH" {
        Err(Error::MissingPayload)
    } else if message == "RETRIEVE" {
        Ok(Command::Retrieve)
    } else if let Some(_payload) = message.strip_prefix("RETRIEVE ") {
        Err(Error::UnexpectedPayload)
    } else if message == "" {
        Err(Error::EmptyMessage)
    } else {
        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::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);
    }
}
}