|
1 | 1 | ## Typestate Pattern with Generics |
2 | 2 |
|
3 | | -Generics can be used with the typestate pattern to reduce duplication and allow |
4 | | -shared logic across state variants, while still encoding state transitions in |
5 | | -the type system. |
| 3 | +TODO |
6 | 4 |
|
7 | | -```rust |
8 | | -#[non_exhaustive] |
9 | | -struct Insecure; |
10 | | -struct Secure { |
11 | | - client_cert: Option<Vec<u8>>, |
12 | | -} |
13 | | - |
14 | | -trait Transport { |
15 | | - /* ... */ |
16 | | -} |
17 | | -impl Transport for Insecure { |
18 | | - /* ... */ |
19 | | -} |
20 | | -impl Transport for Secure { |
21 | | - /* ... */ |
22 | | -} |
23 | | - |
24 | | -#[non_exhaustive] |
25 | | -struct WantsTransport; |
26 | | -struct Ready<T> { |
27 | | - transport: T, |
28 | | -} |
29 | | - |
30 | | -struct ConnectionBuilder<T> { |
31 | | - host: String, |
32 | | - timeout: Option<u64>, |
33 | | - stage: T, |
34 | | -} |
35 | | - |
36 | | -struct Connection {/* ... */} |
37 | | - |
38 | | -impl Connection { |
39 | | - fn new(host: &str) -> ConnectionBuilder<WantsTransport> { |
40 | | - ConnectionBuilder { |
41 | | - host: host.to_owned(), |
42 | | - timeout: None, |
43 | | - stage: WantsTransport, |
44 | | - } |
45 | | - } |
46 | | -} |
47 | | - |
48 | | -impl<T> ConnectionBuilder<T> { |
49 | | - fn timeout(mut self, secs: u64) -> Self { |
50 | | - self.timeout = Some(secs); |
51 | | - self |
52 | | - } |
53 | | -} |
54 | | - |
55 | | -impl ConnectionBuilder<WantsTransport> { |
56 | | - fn insecure(self) -> ConnectionBuilder<Ready<Insecure>> { |
57 | | - ConnectionBuilder { |
58 | | - host: self.host, |
59 | | - timeout: self.timeout, |
60 | | - stage: Ready { transport: Insecure }, |
61 | | - } |
62 | | - } |
63 | | - |
64 | | - fn secure(self) -> ConnectionBuilder<Ready<Secure>> { |
65 | | - ConnectionBuilder { |
66 | | - host: self.host, |
67 | | - timeout: self.timeout, |
68 | | - stage: Ready { transport: Secure { client_cert: None } }, |
69 | | - } |
70 | | - } |
71 | | -} |
72 | | - |
73 | | -impl ConnectionBuilder<Ready<Secure>> { |
74 | | - fn client_certificate(mut self, raw: Vec<u8>) -> Self { |
75 | | - self.stage.transport.client_cert = Some(raw); |
76 | | - self |
77 | | - } |
78 | | -} |
79 | | - |
80 | | -impl<T: Transport> ConnectionBuilder<Ready<T>> { |
81 | | - fn connect(self) -> std::io::Result<Connection> { |
82 | | - // ... use valid state to establish the configured connection |
83 | | - Ok(Connection {}) |
84 | | - } |
85 | | -} |
86 | | - |
87 | | -fn main() -> std::io::Result<()> { |
88 | | - let _conn = Connection::new("db.local") |
89 | | - .secure() |
90 | | - .client_certificate(vec![1, 2, 3]) |
91 | | - .timeout(10) |
92 | | - .connect()?; |
93 | | - Ok(()) |
94 | | -} |
| 5 | +```rust,editable |
| 6 | +// TODO |
95 | 7 | ``` |
96 | 8 |
|
97 | 9 | <details> |
98 | 10 |
|
99 | | -- This example extends the typestate pattern using **generic parameters** to |
100 | | - avoid duplication of common logic. |
101 | | - |
102 | | -- We use a generic type `T` to represent the current stage of the builder, and |
103 | | - share fields like `host` and `timeout` across all stages. |
104 | | - |
105 | | -- The transport phase uses `insecure()` and `secure()` to transition from |
106 | | - `WantsTransport` into `Ready<T>`, where `T` is a type that implements the |
107 | | - `Transport` trait. |
108 | | - |
109 | | -- Only once the connection is in a `Ready<T>` state, we can call `.connect()`, |
110 | | - guaranteed at compile time. |
111 | | - |
112 | | -- Using generics allows us to avoid writing separate `BuilderForSecure`, |
113 | | - `BuilderForInsecure`, etc. structs. |
114 | | - |
115 | | - Shared behavior, like `.timeout(...)`, can be implemented once and reused |
116 | | - across all states. |
117 | | - |
118 | | -- This same design appears |
119 | | - [in real-world libraries like **Rustls**](https://docs.rs/rustls/latest/rustls/struct.ConfigBuilder.html), |
120 | | - where the `ConfigBuilder` uses typestate and generics to guide users through a |
121 | | - safe, ordered configuration flow. |
122 | | - |
123 | | - It enforces at compile time that users must choose protocol versions, a |
124 | | - certificate verifier, and client certificate options, in the correct sequence, |
125 | | - before building a config. |
126 | | - |
127 | | -- **Downsides** of this approach include: |
128 | | - - The documentation of the various builder types can become difficult to |
129 | | - follow, since their names are generated by generics and internal structs |
130 | | - like `Ready<T>`. |
131 | | - - Error messages from the compiler may become more opaque, especially if a |
132 | | - trait bound is not satisfied or a state transition is incomplete. |
133 | | - |
134 | | - The error messages might also be hard to follow due to the complexity as a |
135 | | - result of the nested generics types. |
136 | | - |
137 | | -- Still, in return for this complexity, you get compile-time enforcement of |
138 | | - valid configuration, clear builder sequencing, and no possibility of |
139 | | - forgetting a required step or misusing the API at runtime. |
| 11 | +- TODO |
140 | 12 |
|
141 | 13 | </details> |
0 commit comments