Builder pattern in Rust with Default
derive_builder is fine and I use it sometimes. For the “public client library” case where I want small, readable, reviewable generated code, I hand-roll the builder. The trick is to use Default for the builder struct and thread the config through a build() method. It is three times as much code as derive_builder and zero macro magic to read later.
#[derive(Debug, Clone)]
pub struct Client {
pub endpoint: String,
pub timeout: std::time::Duration,
pub retries: u32,
}
#[derive(Debug, Default)]
pub struct ClientBuilder {
endpoint: Option<String>,
timeout: Option<std::time::Duration>,
retries: Option<u32>,
}
impl ClientBuilder {
pub fn new() -> Self { Self::default() }
pub fn endpoint(mut self, v: impl Into<String>) -> Self {
self.endpoint = Some(v.into());
self
}
pub fn timeout(mut self, v: std::time::Duration) -> Self {
self.timeout = Some(v);
self
}
pub fn retries(mut self, v: u32) -> Self {
self.retries = Some(v);
self
}
pub fn build(self) -> Result<Client, &'static str> {
Ok(Client {
endpoint: self.endpoint.ok_or("endpoint is required")?,
timeout: self.timeout.unwrap_or(std::time::Duration::from_secs(30)),
retries: self.retries.unwrap_or(3),
})
}
}
// Usage:
let c = ClientBuilder::new()
.endpoint("https://example.com")
.timeout(std::time::Duration::from_secs(5))
.build()?;
impl Into<String> lets callers pass &str or String. Required vs optional is encoded in build(). See also /snippets/rust-safe-env-macro/.