ExamplesBy LevelBy TopicLearning Paths
416 Advanced

416: Macro-Generated Builder Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "416: Macro-Generated Builder Pattern" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. The builder pattern reduces construction errors for structs with many optional fields, but implementing it manually requires writing setter methods for every field — tedious and error-prone to keep in sync with the struct itself. Key difference from OCaml: 1. **No boilerplate**: OCaml's optional parameters eliminate the builder pattern entirely for most use cases; Rust's lack of optional parameters motivates the builder pattern and macro generation.

Tutorial

The Problem

The builder pattern reduces construction errors for structs with many optional fields, but implementing it manually requires writing setter methods for every field — tedious and error-prone to keep in sync with the struct itself. Macros can eliminate this boilerplate: define the struct once with field metadata, and a macro generates the builder struct, all setter methods, and the build() method. This keeps the struct definition as the single source of truth.

Builder patterns appear in reqwest::Client::builder(), tokio::runtime::Builder, clap::Command, and any library with complex configuration objects.

🎯 Learning Outcomes

  • • Understand how macros can generate complete builder patterns from a single declaration
  • • Learn how $vis:vis captures visibility modifiers in macros
  • • See how paste! or identifier manipulation enables naming the generated builder type
  • • Understand the required vs. optional field distinction in macro-generated builders
  • • Learn how macro-generated code maintains synchronization between struct and builder
  • Code Example

    macro_rules! builder_setters {
        ($($field:ident : $ty:ty),*) => {
            $(
                pub fn $field(mut self, val: $ty) -> Self {
                    self.$field = Some(val);
                    self
                }
            )*
        };
    }
    
    struct RequestBuilder {
        url: Option<String>,
        method: Option<String>,
    }
    
    impl RequestBuilder {
        builder_setters!(url: String, method: String);
    
        pub fn build(self) -> Result<Request, Error> { ... }
    }

    Key Differences

  • No boilerplate: OCaml's optional parameters eliminate the builder pattern entirely for most use cases; Rust's lack of optional parameters motivates the builder pattern and macro generation.
  • Type safety: Rust macro builders can enforce required fields via Option and Result; OCaml optional parameters always have defaults (cannot be "required optional").
  • Code generation: Rust macros generate real Rust code visible to the compiler; OCaml's approach uses language features rather than generation.
  • Maintenance: Rust macro builders keep struct and builder in sync automatically; OCaml function signatures must be manually updated.
  • OCaml Approach

    OCaml achieves builder-like construction through optional function parameters: let make_request ?(timeout=30) ?(headers=[]) ~url () = .... This requires no code generation — the function signature itself is the builder interface. For more complex cases, OCaml uses a record with optional fields and a make function. The PPX ppx_fields_conv generates field accessors automatically from record type definitions.

    Full Source

    #![allow(clippy::all)]
    //! Macro-Generated Builder Pattern
    //!
    //! Using macros to reduce boilerplate in builder patterns.
    
    /// Generate setter methods for builder fields.
    #[macro_export]
    macro_rules! builder_setters {
        ($($field:ident : $ty:ty),* $(,)?) => {
            $(
                pub fn $field(mut self, val: $ty) -> Self {
                    self.$field = Some(val);
                    self
                }
            )*
        };
    }
    
    /// Generate a builder with required and optional fields.
    #[macro_export]
    macro_rules! define_builder {
        (
            $vis:vis struct $name:ident {
                $(required $req:ident : $req_ty:ty,)*
                $(optional $opt:ident : $opt_ty:ty = $default:expr,)*
            }
        ) => {
            #[derive(Debug, Clone)]
            $vis struct $name {
                $($req: $req_ty,)*
                $($opt: $opt_ty,)*
            }
    
            paste::item! {
                #[derive(Default)]
                $vis struct [<$name Builder>] {
                    $($req: Option<$req_ty>,)*
                    $($opt: Option<$opt_ty>,)*
                }
            }
        };
    }
    
    /// HTTP Request for demonstration.
    #[derive(Debug, Clone)]
    pub struct HttpRequest {
        pub url: String,
        pub method: String,
        pub timeout_ms: u32,
        pub max_retries: u8,
        pub headers: Vec<(String, String)>,
    }
    
    /// Builder for HttpRequest.
    #[derive(Default)]
    pub struct HttpRequestBuilder {
        url: Option<String>,
        method: Option<String>,
        timeout_ms: Option<u32>,
        max_retries: Option<u8>,
        headers: Vec<(String, String)>,
    }
    
    impl HttpRequestBuilder {
        builder_setters!(url: String, method: String, timeout_ms: u32, max_retries: u8);
    
        pub fn header(mut self, key: &str, value: &str) -> Self {
            self.headers.push((key.to_string(), value.to_string()));
            self
        }
    
        pub fn build(self) -> Result<HttpRequest, &'static str> {
            Ok(HttpRequest {
                url: self.url.ok_or("url is required")?,
                method: self.method.unwrap_or_else(|| "GET".to_string()),
                timeout_ms: self.timeout_ms.unwrap_or(5000),
                max_retries: self.max_retries.unwrap_or(3),
                headers: self.headers,
            })
        }
    }
    
    impl HttpRequest {
        pub fn builder() -> HttpRequestBuilder {
            HttpRequestBuilder::default()
        }
    }
    
    /// Email message builder example.
    #[derive(Debug, Clone)]
    pub struct Email {
        pub to: String,
        pub subject: String,
        pub body: String,
        pub cc: Vec<String>,
    }
    
    #[derive(Default)]
    pub struct EmailBuilder {
        to: Option<String>,
        subject: Option<String>,
        body: Option<String>,
        cc: Vec<String>,
    }
    
    impl EmailBuilder {
        builder_setters!(to: String, subject: String, body: String);
    
        pub fn cc(mut self, addr: &str) -> Self {
            self.cc.push(addr.to_string());
            self
        }
    
        pub fn build(self) -> Result<Email, &'static str> {
            Ok(Email {
                to: self.to.ok_or("to is required")?,
                subject: self.subject.ok_or("subject is required")?,
                body: self.body.unwrap_or_default(),
                cc: self.cc,
            })
        }
    }
    
    impl Email {
        pub fn builder() -> EmailBuilder {
            EmailBuilder::default()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_http_request_builder() {
            let req = HttpRequest::builder()
                .url("https://api.example.com".to_string())
                .build()
                .unwrap();
            assert_eq!(req.url, "https://api.example.com");
            assert_eq!(req.method, "GET");
        }
    
        #[test]
        fn test_http_request_with_options() {
            let req = HttpRequest::builder()
                .url("https://api.example.com".to_string())
                .method("POST".to_string())
                .timeout_ms(10000)
                .build()
                .unwrap();
            assert_eq!(req.method, "POST");
            assert_eq!(req.timeout_ms, 10000);
        }
    
        #[test]
        fn test_http_request_with_headers() {
            let req = HttpRequest::builder()
                .url("https://api.example.com".to_string())
                .header("Authorization", "Bearer token")
                .header("Content-Type", "application/json")
                .build()
                .unwrap();
            assert_eq!(req.headers.len(), 2);
        }
    
        #[test]
        fn test_http_request_missing_url() {
            let result = HttpRequest::builder().build();
            assert!(result.is_err());
        }
    
        #[test]
        fn test_email_builder() {
            let email = Email::builder()
                .to("user@example.com".to_string())
                .subject("Hello".to_string())
                .body("World".to_string())
                .build()
                .unwrap();
            assert_eq!(email.to, "user@example.com");
            assert_eq!(email.subject, "Hello");
        }
    
        #[test]
        fn test_email_with_cc() {
            let email = Email::builder()
                .to("user@example.com".to_string())
                .subject("Test".to_string())
                .cc("cc1@example.com")
                .cc("cc2@example.com")
                .build()
                .unwrap();
            assert_eq!(email.cc.len(), 2);
        }
    
        #[test]
        fn test_email_missing_required() {
            let result = Email::builder().to("user@example.com".to_string()).build();
            assert!(result.is_err());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_http_request_builder() {
            let req = HttpRequest::builder()
                .url("https://api.example.com".to_string())
                .build()
                .unwrap();
            assert_eq!(req.url, "https://api.example.com");
            assert_eq!(req.method, "GET");
        }
    
        #[test]
        fn test_http_request_with_options() {
            let req = HttpRequest::builder()
                .url("https://api.example.com".to_string())
                .method("POST".to_string())
                .timeout_ms(10000)
                .build()
                .unwrap();
            assert_eq!(req.method, "POST");
            assert_eq!(req.timeout_ms, 10000);
        }
    
        #[test]
        fn test_http_request_with_headers() {
            let req = HttpRequest::builder()
                .url("https://api.example.com".to_string())
                .header("Authorization", "Bearer token")
                .header("Content-Type", "application/json")
                .build()
                .unwrap();
            assert_eq!(req.headers.len(), 2);
        }
    
        #[test]
        fn test_http_request_missing_url() {
            let result = HttpRequest::builder().build();
            assert!(result.is_err());
        }
    
        #[test]
        fn test_email_builder() {
            let email = Email::builder()
                .to("user@example.com".to_string())
                .subject("Hello".to_string())
                .body("World".to_string())
                .build()
                .unwrap();
            assert_eq!(email.to, "user@example.com");
            assert_eq!(email.subject, "Hello");
        }
    
        #[test]
        fn test_email_with_cc() {
            let email = Email::builder()
                .to("user@example.com".to_string())
                .subject("Test".to_string())
                .cc("cc1@example.com")
                .cc("cc2@example.com")
                .build()
                .unwrap();
            assert_eq!(email.cc.len(), 2);
        }
    
        #[test]
        fn test_email_missing_required() {
            let result = Email::builder().to("user@example.com".to_string()).build();
            assert!(result.is_err());
        }
    }

    Deep Comparison

    OCaml vs Rust: Macro Builder Pattern

    The Pattern

    Builders solve the "many optional parameters" problem:

  • • Required fields must be set
  • • Optional fields have defaults
  • • Method chaining for ergonomics

  • Rust Macro Approach

    macro_rules! builder_setters {
        ($($field:ident : $ty:ty),*) => {
            $(
                pub fn $field(mut self, val: $ty) -> Self {
                    self.$field = Some(val);
                    self
                }
            )*
        };
    }
    
    struct RequestBuilder {
        url: Option<String>,
        method: Option<String>,
    }
    
    impl RequestBuilder {
        builder_setters!(url: String, method: String);
    
        pub fn build(self) -> Result<Request, Error> { ... }
    }
    

    OCaml Approach

    (* Using labeled arguments with defaults *)
    let make_request
        ~url
        ?(method_="GET")
        ?(timeout=5000)
        () =
      { url; method_; timeout }
    
    let req = make_request ~url:"http://..." ~timeout:10000 ()
    

    5 Takeaways

  • Macros eliminate setter boilerplate.
  • One line per field instead of five.

  • OCaml's labeled args are simpler.
  • ?field=default achieves similar ergonomics.

  • Rust builders catch missing required fields.
  • build() returns Result if validation fails.

  • Method chaining is idiomatic in Rust.
  • .field(value).other(value).build()
  • Macros can generate entire builders.
  • Advanced macros create both struct and builder.

    Exercises

  • HTTP request builder: Use define_builder! to create an HttpRequest builder with required url: String and optional method: String = "GET".to_string(), timeout: u64 = 30, and headers: Vec<String> = vec![].
  • Validation in build: Extend the builder so build() returns Result<T, Vec<String>> with all validation errors collected (not just the first). Generate the validation logic from the macro for required fields.
  • Nested builder: Design a macro that supports nested builders: define_builder!(Server { required host: String, optional db: DatabaseConfig = ... }) where DatabaseConfig itself has a builder, and the Server builder exposes db_builder() for fluent nested configuration.
  • Open Source Repos