Skip to content

Commit

Permalink
Simplify the SimpleDB solutions.
Browse files Browse the repository at this point in the history
Now you don't need to know what an iterator is - you can just strip suffixes and prefixes, and use 'contains' to find a single character.
  • Loading branch information
jonathanpallant committed Jul 17, 2024
1 parent bd27e17 commit 0002a03
Show file tree
Hide file tree
Showing 16 changed files with 249 additions and 523 deletions.
8 changes: 4 additions & 4 deletions exercise-book/src/self-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ With the additional properties:
Violations against the form of the messages and the properties are
handled with the following error codes:

- `TrailingData` (bytes found after newline)
- `UnexpectedNewline` (a newline not at the end of the line)

- `IncompleteMessage` (no newline)
- `IncompleteMessage` (no newline at the end)

- `EmptyMessage` (empty string instead of a command)

Expand Down Expand Up @@ -113,14 +113,14 @@ mod tests {
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);
let expected = Err(Error::UnexpectedNewline);
assert_eq!(result, expected);
}

#[test]
fn test_empty_string() {
let line = "";
let result = parse(line);
let result: Result<Command, Error> = parse(line);
let expected = Err(Error::IncompleteMessage);
assert_eq!(result, expected);
}
Expand Down
12 changes: 5 additions & 7 deletions exercise-book/src/simple-db-knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,22 @@ In general, we also recommend to use the Rust documentation to figure out things

`#[derive(PartialEq, Eq)]`

This enables comparison between 2 instances of the type, by comparing every field/variant. This enables the `assert_eq!` macro, which relies on equality being defined. `Eq` for total equality isn’t strictly necessary for this example, but it is good practice to derive it if it applies.
This enables comparison between 2 instances of the type, by comparing every field/variant. This enables the `assert_eq!` macro, which relies on equality being defined. `Eq` for total equality isn’t strictly necessary for this example, but it is good practice to derive it if it applies.

`#[derive(Debug)]`

This enables automatic debug output for the type. The `assert_eq!`macro requires this for testing.

This enables the automatic generation of a debug formatting function for the type. The `assert_eq!` macro requires this for testing.

## Control flow and pattern matching, returning values

This exercise involves handling a number of cases. You are already familiar with `if/else` and a basic form of `match`. Here, we’ll introduce you to `if let`.

```rust, ignore
if let Some(payload) = substrings.next() {
// execute if the above statement is true
}
if let Some(message) = message.strip_prefix('\n') {
// executes if the above pattern is a match
}
```


### When to use what?

`if let` is like a pattern-matching `match` block with only one arm. So, if your `match` only has one arm of interest, consider an `if let` instead.
Expand Down
85 changes: 30 additions & 55 deletions exercise-book/src/simple-db-solution.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,103 +29,79 @@ Define two enums, one is called `Command` and one is called `Error`. `Command` h

</details>

## Step 3: Read the documentation for `str`, especially `splitn()`, `split_once()` to build your logic
## Step 3: Read the documentation for `str`, especially `strip_prefix()`, `strip_suffix()`

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.
tl;dr:

<details>
<summary>The proposed logic</summary>
* `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`.

Split the input with `split_once()` using `\n` as delimiter, this allows to distinguish 3 cases:
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).

- 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
<details>
<summary>The proposed logic</summary>

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.
1. Check if the string ends with the char `'\n'` - if so, keep the rest of it, otherwise return an error.

From here, the actual command cases need to be distinguished with pattern matching:
2. Check if the remainder still contains a `'\n'` - if so, return an error.

- `RETRIEVE` has no whitespace and no payload
- `PUBLISH <payload>` has always whitespace and an optional payload
3. Check if the remainder is empty - if so, return an error.

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

## Step 4: Implement `fn parse()`
5. Check if the remainder is `"PUBLISH"` - if so, return an error because the mandatory payload is missing.

### Step 4a: Sorting out wrongly placed and absent newlines
6. Check if the remainder begins with `"RETRIEVE "` - if so, return an error because that command should not have anything after it.

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 `""`.
7. Check if the remainder is `"RETRIEVE"` - if so, return `Ok(Command::Retrieve)`

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.

<details>
<summary>Solution</summary>

```rust, ignore
{{#include ../../exercise-solutions/simple-db/step4a/src/lib.rs:18:24}}
```
8. Otherwise, it's return an unknown command error.

</details>

### 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!

