Build with Naz : Rust typestate pattern
+ + + + + ++ 👀 Watch Rust 🦀 live coding videos on our + YouTube Channel. + ++ + + + + + + +
+ + + + + + + + + + +
-
+
- What is the typestate pattern? +
- More resources on typestate pattern and others in Rust +
- YouTube video for this article +
- Examples of typestate pattern in Rust + + +
- Build with Naz video series on developerlife.com YouTube channel +
+ + + What is the typestate pattern? # + + +
+ + + +The Typestate Pattern in Rust is a way to manage objects that go through different states +in their lifecycle. It leverages Rust’s powerful type system to enforce these states and +transitions between them, making your code safer and more predictable.
+ +Here are the key ideas behind the Typestate Pattern:
+ +-
+
- States as structs: Each possible state of the object is represented by a separate +struct. This lets you associate specific methods and data with each state. +
- Transitions with ownership: Methods that transition the object to a new state consume +the old state and return a value representing the new state. Rust’s ownership system +ensures you can’t accidentally use the object in an invalid state. +
- Encapsulated functionality: Methods are only available on the structs representing the +valid states. This prevents you from trying to perform actions that aren’t allowed in +the current state. +
Benefits of using the Typestate Pattern:
+ +-
+
- Safer code: By statically checking types at compile time, the compiler prevents you from +accidentally using the object in an invalid state. This leads to fewer runtime errors +and more robust code. +
- Improved readability: The code becomes more self-documenting because the valid state +transitions are encoded in the types themselves. +
- Clearer APIs: By separating functionality based on state, APIs become more intuitive and +easier to understand. +
+ + + More resources on typestate pattern and others in Rust # + + +
+ + + +-
+
- Functional typed design patterns. +
- Enums and typestate (and limitations). +
- Type-Driven API Design in Rust. +
- Rust typestate notes. +
- Rusty Typestates - Starting Out. +
- The Embedded Rust Book - Typestate programming. +
- Typestates in Rust. +
+ + + YouTube video for this article # + + +
+ + + +This blog post has short examples on how to use the typestate pattern in Rust. If you like +to learn via video, please watch the companion video on the developerlife.com YouTube +channel.
+ + + + ++ + + Examples of typestate pattern in Rust # + + +
+ + + +Let’s create some examples to illustrate how to use the typestate pattern in Rust. You can run
+cargo new --bin typestate-pattern
to create a new binary crate.
++ +The code in the video and this tutorial are all in this GitHub +repo.
+
Then add the following to the Cargo.toml
file that’s generated. These pull in all the
+dependencies that we need for these examples.
[package]
+name = "typestate-pattern"
+version = "0.1.0"
+edition = "2021"
+
+[[bin]]
+name = "ex1"
+path = "src/ex1.rs"
+
+[[bin]]
+name = "ex2"
+path = "src/ex2.rs"
+
+[[bin]]
+name = "ex3"
+path = "src/ex3.rs"
+
+[[bin]]
+name = "ex3_1"
+path = "src/ex3_1.rs"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+crossterm = { version = "0.27.0", features = ["event-stream"] }
+
+ + + Example 1: Simple version of this is using enums to encapsulate states as variants # + + +
+ + + +Then you can add the following code to the src/ex1.rs
file.
#[derive(Debug)]
+pub enum InputEvent {
+ Keyboard((KeyPress, Option<Vec<Modifier>>)),
+ Resize(Size),
+ Mouse(MouseEvent),
+}
+
+#[derive(Debug)]
+pub enum Modifier {
+ Shift,
+ Control,
+ Alt,
+}
+
+#[derive(Debug)]
+pub enum KeyPress {
+ Char(char),
+ Enter,
+ Backspace,
+ Delete,
+ Left,
+ Right,
+ Up,
+ Down,
+ Home,
+ End,
+ PageUp,
+ PageDown,
+ Tab,
+ F(u8),
+}
+
+#[derive(Debug)]
+pub enum Size {
+ Height(u16),
+ Width(u16),
+}
+
+#[derive(Debug)]
+pub enum MouseEvent {
+ Press(MouseButton, u16, u16),
+ Release(u16, u16),
+ Hold(u16, u16),
+}
+
+#[derive(Debug)]
+pub enum MouseButton {
+ Left,
+ Right,
+ Middle,
+}
+
+impl InputEvent {
+ pub fn pretty_print(&self) {
+ let it = match self {
+ InputEvent::Keyboard((keypress, modifiers)) => {
+ let mut result = format!("{:?}", keypress);
+ if let Some(modifiers) = modifiers {
+ result.push_str(&format!("{:?}", modifiers));
+ }
+ result
+ }
+ InputEvent::Resize(size) => format!("{:?}", size),
+ InputEvent::Mouse(mouse_event) => format!("{:?}", mouse_event),
+ };
+ println!("{}", it);
+ }
+}
+
+fn main() {
+ let a_pressed = InputEvent::Keyboard((KeyPress::Char('a'), None));
+ println!("{:?}", a_pressed);
+
+ let ctrl_c_pressed = InputEvent::Keyboard(
+ (KeyPress::Char('c'), Some(vec![Modifier::Control]))
+ );
+ println!("{:?}", ctrl_c_pressed);
+
+ let enter_pressed = InputEvent::Keyboard((KeyPress::Enter, None));
+ enter_pressed.pretty_print();
+
+ let mouse_pressed = InputEvent::Mouse(
+ MouseEvent::Press(MouseButton::Left, 10, 20)
+ );
+ mouse_pressed.pretty_print();
+}
+
++ +The code for this example is +here. +Here’s the code for the real +
+InputEvent
.
The main things to note about this code.
+ +-
+
- We have a bunch of enums that represent different types of input events. +
- We have a method on the
InputEvent
enum that pretty prints the event for all variants. +We don’t have a way to restrict methods on a specific variant using this approach.
+
When you run this code (using cargo run --bin ex1
), it should produce the following
+output:
$ cargo run --bin ex1 +Keyboard((Char('a'), None)) +Keyboard((Char('c'), Some([Control]))) +Enter +Press(Left, 10, 20) ++
+ + + Example 2: Slightly more complex versions are where one type + data = another type # + + +
+ + + +For this example, let’s add the following code to the src/ex2.rs
file.
mod ex1;
+use ex1::InputEvent;
+
+#[derive(Debug)]
+pub enum EditorEvent {
+ InsertChar(char),
+ InsertNewLine,
+ Delete,
+ Backspace,
+ MoveCursorLeft,
+ MoveCursorRight,
+ MoveCursorUp,
+ MoveCursorDown,
+ Copy,
+ Paste,
+ Cut,
+ Undo,
+ Redo,
+}
+
+impl TryFrom<InputEvent> for EditorEvent {
+ type Error = String;
+
+ fn try_from(input_event: InputEvent) -> Result<Self, Self::Error> {
+ match input_event {
+ InputEvent::Keyboard((keypress, modifiers)) =>
+ match (keypress, modifiers)
+ {
+ (ex1::KeyPress::Char(ch), None) => Ok(Self::InsertChar(ch)),
+ (ex1::KeyPress::Char(_), Some(_)) => todo!(),
+ (ex1::KeyPress::Enter, None) => Ok(Self::InsertNewLine),
+ (ex1::KeyPress::Enter, Some(_)) => todo!(),
+ (ex1::KeyPress::Backspace, None) => todo!(),
+ (ex1::KeyPress::Backspace, Some(_)) => todo!(),
+ (ex1::KeyPress::Delete, None) => todo!(),
+ (ex1::KeyPress::Delete, Some(_)) => todo!(),
+ (ex1::KeyPress::Left, None) => todo!(),
+ (ex1::KeyPress::Left, Some(_)) => todo!(),
+ (ex1::KeyPress::Right, None) => todo!(),
+ (ex1::KeyPress::Right, Some(_)) => todo!(),
+ (ex1::KeyPress::Up, None) => todo!(),
+ (ex1::KeyPress::Up, Some(_)) => todo!(),
+ (ex1::KeyPress::Down, None) => todo!(),
+ (ex1::KeyPress::Down, Some(_)) => todo!(),
+ (ex1::KeyPress::Home, None) => todo!(),
+ (ex1::KeyPress::Home, Some(_)) => todo!(),
+ (ex1::KeyPress::End, None) => todo!(),
+ (ex1::KeyPress::End, Some(_)) => todo!(),
+ (ex1::KeyPress::PageUp, None) => todo!(),
+ (ex1::KeyPress::PageUp, Some(_)) => todo!(),
+ (ex1::KeyPress::PageDown, None) => todo!(),
+ (ex1::KeyPress::PageDown, Some(_)) => todo!(),
+ (ex1::KeyPress::Tab, None) => todo!(),
+ (ex1::KeyPress::Tab, Some(_)) => todo!(),
+ (ex1::KeyPress::F(_), None) => todo!(),
+ (ex1::KeyPress::F(_), Some(_)) => todo!(),
+ },
+ InputEvent::Resize(_) => todo!(),
+ InputEvent::Mouse(_) => todo!(),
+ }
+ }
+}
+
+fn main() {
+ let a_pressed = InputEvent::Keyboard((ex1::KeyPress::Char('a'), None));
+ println!("{:?}", EditorEvent::try_from(a_pressed));
+
+ let enter_pressed = InputEvent::Keyboard((ex1::KeyPress::Enter, None));
+ println!("{:?}", EditorEvent::try_from(enter_pressed));
+}
+
+
++ +You can get the source code for this example +here. +Here’s the code for the real +
+EditorEvent
.
Here are some notes on this code:
+ +-
+
- We have a new enum called
EditorEvent
that represents different types of events that +can happen in an editor.
+ - We have a
TryFrom
implementation forInputEvent
that converts anInputEvent
into +anEditorEvent
. This is a way to restrict methods to specific variants of an enum by +converting it into a totally different type.
+ - We still don’t have a way to restrict methods to specific variants of the enum. +
When you run this code (using cargo run --bin ex2
), it should produce the following:
$ cargo run --bin ex2 +Ok(InsertChar('a')) +Ok(InsertNewLine) ++
+ + + Example 3: Best of both worlds, using generics and struct / enum with a marker trait # + + +
+ + + +Finally we have arrived at the typestate pattern in Rust. With this example:
+-
+
- You can now group all the states under a marker. +
- You can have methods that are specific to a variant. +
- You can specify methods that are common to all. +
- It’s like a very sophisticated builder pattern if you’re already familiar with that. +
Add the following code to the src/ex3.rs
file.
use self::type_state_builder::HttpResponse;
+use crossterm::style::Stylize;
+
+pub fn main() -> Result<(), String> {
+ let response = HttpResponse::<()>::new();
+ println!("{}", "Start state".red().bold().underlined());
+ println!("response: {:#?}", response);
+ println!(
+ "response size: {}",
+ response.get_size().to_string().blue().bold()
+ );
+
+ // Transition to HeaderAndBody state by calling `set_status_line`.
+ let mut response = response.set_status_line(200, "OK");
+ println!("response: {:#?}", response);
+
+ // Status line is required.
+ println!("{}", "HeaderAndBody state".red().bold().underlined());
+ println!("response_code: {}", response.get_response_code());
+ println!("response body: {:#?}", response.get_body());
+ println!("response: {:#?}", response);
+ println!(
+ "response size: {}",
+ response.get_size().to_string().blue().bold()
+ );
+
+ // Body and headers are optional.
+ println!("{}", "HeaderAndBody state # 2".red().bold().underlined());
+ response.add_header("Content-Type", "text/html");
+ response.set_body("<html><body>Hello World!</body></html>");
+ println!("response: {:#?}", response);
+ println!(
+ "response size: {}",
+ response.get_size().to_string().blue().bold()
+ );
+
+ // Final state.
+ println!("{}", "Final state".red().bold().underlined());
+ let response = response.finish();
+ println!("response_code: {}", response.get_response_code());
+ println!("status_line: {}", response.get_status_line());
+ println!("headers: {:#?}", response.get_headers());
+ println!("body: {}", response.get_body());
+ println!("response: {:#?}", response);
+ println!(
+ "response size: {}",
+ response.get_size().to_string().blue().bold()
+ );
+
+ Ok(())
+}
+
Note the API that we have built here:
+-
+
- You can’t call
get_response_code
orget_body
until you’ve calledset_status_line
.
+ - You can’t call
add_header
orset_body
until you’ve calledset_status_line
.
+ - You can’t call
finish
until you’ve calledset_status_line
.
+ - We have 3 states:
Start
,HeaderAndBody
, andFinal
. These are meant to be used as +markers to restrict methods to specific states. Each is a struct with a marker trait. +And it may or may not contain data / fields.
+ - We have a
HttpResponse
struct that uses a generic typeT: Marker
to represent the +state. This is a way to restrict methods to specific states.
+ - We can transition between states by calling methods that consume the current state and
+return a new state. These methods are specific to the state they transition from. And
+they can be implemented via
impl HttpResponse<T: Marker> { ... }
blocks, whereT
is +theStart
,HeaderAndBody
, orFinal
state.
+ - We can even implement methods that are valid for a non-existent state using
impl +HttpResponse<()> { ... }
. This is the constructor.
+ - In the
Final
state, the data becomes immutable.
+
Add the following code to desribe the different state structs.
+ +pub mod state {
+ #[derive(Debug, Clone, Default)]
+ pub struct Start {}
+
+ #[derive(Debug, Clone, Default)]
+ pub struct HeaderAndBody {
+ pub response_code: u8,
+ pub status_line: String,
+ pub headers: Option<Vec<(String, String)>>,
+ pub body: Option<String>,
+ }
+
+ #[derive(Debug, Clone, Default)]
+ pub struct Final {
+ pub response_code: u8,
+ pub status_line: String,
+ pub headers: Vec<(String, String)>,
+ pub body: String,
+ }
+
+ // The following marker trait is used to restrict the operations
+ // that are available in each state. This isn't strictly necessary,
+ // but it's a nice thing to use in a where clause to restrict types.
+ pub trait Marker {}
+ impl Marker for () {}
+ impl Marker for Start {}
+ impl Marker for HeaderAndBody {}
+ impl Marker for Final {}
+}
+
Here is the code for the HttpResponse
struct.
pub mod type_state_builder {
+ use super::state::{Final, HeaderAndBody, Marker, Start};
+
+ #[derive(Debug, Clone, Default)]
+ pub struct HttpResponse<S: Marker> {
+ pub state: S,
+ }
+
+ // Operations that are available in all states.
+ impl<S> HttpResponse<S>
+ where
+ S: Marker,
+ {
+ pub fn get_size(&self) -> String {
+ let len = std::mem::size_of_val(self);
+ format!("{} bytes", len)
+ }
+ }
+
+ // Operations that are only valid in `()`.
+ impl HttpResponse<()> {
+ pub fn new() -> HttpResponse<Start> {
+ HttpResponse { state: Start {} }
+ }
+ }
+
+ // Operations that are only valid in `Start`.
+ impl HttpResponse<Start> {
+ pub fn set_status_line(
+ self,
+ response_code: u8,
+ message: &str,
+ ) -> HttpResponse<HeaderAndBody> {
+ HttpResponse {
+ state: HeaderAndBody {
+ response_code,
+ status_line: format!(
+ "HTTP/1.1 {} {}", response_code, message
+ ),
+ ..Default::default()
+ },
+ }
+ }
+ }
+
+ // Operations that are only valid in `HeaderAndBodyState`.
+ impl HttpResponse<HeaderAndBody> {
+ // setter.
+ pub fn add_header(&mut self, key: &str, value: &str) {
+ if self.state.headers.is_none() {
+ self.state.headers.replace(Vec::new());
+ }
+ if let Some(v) = self.state.headers.as_mut() {
+ v.push((key.to_string(), value.to_string()))
+ }
+ }
+
+ // getter.
+ pub fn get_response_code(&self) -> u8 {
+ self.state.response_code
+ }
+
+ // setter.
+ pub fn set_body(&mut self, body: &str) {
+ self.state.body.replace(body.to_string());
+ }
+
+ // getter.
+ pub fn get_body(&self) -> Option<&str> {
+ self.state.body.as_deref()
+ }
+
+ // transition to Final state.
+ pub fn finish(mut self) -> HttpResponse<Final> {
+ HttpResponse {
+ state: Final {
+ response_code: self.state.response_code,
+ status_line: self.state.status_line.clone(),
+ headers: self.state.headers.take().unwrap_or_default(),
+ body: self.state.body.take().unwrap_or_default(),
+ },
+ }
+ }
+ }
+
+ // Operations that are only valid in `Final`.
+ impl HttpResponse<Final> {
+ // getter.
+ pub fn get_headers(&self) -> &Vec<(String, String)> {
+ &self.state.headers
+ }
+
+ // getter.
+ pub fn get_body(&self) -> &str {
+ &self.state.body
+ }
+
+ // getter.
+ pub fn get_response_code(&self) -> u8 {
+ self.state.response_code
+ }
+
+ // getter.
+ pub fn get_status_line(&self) -> &str {
+ &self.state.status_line
+ }
+ }
+}
+
When you run the code using cargo run --bin ex3
, it should produce the following output.
$ cargo run --bin ex3 +Start state +response: HttpResponse { + state: Start, +} +response size: 0 bytes +response: HttpResponse { + state: HeaderAndBody { + response_code: 200, + status_line: "HTTP/1.1 200 OK", + headers: None, + body: None, + }, +} +HeaderAndBody state +response_code: 200 +response body: None +response: HttpResponse { + state: HeaderAndBody { + response_code: 200, + status_line: "HTTP/1.1 200 OK", + headers: None, + body: None, + }, +} +response size: 80 bytes +HeaderAndBody state # 2 +response: HttpResponse { + state: HeaderAndBody { + response_code: 200, + status_line: "HTTP/1.1 200 OK", + headers: Some( + [ + ( + "Content-Type", + "text/html", + ), + ], + ), + body: Some( + "<html><body>Hello World!</body></html>", + ), + }, +} +response size: 80 bytes +Final state +response_code: 200 +status_line: HTTP/1.1 200 OK +headers: [ + ( + "Content-Type", + "text/html", + ), +] +body: <html><body>Hello World!</body></html> +response: HttpResponse { + state: Final { + response_code: 200, + status_line: "HTTP/1.1 200 OK", + headers: [ + ( + "Content-Type", + "text/html", + ), + ], + body: "<html><body>Hello World!</body></html>", + }, +} +response size: 80 bytes ++
+ + + Example 3.1: Using enum and PhantomData instead of struct # + + +
+ + +-
+
- You can use enums instead of structs if you have shared data (inner) that you move with +state transitions. +
- And you have to use
PhantomData
here.
+
Add the following code to the src/ex3_1.rs
file.
use self::type_state_builder::HttpResponse;
+use crossterm::style::Stylize;
+
+pub fn main() -> Result<(), String> {
+ let response = HttpResponse::<()>::new();
+ println!("{}", "Start state".red().bold().underlined());
+ println!("response: {:#?}", response);
+ println!(
+ "response size: {}",
+ response.get_size().to_string().blue().bold()
+ );
+
+ // Status line is required.
+ println!("{}", "HeaderAndBody state".red().bold().underlined());
+ let mut response = response.set_status_line(200, "OK");
+ println!("response_code: {}", response.get_response_code());
+ println!("response body: {:#?}", response.get_body());
+ println!("response: {:#?}", response);
+ println!(
+ "response size: {}",
+ response.get_size().to_string().blue().bold()
+ );
+
+ // Body and headers are optional.
+ println!("{}", "HeaderAndBody state # 2".red().bold().underlined());
+ response.add_header("Content-Type", "text/html");
+ response.set_body("<html><body>Hello World!</body></html>");
+ println!("response: {:#?}", response);
+ println!(
+ "response size: {}",
+ response.get_size().to_string().blue().bold()
+ );
+
+ // Final state.
+ println!("{}", "Final state".red().bold().underlined());
+ let response = response.finish();
+ println!("response_code: {}", response.get_response_code());
+ println!("status_line: {}", response.get_status_line());
+ println!("headers: {:#?}", response.get_headers());
+ println!("body: {:#?}", response.get_body());
+ println!("response: {:#?}", response);
+ println!(
+ "response size: {}",
+ response.get_size().to_string().blue().bold()
+ );
+
+ Ok(())
+}
+
Note that this main
function is the same as the one in the previous example.
The following code will be different. We are adding a new data
module.
pub mod data {
+ #[derive(Debug, Clone, Default)]
+ pub struct HttpResponseData {
+ pub response_code: u8,
+ pub status_line: String,
+ pub headers: Option<Vec<(String, String)>>,
+ pub body: Option<String>,
+ }
+}
+
Here’s the new state
module. Note the use of enums and PhantomData
instead of structs.
pub mod state {
+ #[derive(Debug, Clone)]
+ pub enum Start {}
+
+ #[derive(Debug, Clone)]
+ pub enum HeaderAndBody {}
+
+ #[derive(Debug, Clone)]
+ pub struct Final {}
+
+ // The following marker trait is used to restrict the operations
+ // that are available in each state. This isn't strictly necessary,
+ // but it's a nice thing to use in a where clause to restrict types.
+ pub trait Marker {}
+ impl Marker for () {}
+ impl Marker for Start {}
+ impl Marker for HeaderAndBody {}
+ impl Marker for Final {}
+}
+
Here is the changed code for the HttpResponse
struct.
pub mod type_state_builder {
+ use super::{
+ data::HttpResponseData,
+ state::{Final, HeaderAndBody, Marker, Start},
+ };
+ use std::marker::PhantomData;
+
+ #[derive(Debug, Clone)]
+ pub struct HttpResponse<S: Marker> {
+ pub data: HttpResponseData,
+ pub state: PhantomData<S>,
+ }
+
+ // Operations that are only valid in ().
+ impl HttpResponse<()> {
+ pub fn new() -> HttpResponse<Start> {
+ HttpResponse {
+ data: HttpResponseData::default(),
+ state: PhantomData::<Start>,
+ }
+ }
+ }
+
+ // Operations that are only valid in Start.
+ impl HttpResponse<Start> {
+ // setter.
+ pub fn set_status_line(
+ self,
+ response_code: u8,
+ message: &str,
+ ) -> HttpResponse<HeaderAndBody> {
+ HttpResponse {
+ data: {
+ let mut data = self.data;
+ data.response_code = response_code;
+ data.status_line = format!(
+ "HTTP/1.1 {} {}", response_code, message
+ );
+ data
+ },
+ state: PhantomData::<HeaderAndBody>,
+ }
+ }
+ }
+
+ // Operations that are only valid in HeaderAndBodyState.
+ impl HttpResponse<HeaderAndBody> {
+ // setter.
+ pub fn add_header(&mut self, key: &str, value: &str) {
+ let mut_data = &mut self.data;
+ if mut_data.headers.is_none() {
+ mut_data.headers.replace(Vec::new());
+ }
+ if let Some(headers) = mut_data.headers.as_mut() {
+ headers.push((key.to_string(), value.to_string()))
+ }
+ }
+
+ // getter.
+ pub fn get_response_code(&self) -> u8 {
+ self.data.response_code
+ }
+
+ // setter.
+ pub fn set_body(&mut self, body: &str) {
+ self.data.body.replace(body.to_string());
+ }
+
+ // getter.
+ pub fn get_body(&self) -> Option<&str> {
+ self.data.body.as_deref()
+ }
+
+ // transition to Final state.
+ pub fn finish(self) -> HttpResponse<Final> {
+ let mut data = self.data;
+ HttpResponse {
+ data: HttpResponseData {
+ response_code: data.response_code,
+ status_line: data.status_line.clone(),
+ headers: Some(data.headers.take().unwrap_or_default()),
+ body: Some(data.body.take().unwrap_or_default()),
+ },
+ state: PhantomData::<Final>,
+ }
+ }
+ }
+
+ // Operations that are only valid in FinalState.
+ impl HttpResponse<Final> {
+ pub fn get_headers(&self) -> &Option<Vec<(String, String)>> {
+ &self.data.headers
+ }
+
+ pub fn get_body(&self) -> &Option<String> {
+ &self.data.body
+ }
+
+ pub fn get_response_code(&self) -> u8 {
+ self.data.response_code
+ }
+
+ pub fn get_status_line(&self) -> &str {
+ &self.data.status_line
+ }
+ }
+
+ // Operations that are available in all states.
+ impl<S> HttpResponse<S>
+ where
+ S: Marker,
+ {
+ pub fn get_size(&self) -> String {
+ let len = std::mem::size_of_val(self);
+ format!("{} bytes", len)
+ }
+ }
+}
+
Here’s the output when you run the code using cargo run --bin ex3_1
.
$ cargo run --bin ex3_1 +Start state +response: HttpResponse { + data: HttpResponseData { + response_code: 0, + status_line: "", + headers: None, + body: None, + }, + state: PhantomData<ex3_1::state::Start>, +} +response size: 80 bytes +HeaderAndBody state +response_code: 200 +response body: None +response: HttpResponse { + data: HttpResponseData { + response_code: 200, + status_line: "HTTP/1.1 200 OK", + headers: None, + body: None, + }, + state: PhantomData<ex3_1::state::HeaderAndBody>, +} +response size: 80 bytes +HeaderAndBody state # 2 +response: HttpResponse { + data: HttpResponseData { + response_code: 200, + status_line: "HTTP/1.1 200 OK", + headers: Some( + [ + ( + "Content-Type", + "text/html", + ), + ], + ), + body: Some( + "<html><body>Hello World!</body></html>", + ), + }, + state: PhantomData<ex3_1::state::HeaderAndBody>, +} +response size: 80 bytes +Final state +response_code: 200 +status_line: HTTP/1.1 200 OK +headers: Some( + [ + ( + "Content-Type", + "text/html", + ), + ], +) +body: Some( + "<html><body>Hello World!</body></html>", +) +response: HttpResponse { + data: HttpResponseData { + response_code: 200, + status_line: "HTTP/1.1 200 OK", + headers: Some( + [ + ( + "Content-Type", + "text/html", + ), + ], + ), + body: Some( + "<html><body>Hello World!</body></html>", + ), + }, + state: PhantomData<ex3_1::state::Final>, +} +response size: 80 bytes ++
+ + + Parting thoughts # + + +
+ + + +To get an experiential understanding of the typestate pattern, you should try to build +something using it. It’s a powerful pattern that can help you write more robust and +predictable code. And it’s a great way to leverage Rust’s type system to enforce state +transitions in your code. I encourage you to clone the repo and run the code to see how it +works. And make changes to it to see if you can make it behave differently and use it in +your own projects.
++ + + Build with Naz video series on developerlife.com YouTube channel # + + +
+ + + +You can watch a video series on building this crate with Naz on the +developerlife.com YouTube channel.
+ +-
+
- YT channel +
- Playlists + + +
+ +📦 Install our useful Rust command line apps using+ + + + + +cargo install r3bl-cmdr
+(they are from the r3bl-open-core +project): ++
+ +- 🐱
+giti
: run interactive git commands with confidence in your terminal- 🦜
+edi
: edit Markdown with style in your terminal+giti in action + +
+ ++edi in action + +
+ +