diff --git a/01-problem/README.md b/01-problem/README.md new file mode 100644 index 0000000..403e6f5 --- /dev/null +++ b/01-problem/README.md @@ -0,0 +1,31 @@ +# The Problem + +We have a `CredentialManager` implementation which can be in two states: **locked** or **unlocked**. + +When the manager is locked, it can be unlocked with the `unlock(...)` method. Now unlocked, the `set(...)` and `get(...)` methods can be performed on the manager. Also, it can be locked again using the `lock()` method. + +```rust +let mut pw = CredentialManager::new(); +pw.unlock("pssst"); +pw.set("foo", "bar"); +let val = pw.get("foo"); +pw.lock(); +``` + +But with the current implementation, on a locked manager, the `get` and `set` could be performed which would result in erroneous results. + +```rust +let mut pw = CredentialManager::new(); +pw.set("foo", "bar"); // -> 💥 +``` + +Also, nothing prevents us from unlocking an already unlocked manager as well as locking an already locked manager. + +```rust +let mut pw = CredentialManager::new(); +pw.unlock("pssst"); +pw.unlock("pssst"); // -> 💥 + +let mut pw = CredentialManager::new(); +pw.lock(); // -> 💥 +``` diff --git a/02-runtime-check/README.md b/02-runtime-check/README.md new file mode 100644 index 0000000..42e163f --- /dev/null +++ b/02-runtime-check/README.md @@ -0,0 +1,18 @@ +# Solution 1: Runtime Checks + +One solution to the problem would be to simply check the locked state and restrict methods to be only callable in the corrent locked state. + +```rust +let mut pw = CredentialManager::new(); +pw.set("foo", "bar")?; // this would bail with an error result +pw.unlock("pssst")?; +pw.unlock("pssst")?; // this would bail with an error result +pw.set("foo", "bar")?; +let val = pw.get("foo")?; +pw.lock()?; +pw.lock()?; // this would bail with an error result +``` + +## Caveats + +Performing runtime checks on the state requires returning `Result` and also requires the check and handling of erroneous results on every call to the manager. diff --git a/03-builder-pattern/README.md b/03-builder-pattern/README.md new file mode 100644 index 0000000..d98679a --- /dev/null +++ b/03-builder-pattern/README.md @@ -0,0 +1,51 @@ +# Solution 2: "Builder Pattern" + +A better solution is to use two different structs for each state with their separate implementations. That ensured on a compiler level that the manager can not be misused in either state it is. + +```rust +let pw = LockedCredentialManager::new(); +pw.set("foo", "bar"); +``` + +This produces the following compiler error: + +``` +error[E0599]: no method named `set` found for struct `LockedCredentialManager` in the current scope + --> 03-builder-pattern\src\main.rs:45:8 + | + 5 | struct LockedCredentialManager { + | ------------------------------ method `set` not found for this struct +... +45 | pw.set("foo", "bar"); + | ^^^ method not found in `LockedCredentialManager` +``` + +And because we pass the struct by reference in the `unlock` and `lock` methods, resulting from Rust's [onwership memory model](https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html), the previous object can not be used after the transformation. And because this is checked at compile time, it makes the double locking and unlocking a non-issue in production. Let's see what the following code results in. + +```rust + let pw = LockedCredentialManager::new(); + let mut pw = pw.unlock("pssst"); + pw.lock(); + pw.lock(); +``` + +This code results in the following compiler error: + +``` +error[E0382]: use of moved value: `pw` + --> 03-builder-pattern\src\main.rs:49:5 + | +45 | let mut pw = pw.unlock("pssst"); + | ------ move occurs because `pw` has type `UnlockedCredentialManager`, which does not implement the `Copy` trait +... +48 | pw.lock(); + | ------ `pw` moved due to this method call +49 | pw.lock(); + | ^^ value used here after move + | +note: `UnlockedCredentialManager::lock` takes ownership of the receiver `self`, which moves `pw` + --> 03-builder-pattern\src\main.rs:36:17 + | +36 | pub fn lock(self) -> LockedCredentialManager { + | ^^^^ +``` diff --git a/04-typestate/README.md b/04-typestate/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.toml b/Cargo.toml index 935cde4..a2c3360 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ [workspace] +resolver = "3" members = ["01-problem", "02-runtime-check", "03-builder-pattern", "04-typestate"] resolver = "3"