Skip to content

Commit

Permalink
Merge pull request #105 from ferrous-systems/update-simpledb
Browse files Browse the repository at this point in the history
Update SimpleDB
  • Loading branch information
jonathanpallant committed Jul 17, 2024
2 parents bd27e17 + fd41e28 commit 4ce3c30
Show file tree
Hide file tree
Showing 18 changed files with 261 additions and 529 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ jobs:
- name: Install targets
run: |
rustup target add thumbv7em-none-eabihf
rustup toolchain add beta
rustup component add rust-src --toolchain beta
rustup component add rust-src
- name: Find slug name
run: |
Expand Down
4 changes: 2 additions & 2 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ popd
popd
pushd qemu-code
pushd uart-driver
# Build in beta because armv8r-none-eabihf isn't in stable 1.77
RUSTC_BOOTSTRAP=1 cargo +beta build -Zbuild-std=core
# Build from source because armv8r-none-eabihf isn't Tier 2
RUSTC_BOOTSTRAP=1 cargo build -Zbuild-std=core
popd
popd
pushd nrf52-code
Expand Down
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
23 changes: 14 additions & 9 deletions exercise-book/src/simple-db-knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,32 @@ 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`.
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`, and `let else`:

```rust, ignore
if let Some(payload) = substrings.next() {
// execute if the above statement is true
}
if let Some(message) = message.strip_prefix("PREFIX:") {
// Executes if the above pattern is a match.
}
// The variable `message` is NOT available here.
let Some(message) = message.strip_prefix("PREFIX:") else {
// Executes if the above pattern is NOT a match.
// Must have an early return in this block.
}
// The variable `message` is still available here.
```


### 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.
`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` or `let else` instead (depending on whether the pattern match means success, or the pattern match means there's an error).

`match` can be used to handle more fine grained and complex pattern matching, especially when there are several, equally ranked possibilities. The match arms may have to include a catch all `_ =>` arm, for every possible case that is not explicitly spelled out. The order of the match arms matter: The catch all branch needs to be last, otherwise, it catches all…

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, 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 4ce3c30

Please sign in to comment.