You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-09-16 09:36:41 +02:00
apply remaining feedback of @randomPoison
This commit is contained in:
@@ -441,6 +441,10 @@
|
|||||||
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.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)
|
- [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)
|
- [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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -53,18 +53,7 @@ impl SerializeList {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
Diagram of valid transitions:
|
||||||
|
|
||||||
- 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).
|
|
||||||
|
|
||||||
- To better understand this limitation, let’s map the valid transitions:
|
|
||||||
|
|
||||||
```bob
|
```bob
|
||||||
+-----------+ +---------+------------+-----+
|
+-----------+ +---------+------------+-----+
|
||||||
@@ -80,7 +69,18 @@ serializer --> structure --> property --> list +-+
|
|||||||
+--------------------------+
|
+--------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
- From this diagram, we can observe:
|
<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 transitions are recursive
|
||||||
- The return types depend on _where_ a substructure or list appears
|
- The return types depend on _where_ a substructure or list appears
|
||||||
- Each context requires a return path to its parent
|
- Each context requires a return path to its parent
|
||||||
|
@@ -6,184 +6,19 @@ especially useful when the number of states grows or when multiple states share
|
|||||||
behavior but differ in structure.
|
behavior but differ in structure.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# use std::fmt::Write as _;
|
{{#include typestate-generics.rs:Serializer-def}}
|
||||||
#
|
|
||||||
struct Serializer<S> {
|
|
||||||
// [...]
|
|
||||||
# indent: usize,
|
|
||||||
# buffer: String,
|
|
||||||
# state: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Root;
|
{{#include typestate-generics.rs:Root-def}}
|
||||||
struct Struct<S>(S);
|
{{#include typestate-generics.rs:Struct-def}}
|
||||||
struct List<S>(S);
|
{{#include typestate-generics.rs:Property-def}}
|
||||||
struct Property<S>(S);
|
{{#include typestate-generics.rs:List-def}}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
# }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
# }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
# }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
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:
|
||||||
|
|
||||||
- The full code for this example is available
|
Diagram of valid transitions:
|
||||||
[in the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=48b106089ca600453f3ed00a0a31af26).
|
|
||||||
|
|
||||||
- By using generics to track the parent context, we can construct arbitrarily
|
|
||||||
nested serializers that enforce valid transitions between struct, list, and
|
|
||||||
property states.
|
|
||||||
|
|
||||||
- This lets us build a recursive structure while preserving control over what
|
|
||||||
methods are accessible in each state.
|
|
||||||
|
|
||||||
- Methods common to all states can be implemented for any `S` in
|
|
||||||
`Serializer<S>`.
|
|
||||||
|
|
||||||
- These marker types (e.g., `List<S>`) incur no memory or runtime overhead, as
|
|
||||||
they hold no data other than a possible Zero-Sized Type. Their sole purpose is
|
|
||||||
to enforce correct API usage by leveraging the type system.
|
|
||||||
|
|
||||||
- Here's how the flow maps to a state machine:
|
|
||||||
|
|
||||||
```bob
|
```bob
|
||||||
+-----------+ +---------+------------+-----+
|
+-----------+ +---------+------------+-----+
|
||||||
@@ -199,68 +34,19 @@ serializer --> structure --> property --> list +-+
|
|||||||
+--------------------------+
|
+--------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
- And this is reflected directly in the types of our serializer:
|
<details>
|
||||||
|
|
||||||
```bob
|
- By leveraging generics to track the parent context, we can construct
|
||||||
+------+
|
arbitrarily nested serializers that enforce valid transitions between struct,
|
||||||
finish | |
|
list, and property states.
|
||||||
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 |
|
|
||||||
+-------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
- Of course, this pattern isn't a silver bullet. It still allows issues like:
|
- This enables us to build a recursive structure while maintaining strict
|
||||||
- Empty or invalid property names (which can be fixed using
|
control over which methods are accessible in each state.
|
||||||
[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
|
- Methods common to all states can be defined for any `S` in `Serializer<S>`.
|
||||||
`Result`, allowing recovery:
|
|
||||||
|
|
||||||
```rust,compile_fail
|
- Marker types (e.g., `List<S>`) introduce no memory or runtime overhead, as
|
||||||
struct PropertySerializeError<S> {
|
they contain no data other than a possible Zero-Sized Type. Their only role is
|
||||||
kind: PropertyError,
|
to enforce correct API usage through the type system.
|
||||||
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>
|
</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