<details>
<summary>Solution</summary>
## Step 4: Implement `fn parse()`

```rust, ignore
{{#include ../../exercise-solutions/simple-db/step4b/src/lib.rs:18:30}}
```
### Step 4a: Sorting out wrongly placed and absent newlines

</details>
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.

### Step 4c: Pattern matching for the command keywords
Now look for newlines within the remainder using the `contains()` method and if you find any, that's an error.

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.
Tip: Introduce a generic variant `Command::Command` that temporarily stands for a valid command.

<details>
<summary>Solution</summary>

```rust, ignore
{{#include ../../exercise-solutions/simple-db/step4c/src/lib.rs:17:32}}
{{#include ../../exercise-solutions/simple-db/step4a/src/lib.rs:18:27}}
```

</details>

### Step 4d: Add Retrieve Case
### Step 4b: Looking for "RETRIEVE"

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)`.
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.

<details>
<summary>Solution</summary>

```rust, ignore
{{#include ../../exercise-solutions/simple-db/step4d/src/lib.rs:17:39}}
{{#include ../../exercise-solutions/simple-db/step4b/src/lib.rs:18:34}}
```

</details>

### 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.
### Step 4c: Looking for "PUBLISH"

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)`.
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.

<details>
<summary>Solution</summary>

```rust, ignore
{{#include ../../exercise-solutions/simple-db/step4e/src/lib.rs:17:46}}
{{#include ../../exercise-solutions/simple-db/step4c/src/lib.rs:18:38}}
```

</details>
Expand All @@ -138,8 +114,7 @@ If all else fails, feel free to copy this solution to play around with it.
<summary>Solution</summary>

```rust
{{#include ../../exercise-solutions/simple-db/step4e/src/lib.rs}}
{{#include ../../exercise-solutions/simple-db/step4c/src/lib.rs}}
```

</details>

110 changes: 6 additions & 104 deletions exercise-book/src/simple-db.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ In this exercise, we will implement a toy protocol parser for a simple protocol

- create a safe protocol parser in Rust manually


## Prerequisites

- basic pattern matching with `match`
Expand All @@ -25,7 +24,7 @@ In this exercise, we will implement a toy protocol parser for a simple protocol

1. Create a library project called `simple-db`.
2. Implement appropriate data structures for `Command` and `Error`.
3. Read the documentation for [`str`](https://doc.rust-lang.org/std/primitive.str.html), especially [`split_once()`](https://doc.rust-lang.org/std/primitive.str.html#method.split_once) and [`splitn()`](https://doc.rust-lang.org/std/primitive.str.html#method.splitn). Pay attention to their return type. Use the result value of `split_once()` and `splitn()` to guide your logic. The Step-by-Step-Solution contains a proposal.
3. Read the documentation for [`str`](https://doc.rust-lang.org/std/primitive.str.html), especially [`strip_prefix()`](https://doc.rust-lang.org/std/primitive.str.html#method.strip_prefix) and [`strip_suffix()`](https://doc.rust-lang.org/std/primitive.str.html#method.strip_suffix). Pay attention to their return type.
4. 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.

```rust, ignore
Expand All @@ -34,7 +33,7 @@ pub fn parse(input: &str) -> Result<Command, Error> {
}
```

The Step-by-Step-Solution contains steps 4a-e that explain a possible way to handle the cases in detail.
The Step-by-Step-Solution contains steps 4a-c that explain a possible way to handle the cases in detail.

### Optional Tasks:

Expand All @@ -58,17 +57,17 @@ With the additional properties:

2. A missing newline at the end of the command is an error.

3. Data after the first newline is an error.
3. A newline other than at the end of the command is an error.

4. Empty payloads are allowed. In this case, the command is
`PUBLISH \n`.

Violations against the form of the messages and the properties are
handled with the following error codes:

- `TrailingData` (bytes found after newline)
- `UnexpectedNewline` (a newline not at the end of the line)

- `IncompleteMessage` (no newline)
- `IncompleteMessage` (no newline at the end)

- `EmptyMessage` (empty string instead of a command)

Expand All @@ -84,102 +83,5 @@ handled with the following error codes:
Below are the tests your protocol parser needs to pass. You can copy them to the bottom of your `lib.rs`.

```rust, ignore
#[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);
}
}
{{#include ../../exercise-solutions/simple-db/step4c/src/lib.rs:41:138}}
```
Loading

0 comments on commit 0002a03

Please sign in to comment.