Crates.io | builder-pattern-fsm |
lib.rs | builder-pattern-fsm |
version | 0.0.1 |
source | src |
created_at | 2023-09-27 03:30:09.470104 |
updated_at | 2023-09-27 03:30:09.470104 |
description | Finite state machine based builder pattern generator |
homepage | |
repository | |
max_upload_size | |
id | 984267 |
size | 18,674 |
This repository endeavors to develop a builder derive macro that is more intelligent and cognizant of the existing builder context.
Consider having a Rust struct as follows:
struct User {
name: &'static str,
age: Option<u32>,
address: Option<&'static str>,
}
Now, suppose you wish to implement a builder pattern for this struct; it could be crafted as follows:
impl User {
pub fn builder() -> UserBuilder {
UserBuilder::default()
}
}
#[derive(Default)]
pub struct UserBuilder {
name: Option<&'static str>,
age: Option<u32>,
address: Option<&'static str>,
}
impl UserBuilder {
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
pub fn age(mut self, age: u32) -> Self {
self.age = Some(age);
self
}
pub fn address(mut self, address: &'static str) -> Self {
self.address = Some(address);
self
}
pub fn build(self) -> Result<User, &'static str> {
let name = self.name.ok_or("Name is required")?;
Ok(User {
name,
age: self.age,
address: self.address,
})
}
}
which can be later use like this:
let user = User::builder()
.name("Alice")
.age(30)
.address("Wonderland")
.build()
.unwrap();
println!("User: {:?}", user);
Regardless of whether you favor this approach, it presents several challenges:
Result
Although the builder technically knows its state, we are compelled to return the User
instance, constructed by the builder, wrapped in a Result
This results in the following sequence of methods being entirely feasible:
let user = User::builder()
.name("Alice")
.age(30)
.age(20)
.address("Wonderland")
.age(333)
.build()
.unwrap();
This issue stems from the previous one; when I enter:
let user = User::builder()
.name("Alice")
.
Upon receiving this .
input, the language server protocol (LSP
) proposes the complete list of builder methods - name
, age
, and address
, even though name
has just been set. Furthermore, the visibility of the build
method is unrestricted - you can invoke it in the midst of the build process, potentially leading to a panic
when attempting tounwrap
the result.
Hence, introducing a new field to the User
struct will necessitate corresponding adjustments on the Builder
side.
This repository offers a solution to these issues by providing a proc_macro that automatically generates the Builder implementation for the struct, devoid of the aforementioned problems.
#[derive(nicer_builder::Builder)]
struct User {
name: &'static str,
age: Option<u32>,
address: Option<&'static str>,
}
let alice = User::builder()
.with_address("SF")
.with_age(10)
.with_name("alice")
.build();
Notably, the build method no longer returns a Result
; instead, it directly returns the actual User
instance, and it is guaranteed not to fail at compile time.
The proc_macro
provided by this crate essentially generates a comprehensive state machine. Each node of this machine contains a dedicated sub-builder implementation, defining its own set of methods. In simpler terms, the proc_macro
would generate approximately 2^(number of fields)
new structs, which can become costly quicker than one might prefer.