You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-10-09 10:55:26 +02:00
add typestate pattern chapter for idiomatic rust (#2821)
This commit is contained in:
committed by
GitHub
parent
a9497bdc61
commit
63e50a8058
@@ -437,6 +437,14 @@
|
||||
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
|
||||
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
|
||||
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
|
||||
- [Typestate Pattern](idiomatic/leveraging-the-type-system/typestate-pattern.md)
|
||||
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.md)
|
||||
- [Beyond Simple Typestate](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md)
|
||||
- [Typestate Pattern with Generics](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md)
|
||||
- [Serializer: implement Root](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/root.md)
|
||||
- [Serializer: implement Struct](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/struct.md)
|
||||
- [Serializer: implement Property](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/property.md)
|
||||
- [Serializer: Complete implementation](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/complete.md)
|
||||
|
||||
---
|
||||
|
||||
|
@@ -0,0 +1,75 @@
|
||||
---
|
||||
minutes: 30
|
||||
---
|
||||
|
||||
## Typestate Pattern: Problem
|
||||
|
||||
How can we ensure that only valid operations are allowed on a value based on its
|
||||
current state?
|
||||
|
||||
```rust,editable
|
||||
use std::fmt::Write as _;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Serializer {
|
||||
output: String,
|
||||
}
|
||||
|
||||
impl Serializer {
|
||||
fn serialize_struct_start(&mut self, name: &str) {
|
||||
let _ = writeln!(&mut self.output, "{name} {{");
|
||||
}
|
||||
|
||||
fn serialize_struct_field(&mut self, key: &str, value: &str) {
|
||||
let _ = writeln!(&mut self.output, " {key}={value};");
|
||||
}
|
||||
|
||||
fn serialize_struct_end(&mut self) {
|
||||
self.output.push_str("}\n");
|
||||
}
|
||||
|
||||
fn finish(self) -> String {
|
||||
self.output
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut serializer = Serializer::default();
|
||||
serializer.serialize_struct_start("User");
|
||||
serializer.serialize_struct_field("id", "42");
|
||||
serializer.serialize_struct_field("name", "Alice");
|
||||
|
||||
// serializer.serialize_struct_end(); // ← Oops! Forgotten
|
||||
|
||||
println!("{}", serializer.finish());
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- This `Serializer` is meant to write a structured value.
|
||||
|
||||
- However, in this example we forgot to call `serialize_struct_end()` before
|
||||
`finish()`. As a result, the serialized output is incomplete or syntactically
|
||||
incorrect.
|
||||
|
||||
- One approach to fix this would be to track internal state manually, and return
|
||||
a `Result` from methods like `serialize_struct_field()` or `finish()` if the
|
||||
current state is invalid.
|
||||
|
||||
- But this has downsides:
|
||||
|
||||
- It is easy to get wrong as an implementer. Rust’s type system cannot help
|
||||
enforce the correctness of our state transitions.
|
||||
|
||||
- It also adds unnecessary burden on the user, who must handle `Result` values
|
||||
for operations that are misused in source code rather than at runtime.
|
||||
|
||||
- A better solution is to model the valid state transitions directly in the type
|
||||
system.
|
||||
|
||||
In the next slide, we will apply the **typestate pattern** to enforce correct
|
||||
usage at compile time and make it impossible to call incompatible methods or
|
||||
forget to do a required action.
|
||||
|
||||
</details>
|
@@ -0,0 +1,94 @@
|
||||
## Beyond Simple Typestate
|
||||
|
||||
How do we manage increasingly complex configuration flows with many possible
|
||||
states and transitions, while still preventing incompatible operations?
|
||||
|
||||
```rust
|
||||
struct Serializer {/* [...] */}
|
||||
struct SerializeStruct {/* [...] */}
|
||||
struct SerializeStructProperty {/* [...] */}
|
||||
struct SerializeList {/* [...] */}
|
||||
|
||||
impl Serializer {
|
||||
// TODO, implement:
|
||||
//
|
||||
// fn serialize_struct(self, name: &str) -> SerializeStruct
|
||||
// fn finish(self) -> String
|
||||
}
|
||||
|
||||
impl SerializeStruct {
|
||||
// TODO, implement:
|
||||
//
|
||||
// fn serialize_property(mut self, name: &str) -> SerializeStructProperty
|
||||
|
||||
// TODO,
|
||||
// How should we finish this struct? This depends on where it appears:
|
||||
// - At the root level: return `Serializer`
|
||||
// - As a property inside another struct: return `SerializeStruct`
|
||||
// - As a value inside a list: return `SerializeList`
|
||||
//
|
||||
// fn finish(self) -> ???
|
||||
}
|
||||
|
||||
impl SerializeStructProperty {
|
||||
// TODO, implement:
|
||||
//
|
||||
// fn serialize_string(self, value: &str) -> SerializeStruct
|
||||
// fn serialize_struct(self, name: &str) -> SerializeStruct
|
||||
// fn serialize_list(self) -> SerializeList
|
||||
// fn finish(self) -> SerializeStruct
|
||||
}
|
||||
|
||||
impl SerializeList {
|
||||
// TODO, implement:
|
||||
//
|
||||
// fn serialize_string(mut self, value: &str) -> Self
|
||||
// fn serialize_struct(mut self, value: &str) -> SerializeStruct
|
||||
// fn serialize_list(mut self) -> SerializeList
|
||||
|
||||
// TODO:
|
||||
// Like `SerializeStruct::finish`, the return type depends on nesting.
|
||||
//
|
||||
// fn finish(mut self) -> ???
|
||||
}
|
||||
```
|
||||
|
||||
Diagram of valid transitions:
|
||||
|
||||
```bob
|
||||
+-----------+ +---------+------------+-----+
|
||||
| | | | | |
|
||||
V | V | V |
|
||||
+ |
|
||||
serializer --> structure --> property --> list +-+
|
||||
|
||||
| | ^ | ^
|
||||
V | | | |
|
||||
| +-----------+ |
|
||||
String | |
|
||||
+--------------------------+
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- Building on our previous serializer, we now want to support **nested
|
||||
structures** and **lists**.
|
||||
|
||||
- However, this introduces both **duplication** and **structural complexity**.
|
||||
|
||||
- Even more critically, we now hit a **type system limitation**: we cannot
|
||||
cleanly express what `finish()` should return without duplicating variants for
|
||||
every nesting context (e.g. root, struct, list).
|
||||
|
||||
- From the diagram of valid transitions, we can observe:
|
||||
- The transitions are recursive
|
||||
- The return types depend on _where_ a substructure or list appears
|
||||
- Each context requires a return path to its parent
|
||||
|
||||
- With only concrete types, this becomes unmanageable. Our current approach
|
||||
leads to an explosion of types and manual wiring.
|
||||
|
||||
- In the next chapter, we’ll see how **generics** let us model recursive flows
|
||||
with less boilerplate, while still enforcing valid operations at compile time.
|
||||
|
||||
</details>
|
@@ -0,0 +1,101 @@
|
||||
## Typestate Pattern: Example
|
||||
|
||||
The typestate pattern encodes part of a value’s runtime state into its type.
|
||||
This allows us to prevent invalid or inapplicable operations at compile time.
|
||||
|
||||
```rust,editable
|
||||
use std::fmt::Write as _;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Serializer {
|
||||
output: String,
|
||||
}
|
||||
|
||||
struct SerializeStruct {
|
||||
serializer: Serializer,
|
||||
}
|
||||
|
||||
impl Serializer {
|
||||
fn serialize_struct(mut self, name: &str) -> SerializeStruct {
|
||||
writeln!(&mut self.output, "{name} {{").unwrap();
|
||||
SerializeStruct { serializer: self }
|
||||
}
|
||||
|
||||
fn finish(self) -> String {
|
||||
self.output
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeStruct {
|
||||
fn serialize_field(mut self, key: &str, value: &str) -> Self {
|
||||
writeln!(&mut self.serializer.output, " {key}={value};").unwrap();
|
||||
self
|
||||
}
|
||||
|
||||
fn finish_struct(mut self) -> Serializer {
|
||||
self.serializer.output.push_str("}\n");
|
||||
self.serializer
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let serializer = Serializer::default()
|
||||
.serialize_struct("User")
|
||||
.serialize_field("id", "42")
|
||||
.serialize_field("name", "Alice")
|
||||
.finish_struct();
|
||||
|
||||
println!("{}", serializer.finish());
|
||||
}
|
||||
```
|
||||
|
||||
`Serializer` usage flowchart:
|
||||
|
||||
```bob
|
||||
+------------+ serialize struct +-----------------+
|
||||
| Serializer | ------------------> | SerializeStruct | <------+
|
||||
+------------+ +-----------------+ |
|
||||
|
|
||||
| ^ | | |
|
||||
| | finish struct | | serialize field |
|
||||
| +-----------------------------+ +------------------+
|
||||
|
|
||||
+---> finish
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- This example is inspired by Serde’s
|
||||
[`Serializer` trait](https://docs.rs/serde/latest/serde/ser/trait.Serializer.html).
|
||||
Serde uses typestates internally to ensure serialization follows a valid
|
||||
structure. For more, see: <https://serde.rs/impl-serializer.html>
|
||||
|
||||
- The key idea behind typestate is that state transitions happen by consuming a
|
||||
value and producing a new one. At each step, only operations valid for that
|
||||
state are available.
|
||||
|
||||
- In this example:
|
||||
|
||||
- We begin with a `Serializer`, which only allows us to start serializing a
|
||||
struct.
|
||||
|
||||
- Once we call `.serialize_struct(...)`, ownership moves into a
|
||||
`SerializeStruct` value. From that point on, we can only call methods
|
||||
related to serializing struct fields.
|
||||
|
||||
- The original `Serializer` is no longer accessible — preventing us from
|
||||
mixing modes (such as starting another _struct_ mid-struct) or calling
|
||||
`finish()` too early.
|
||||
|
||||
- Only after calling `.finish_struct()` do we receive the `Serializer` back.
|
||||
At that point, the output can be finalized or reused.
|
||||
|
||||
- If we forget to call `finish_struct()` and drop the `SerializeStruct` early,
|
||||
the `Serializer` is also dropped. This ensures incomplete output cannot leak
|
||||
into the system.
|
||||
|
||||
- By contrast, if we had implemented everything on `Serializer` directly — as
|
||||
seen on the previous slide, nothing would stop someone from skipping important
|
||||
steps or mixing serialization flows.
|
||||
|
||||
</details>
|
@@ -0,0 +1,52 @@
|
||||
## Typestate Pattern with Generics
|
||||
|
||||
By combining typestate modeling with generics, we can express a wider range of
|
||||
valid states and transitions without duplicating logic. This approach is
|
||||
especially useful when the number of states grows or when multiple states share
|
||||
behavior but differ in structure.
|
||||
|
||||
```rust
|
||||
{{#include typestate-generics.rs:Serializer-def}}
|
||||
|
||||
{{#include typestate-generics.rs:Root-def}}
|
||||
{{#include typestate-generics.rs:Struct-def}}
|
||||
{{#include typestate-generics.rs:Property-def}}
|
||||
{{#include typestate-generics.rs:List-def}}
|
||||
```
|
||||
|
||||
We now have all the tools needed to implement the methods for the `Serializer`
|
||||
and its state type definitions. This ensures that our API only permits valid
|
||||
transitions, as illustrated in the following diagram:
|
||||
|
||||
Diagram of valid transitions:
|
||||
|
||||
```bob
|
||||
+-----------+ +---------+------------+-----+
|
||||
| | | | | |
|
||||
V | V | V |
|
||||
+ |
|
||||
serializer --> structure --> property --> list +-+
|
||||
|
||||
| | ^ | ^
|
||||
V | | | |
|
||||
| +-----------+ |
|
||||
String | |
|
||||
+--------------------------+
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- By leveraging generics to track the parent context, we can construct
|
||||
arbitrarily nested serializers that enforce valid transitions between struct,
|
||||
list, and property states.
|
||||
|
||||
- This enables us to build a recursive structure while maintaining strict
|
||||
control over which methods are accessible in each state.
|
||||
|
||||
- Methods common to all states can be defined for any `S` in `Serializer<S>`.
|
||||
|
||||
- Marker types (e.g., `List<S>`) introduce no memory or runtime overhead, as
|
||||
they contain no data other than a possible Zero-Sized Type. Their only role is
|
||||
to enforce correct API usage through the type system.
|
||||
|
||||
</details>
|
@@ -0,0 +1,164 @@
|
||||
// ANCHOR: Complete
|
||||
use std::fmt::Write as _;
|
||||
|
||||
// ANCHOR: Serializer-def
|
||||
struct Serializer<S> {
|
||||
// [...]
|
||||
indent: usize,
|
||||
buffer: String,
|
||||
state: S,
|
||||
}
|
||||
// ANCHOR_END: Serializer-def
|
||||
|
||||
// ANCHOR: Root-def
|
||||
struct Root;
|
||||
// ANCHOR_END: Root-def
|
||||
|
||||
// ANCHOR: Struct-def
|
||||
struct Struct<S>(S);
|
||||
// ANCHOR_END: Struct-def
|
||||
|
||||
// ANCHOR: List-def
|
||||
struct List<S>(S);
|
||||
// ANCHOR_END: List-def
|
||||
|
||||
// ANCHOR: Property-def
|
||||
struct Property<S>(S);
|
||||
// ANCHOR_END: Property-def
|
||||
|
||||
// ANCHOR: Root-impl
|
||||
impl Serializer<Root> {
|
||||
fn new() -> Self {
|
||||
// [...]
|
||||
Self { indent: 0, buffer: String::new(), state: Root }
|
||||
}
|
||||
|
||||
fn serialize_struct(mut self, name: &str) -> Serializer<Struct<Root>> {
|
||||
// [...]
|
||||
writeln!(self.buffer, "{name} {{").unwrap();
|
||||
Serializer {
|
||||
indent: self.indent + 1,
|
||||
buffer: self.buffer,
|
||||
state: Struct(self.state),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> String {
|
||||
// [...]
|
||||
self.buffer
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: Root-impl
|
||||
|
||||
// ANCHOR: Struct-impl
|
||||
impl<S> Serializer<Struct<S>> {
|
||||
fn serialize_property(mut self, name: &str) -> Serializer<Property<Struct<S>>> {
|
||||
// [...]
|
||||
write!(self.buffer, "{}{name}: ", " ".repeat(self.indent * 2)).unwrap();
|
||||
Serializer {
|
||||
indent: self.indent,
|
||||
buffer: self.buffer,
|
||||
state: Property(self.state),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_struct(mut self) -> Serializer<S> {
|
||||
// [...]
|
||||
self.indent -= 1;
|
||||
writeln!(self.buffer, "{}}}", " ".repeat(self.indent * 2)).unwrap();
|
||||
Serializer { indent: self.indent, buffer: self.buffer, state: self.state.0 }
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: Struct-impl
|
||||
|
||||
// ANCHOR: Property-impl
|
||||
impl<S> Serializer<Property<Struct<S>>> {
|
||||
fn serialize_struct(mut self, name: &str) -> Serializer<Struct<Struct<S>>> {
|
||||
// [...]
|
||||
writeln!(self.buffer, "{name} {{").unwrap();
|
||||
Serializer {
|
||||
indent: self.indent + 1,
|
||||
buffer: self.buffer,
|
||||
state: Struct(self.state.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_list(mut self) -> Serializer<List<Struct<S>>> {
|
||||
// [...]
|
||||
writeln!(self.buffer, "[").unwrap();
|
||||
Serializer {
|
||||
indent: self.indent + 1,
|
||||
buffer: self.buffer,
|
||||
state: List(self.state.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_string(mut self, value: &str) -> Serializer<Struct<S>> {
|
||||
// [...]
|
||||
writeln!(self.buffer, "{value},").unwrap();
|
||||
Serializer { indent: self.indent, buffer: self.buffer, state: self.state.0 }
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: Property-impl
|
||||
|
||||
// ANCHOR: List-impl
|
||||
impl<S> Serializer<List<S>> {
|
||||
fn serialize_struct(mut self, name: &str) -> Serializer<Struct<List<S>>> {
|
||||
// [...]
|
||||
writeln!(self.buffer, "{}{name} {{", " ".repeat(self.indent * 2)).unwrap();
|
||||
Serializer {
|
||||
indent: self.indent + 1,
|
||||
buffer: self.buffer,
|
||||
state: Struct(self.state),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_string(mut self, value: &str) -> Self {
|
||||
// [...]
|
||||
writeln!(self.buffer, "{}{value},", " ".repeat(self.indent * 2)).unwrap();
|
||||
self
|
||||
}
|
||||
|
||||
fn finish_list(mut self) -> Serializer<S> {
|
||||
// [...]
|
||||
self.indent -= 1;
|
||||
writeln!(self.buffer, "{}]", " ".repeat(self.indent * 2)).unwrap();
|
||||
Serializer { indent: self.indent, buffer: self.buffer, state: self.state.0 }
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: List-impl
|
||||
|
||||
// ANCHOR: main
|
||||
fn main() {
|
||||
#[rustfmt::skip]
|
||||
let serializer = Serializer::new()
|
||||
.serialize_struct("Foo")
|
||||
.serialize_property("bar")
|
||||
.serialize_struct("Bar")
|
||||
.serialize_property("baz")
|
||||
.serialize_list()
|
||||
.serialize_string("abc")
|
||||
.serialize_struct("Baz")
|
||||
.serialize_property("partial")
|
||||
.serialize_string("def")
|
||||
.serialize_property("empty")
|
||||
.serialize_struct("Empty")
|
||||
.finish_struct()
|
||||
.finish_struct()
|
||||
.finish_list()
|
||||
.finish_struct()
|
||||
.finish_struct();
|
||||
|
||||
let output = serializer.finish();
|
||||
|
||||
println!("{output}");
|
||||
|
||||
// These will all fail at compile time:
|
||||
|
||||
// Serializer::new().serialize_list();
|
||||
// Serializer::new().serialize_string("foo");
|
||||
// Serializer::new().serialize_struct("Foo").serialize_string("bar");
|
||||
// Serializer::new().serialize_struct("Foo").serialize_list();
|
||||
// Serializer::new().serialize_property("foo");
|
||||
}
|
||||
// ANCHOR_END: main
|
@@ -0,0 +1,89 @@
|
||||
## Serializer: complete implementation
|
||||
|
||||
Looking back at our original desired flow:
|
||||
|
||||
```bob
|
||||
+-----------+ +---------+------------+-----+
|
||||
| | | | | |
|
||||
V | V | V |
|
||||
+ |
|
||||
serializer --> structure --> property --> list +-+
|
||||
|
||||
| | ^ | ^
|
||||
V | | | |
|
||||
| +-----------+ |
|
||||
String | |
|
||||
+--------------------------+
|
||||
```
|
||||
|
||||
We can now see this reflected directly in the types of our serializer:
|
||||
|
||||
```bob
|
||||
+------+
|
||||
finish | |
|
||||
serialize struct V |
|
||||
struct
|
||||
+---------------------+ --------------> +-----------------------------+ <---------------+
|
||||
| Serializer [ Root ] | | Serializer [ Struct [ S ] ] | |
|
||||
+---------------------+ <-------------- +-----------------------------+ <-----------+ |
|
||||
finish struct | |
|
||||
| | serialize | | |
|
||||
| +----------+ property V serialize | |
|
||||
| | string or | |
|
||||
finish | | +-------------------------------+ struct | |
|
||||
V | | Serializer [ Property [ S ] ] | ------------+ |
|
||||
finish | +-------------------------------+ |
|
||||
+--------+ struct | |
|
||||
| String | | serialize | |
|
||||
+--------+ | list V |
|
||||
| finish |
|
||||
| +---------------------------+ list |
|
||||
+------> | Serializer [ List [ S ] ] | ----------------+
|
||||
+---------------------------+
|
||||
serialize
|
||||
list or string ^
|
||||
| or finish list |
|
||||
+-------------------+
|
||||
```
|
||||
|
||||
The code for the full implementation of the `Serializer` and all its states can
|
||||
be found in
|
||||
[this Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=c9cbb831cd05fe9db4ce42713c83ca16).
|
||||
|
||||
<details>
|
||||
|
||||
- This pattern isn't a silver bullet. It still allows issues like:
|
||||
- Empty or invalid property names (which can be fixed using
|
||||
[the newtype pattern](../../newtype-pattern.md))
|
||||
- Duplicate property names (which could be tracked in `Struct<S>` and handled
|
||||
via `Result`)
|
||||
|
||||
- If validation failures occur, we can also change method signatures to return a
|
||||
`Result`, allowing recovery:
|
||||
|
||||
```rust,compile_fail
|
||||
struct PropertySerializeError<S> {
|
||||
kind: PropertyError,
|
||||
serializer: Serializer<Struct<S>>,
|
||||
}
|
||||
|
||||
impl<S> Serializer<Struct<S>> {
|
||||
fn serialize_property(
|
||||
self,
|
||||
name: &str,
|
||||
) -> Result<Serializer<Property<Struct<S>>>, PropertySerializeError<S>> {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- While this API is powerful, it’s not always ergonomic. Production serializers
|
||||
typically favor simpler APIs and reserve the typestate pattern for enforcing
|
||||
critical invariants.
|
||||
|
||||
- One excellent real-world example is
|
||||
[`rustls::ClientConfig`](https://docs.rs/rustls/latest/rustls/client/struct.ClientConfig.html#method.builder),
|
||||
which uses typestate with generics to guide the user through safe and correct
|
||||
configuration steps.
|
||||
|
||||
</details>
|
@@ -0,0 +1,49 @@
|
||||
## Serializer: implement Property
|
||||
|
||||
```rust
|
||||
# use std::fmt::Write as _;
|
||||
{{#include ../typestate-generics.rs:Serializer-def}}
|
||||
|
||||
{{#include ../typestate-generics.rs:Struct-def}}
|
||||
{{#include ../typestate-generics.rs:Property-def}}
|
||||
{{#include ../typestate-generics.rs:List-def}}
|
||||
|
||||
{{#include ../typestate-generics.rs:Property-impl}}
|
||||
```
|
||||
|
||||
With the addition of the Property state methods, our diagram is now nearly
|
||||
complete:
|
||||
|
||||
```bob
|
||||
+------+
|
||||
finish | |
|
||||
serialize struct V |
|
||||
struct
|
||||
+---------------------+ --------------> +-----------------------------+
|
||||
| Serializer [ Root ] | | Serializer [ Struct [ S ] ] |
|
||||
+---------------------+ <-------------- +-----------------------------+ <-----------+
|
||||
finish struct |
|
||||
| serialize | |
|
||||
| property V serialize |
|
||||
| string or |
|
||||
finish | +-------------------------------+ struct |
|
||||
V | Serializer [ Property [ S ] ] | ------------+
|
||||
+-------------------------------+
|
||||
+--------+
|
||||
| String | serialize |
|
||||
+--------+ list V
|
||||
|
||||
+---------------------------+
|
||||
| Serializer [ List [ S ] ] |
|
||||
+---------------------------+
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- A property can be defined as a `String`, `Struct<S>`, or `List<S>`, enabling
|
||||
the representation of nested structures.
|
||||
|
||||
- This concludes the step-by-step implementation. The full implementation,
|
||||
including support for `List<S>`, is shown in the next slide.
|
||||
|
||||
</details>
|
@@ -0,0 +1,40 @@
|
||||
## Serializer: implement Root
|
||||
|
||||
```rust
|
||||
# use std::fmt::Write as _;
|
||||
{{#include ../typestate-generics.rs:Serializer-def}}
|
||||
|
||||
{{#include ../typestate-generics.rs:Root-def}}
|
||||
{{#include ../typestate-generics.rs:Struct-def}}
|
||||
|
||||
{{#include ../typestate-generics.rs:Root-impl}}
|
||||
```
|
||||
|
||||
Referring back to our original diagram of valid transitions, we can visualize
|
||||
the beginning of our implementation as follows:
|
||||
|
||||
```bob
|
||||
serialize
|
||||
struct
|
||||
+---------------------+ --------------> +--------------------------------+
|
||||
| Serializer [ Root ] | | Serializer [ Struct [ Root ] ] |
|
||||
+---------------------+ <-------------- +--------------------------------+
|
||||
finish struct
|
||||
|
|
||||
|
|
||||
|
|
||||
finish |
|
||||
V
|
||||
|
||||
+--------+
|
||||
| String |
|
||||
+--------+
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- At the "root" of our `Serializer`, the only construct allowed is a `Struct`.
|
||||
|
||||
- The `Serializer` can only be finalized into a `String` from this root level.
|
||||
|
||||
</details>
|
@@ -0,0 +1,43 @@
|
||||
## Serializer: implement Struct
|
||||
|
||||
```rust
|
||||
# use std::fmt::Write as _;
|
||||
{{#include ../typestate-generics.rs:Serializer-def}}
|
||||
|
||||
{{#include ../typestate-generics.rs:Struct-def}}
|
||||
{{#include ../typestate-generics.rs:Property-def}}
|
||||
|
||||
{{#include ../typestate-generics.rs:Struct-impl}}
|
||||
```
|
||||
|
||||
The diagram can now be expanded as follows:
|
||||
|
||||
```bob
|
||||
+------+
|
||||
finish | |
|
||||
serialize struct V |
|
||||
struct
|
||||
+---------------------+ --------------> +-----------------------------+
|
||||
| Serializer [ Root ] | | Serializer [ Struct [ S ] ] |
|
||||
+---------------------+ <-------------- +-----------------------------+
|
||||
finish struct
|
||||
| serialize |
|
||||
| property V
|
||||
|
|
||||
finish | +------------------------------------------+
|
||||
V | Serializer [ Property [ Struct [ S ] ] ] |
|
||||
+------------------------------------------+
|
||||
+--------+
|
||||
| String |
|
||||
+--------+
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- A `Struct` can only contain a `Property`;
|
||||
|
||||
- Finishing a `Struct` returns control back to its parent, which in our previous
|
||||
slide was assumed the `Root`, but in reality however it can be also something
|
||||
else such as `Struct` in case of nested "structs".
|
||||
|
||||
</details>
|
Reference in New Issue
Block a user