Rust Builder Pattern Procedural Macro

本文档详细解释了如何使用 Rust 的过程宏(procedural macro)来实现构建者模式(Builder Pattern)。这个实现类似于 derive_builder crate 的功能,支持常规设置方法、链式调用以及处理重复字段。

设计目标

这个 Builder 宏旨在实现以下功能:

  1. 为结构体自动生成一个对应的 Builder 结构体
  2. 支持常规的 setter 方法和链式调用
  3. 处理 Vec 类型的重复字段,提供单项添加方法
  4. 对必填字段进行验证,仅允许在所有必填字段都设置后构建
  5. Option 类型字段提供特殊处理,使其成为可选字段

直接上完整项目结构

代码解析请看后续

macrobuilder
├── Cargo.lock
├── Cargo.toml
├── derive_builder
│  ├── Cargo.toml
│  └── src
│     └── lib.rs
├── README.md
└── testbuilder
   ├── Cargo.toml
   └── src
      └── main.rs

macrobuilder

这是个 workspace

./Cargo.toml

[workspace]
resolver = "3"
members = ["derive_builder", "testbuilder"]

derive_builder

./derive_builder/Cargo.toml

[package]
name = "derive_builder"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true

[dependencies]
darling = "0.20.11"
proc-macro2 = "1.0.95"
quote = "1.0.40"
syn = { version = "2.0.101", features = ["full", "extra-traits"] }

./derive_builder/src/lib.rs

use darling::{FromDeriveInput, FromField};
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::{DeriveInput, Ident, Type};

/// 定义从字段提取的属性信息结构
#[derive(Debug, FromField)]
#[darling(attributes(builder))]
struct BuilderFieldOptions {
    /// 是否跳过该字段
    #[darling(default)]
    skip: bool,

    /// "each" 属性,用于处理 Vec 类型的重复字段
    /// 如 #[builder(each = "arg")] args: Vec<String>
    #[darling(default)]
    each: Option<String>,
}

/// 定义从派生输入提取的属性信息结构
#[allow(unused)]
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(builder), supports(struct_named))]
struct BuilderOptions {
    ident: Ident,
    data: darling::ast::Data<(), BuilderField>,
}

/// 表示构建器中的一个字段
#[allow(unused)]
#[derive(Debug, FromField)]
#[darling(attributes(builder))]
struct BuilderField {
    ident: Option<Ident>,
    ty: Type,
    #[darling(default)]
    skip: bool,
    #[darling(default)]
    each: Option<String>,
}

/// 主要的派生宏入口点
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // 解析输入的 TokenStream 为 DeriveInput
    let input = syn::parse_macro_input!(input as DeriveInput);

    // 提取结构体名称和字段
    let name = &input.ident;
    let builder_name = quote::format_ident!("{}Builder", name);

    // 从输入中提取字段
    let fields = match &input.data {
        syn::Data::Struct(data) => match &data.fields {
            syn::Fields::Named(fields) => &fields.named,
            _ => panic!("Builder 只支持命名字段的结构体"),
        },
        _ => panic!("Builder 只支持结构体"),
    };

    // 处理每个字段,生成对应的构建器字段、setter 方法等
    let mut builder_fields = std::vec::Vec::new();
    let mut builder_init = std::vec::Vec::new();
    let mut builder_setters = std::vec::Vec::new();
    let mut builder_build_fields = std::vec::Vec::new();

    for field in fields {
        // 获取字段名称和类型
        let field_name = field.ident.as_ref().unwrap();
        let field_type = &field.ty;

        // 解析字段上的 #[builder(...)] 属性
        let options = match BuilderFieldOptions::from_field(field) {
            std::result::Result::Ok(options) => options,
            std::result::Result::Err(e) => panic!("解析字段属性失败:{}", e),
        };

        // 如果设置了 skip,则跳过该字段
        if options.skip {
            continue;
        }

        // 检查是否是 Vec 类型且有 each 属性
        let is_vec = if let syn::Type::Path(type_path) = &field_type {
            let last_segment = type_path.path.segments.last().unwrap();
            last_segment.ident == "Vec"
        } else {
            false
        };

        // 确定构建器中字段的类型
        let builder_field_type = if is_option_type(field_type) {
            // 如果是 Option<T>,构建器中的类型也是 Option<T>
            quote! { #field_type }
        } else {
            // 否则,构建器中的类型是 Option<T>
            quote! { std::option::Option<#field_type> }
        };

        // 添加构建器字段
        builder_fields.push(quote! {
            #field_name: #builder_field_type
        });

        // 初始化构建器字段为 None
        builder_init.push(quote! {
            #field_name: std::option::Option::None
        });

        let setter_value_type = if is_option_type(field_type) {
            extract_option_inner_type(field_type)
        } else {
            quote! { #field_type }
        };

        // 为字段生成 setter 方法
        let regular_setter = quote! {
            // 常规 setter 方法,允许直接设置整个字段值
            pub fn #field_name(&mut self, #field_name: #setter_value_type) -> &mut Self {
                self.#field_name = std::option::Option::Some(#field_name);
                self
            }
        };
        builder_setters.push(regular_setter);

        // 如果是 Vec 类型且有 each 属性,添加单项添加方法
        if is_vec && options.each.is_some() {
            let each_name =
                proc_macro2::Ident::new(&options.each.unwrap(), proc_macro2::Span::call_site());

            // 如果 each_name 和 field_name 是不一样的,那么就生成 each 的方法
            if each_name.to_string() != field_name.to_string() {
                // 提取 Vec 的内部类型
                let inner_type = extract_vec_inner_type(field_type);

                // 添加单项添加方法
                let each_setter = quote! {
                    // 单项添加方法,用于向 Vec 中添加单个元素
                    pub fn #each_name(&mut self, item: #inner_type) -> &mut Self {
                        if self.#field_name.is_none() {
                            self.#field_name = std::option::Option::Some(std::vec::Vec::new());
                        }
                        self.#field_name.as_mut().unwrap().push(item);
                        self
                    }
                };
                builder_setters.push(each_setter);
            }
        }

        // 生成构建时的字段处理逻辑
        if is_option_type(field_type) {
            // 对于 Option 类型,如果 builder 中是 None 就保持 None
            builder_build_fields.push(quote! {
                #field_name: self.#field_name.clone()
            });
        } else {
            // 对于非 Option 类型,如果 builder 中是 None 就返回错误
            builder_build_fields.push(quote! {
                // ok_or(concat!(stringify!(#field_name), " is not set"))
                #field_name: self.#field_name.clone().unwrap_or_default()
            });
        }
    }

    // 生成最终的构建器代码
    let expanded = quote! {
        // 定义构建器结构体
        pub struct #builder_name {
            #(#builder_fields,)*
        }

        // 为主结构体实现 builder 方法
        impl #name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#builder_init,)*
                }
            }
        }

        // 为构建器实现各种方法
        impl #builder_name {
            // 生成 setter 方法
            #(#builder_setters)*

            // 构建方法,创建最终的结构体实例
            pub fn build(&self) -> std::result::Result<#name, std::string::String> {
                std::result::Result::Ok(#name {
                    #(#builder_build_fields,)*
                })
            }
        }
    };

    expanded.into()
}

/// 检查类型是否为 Option<T>
fn is_option_type(ty: &syn::Type) -> std::primitive::bool {
    if let syn::Type::Path(type_path) = ty {
        if let std::option::Option::Some(segment) = type_path.path.segments.last() {
            return segment.ident == "Option";
        }
    }
    false
}

/// 从 Vec<T> 中提取 T 的类型
fn extract_vec_inner_type(ty: &syn::Type) -> proc_macro2::TokenStream {
    extract_generic_inner_type(ty, "Vec")
}

/// 从 Option<T> 中提取 T 的类型
fn extract_option_inner_type(ty: &syn::Type) -> proc_macro2::TokenStream {
    extract_generic_inner_type(ty, "Option")
}

/// 从 Generic<T> 中提取 T 的类型
fn extract_generic_inner_type(
    ty: &syn::Type,
    generic_name: &std::primitive::str,
) -> proc_macro2::TokenStream {
    if let syn::Type::Path(type_path) = ty {
        if let std::option::Option::Some(segment) = type_path.path.segments.last() {
            if segment.ident == generic_name {
                if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
                    if let std::option::Option::Some(arg) = args.args.first() {
                        return arg.to_token_stream();
                    }
                }
            }
        }
    }

    // 如果无法提取,返回一般 TokenStream
    quote! { () }
}

testbuilder

./testbuilder/Cargo.toml

[package]
name = "testbuilder"
version = "0.1.0"
edition = "2024"

[dependencies]
derive_builder = { path = "../derive_builder" }

./testbuilder/src/main.rs

use derive_builder::Builder;

#[derive(Builder, Debug)]
pub struct Command {
    executable: String,
    #[builder(each = "arg")]
    args: Vec<String>,
    #[builder(each = "env")]
    env: Vec<String>,
    current_dir: Option<String>,
}

fn main() {
    // 测试个别 setter 方法
    println!("测试 setter 方法:");
    let mut builder = Command::builder();
    builder.executable("cargo".to_owned());
    builder.args(vec!["build".to_owned(), "--release".to_owned()]);
    builder.env(vec![]);
    // 注意未设置 current_dir,因为它是 Option<String> 类型

    let command = builder.build().unwrap();
    println!("Command: {:?}", command);
    assert_eq!(command.executable, "cargo");
    assert_eq!(command.current_dir, None);

    // 测试链式调用
    println!("\n测试链式调用:");
    let command = Command::builder()
        .executable("cargo".to_owned())
        .args(vec!["build".to_owned(), "--release".to_owned()])
        .env(vec![])
        .current_dir("..".to_owned())
        .build()
        .unwrap();

    println!("Command: {:?}", command);
    assert_eq!(command.executable, "cargo");
    assert_eq!(command.current_dir, Some("..".to_owned()));

    // 测试重复字段的 each 方法
    println!("\n测试重复字段:");
    let command = Command::builder()
        .executable("cargo".to_owned())
        .arg("build".to_owned())
        .arg("--release".to_owned())
        .build()
        .unwrap();

    println!("Command: {:?}", command);
    assert_eq!(command.executable, "cargo");
    assert_eq!(command.args, vec!["build", "--release"]);
}

宏展开

利用 cargo-expand 这个 cli 工具便可以将宏进行展开, 以便于查看和理解, 同时应用于调试

# 安装 cargo-expand
cargo install cargo-expand

使用 cargo-expand

cd .\testbuilder
cargo expand

会查看到 .\testbuilder\src\main.rs 的宏展开后的完整代码,
根据完整代码便可以去分析宏的问题, 进而做出修改, 并且后续再通过 cargo expand 进行展开分析.

如下面所示

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
use derive_builder::Builder;
pub struct Command {
    executable: String,
    #[builder(each = "arg")]
    args: Vec<String>,
    #[builder(each = "env")]
    env: Vec<String>,
    current_dir: Option<String>,
}
pub struct CommandBuilder {
    executable: std::option::Option<String>,
    args: std::option::Option<Vec<String>>,
    env: std::option::Option<Vec<String>>,
    current_dir: Option<String>,
}
impl Command {
    pub fn builder() -> CommandBuilder {
        CommandBuilder {
            executable: std::option::Option::None,
            args: std::option::Option::None,
            env: std::option::Option::None,
            current_dir: std::option::Option::None,
        }
    }
}
impl CommandBuilder {
    pub fn executable(&mut self, executable: String) -> &mut Self {
        self.executable = std::option::Option::Some(executable);
        self
    }
    pub fn args(&mut self, args: Vec<String>) -> &mut Self {
        self.args = std::option::Option::Some(args);
        self
    }
    pub fn arg(&mut self, item: String) -> &mut Self {
        if self.args.is_none() {
            self.args = std::option::Option::Some(std::vec::Vec::new());
        }
        self.args.as_mut().unwrap().push(item);
        self
    }
    pub fn env(&mut self, env: Vec<String>) -> &mut Self {
        self.env = std::option::Option::Some(env);
        self
    }
    pub fn current_dir(&mut self, current_dir: String) -> &mut Self {
        self.current_dir = std::option::Option::Some(current_dir);
        self
    }
    pub fn build(&self) -> std::result::Result<Command, std::string::String> {
        std::result::Result::Ok(Command {
            executable: self.executable.clone().unwrap_or_default(),
            args: self.args.clone().unwrap_or_default(),
            env: self.env.clone().unwrap_or_default(),
            current_dir: self.current_dir.clone(),
        })
    }
}
#[automatically_derived]
impl ::core::fmt::Debug for Command {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field4_finish(
            f,
            "Command",
            "executable",
            &self.executable,
            "args",
            &self.args,
            "env",
            &self.env,
            "current_dir",
            &&self.current_dir,
        )
    }
}
fn main() {
    {
        ::std::io::_print(format_args!("测试 setter 方法:\n"));
    };
    let mut builder = Command::builder();
    builder.executable("cargo".to_owned());
    builder
        .args(
            <[_]>::into_vec(
                ::alloc::boxed::box_new(["build".to_owned(), "--release".to_owned()]),
            ),
        );
    builder.env(::alloc::vec::Vec::new());
    let command = builder.build().unwrap();
    {
        ::std::io::_print(format_args!("Command: {0:?}\n", command));
    };
    match (&command.executable, &"cargo") {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    match (&command.current_dir, &None) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    {
        ::std::io::_print(format_args!("\n测试链式调用:\n"));
    };
    let command = Command::builder()
        .executable("cargo".to_owned())
        .args(
            <[_]>::into_vec(
                ::alloc::boxed::box_new(["build".to_owned(), "--release".to_owned()]),
            ),
        )
        .env(::alloc::vec::Vec::new())
        .current_dir("..".to_owned())
        .build()
        .unwrap();
    {
        ::std::io::_print(format_args!("Command: {0:?}\n", command));
    };
    match (&command.executable, &"cargo") {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    match (&command.current_dir, &Some("..".to_owned())) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    {
        ::std::io::_print(format_args!("\n测试重复字段:\n"));
    };
    let command = Command::builder()
        .executable("cargo".to_owned())
        .arg("build".to_owned())
        .arg("--release".to_owned())
        .build()
        .unwrap();
    {
        ::std::io::_print(format_args!("Command: {0:?}\n", command));
    };
    match (&command.executable, &"cargo") {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    match (
        &command.args,
        &<[_]>::into_vec(::alloc::boxed::box_new(["build", "--release"])),
    ) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
}

宏代码解析

关键数据结构

  1. BuilderFieldOptions - 用于从字段属性中提取配置

    • skip: 是否跳过该字段
    • each: 用于处理重复字段,指定单项添加方法的名称
  2. BuilderOptions - 从派生输入中提取信息

    • 保存结构体标识符和解析后的字段数据
  3. BuilderField - 表示构建器中的一个字段

    • 包含字段名称、类型和属性设置

工作流程

  1. 解析输入 - 解析传入的 TokenStream 为 AST(抽象语法树)
  2. 提取字段 - 从结构体定义中提取所有字段
  3. 处理字段 - 为每个字段生成对应的构建器字段和 setter 方法
  4. 生成代码 - 使用 quote 宏生成最终的构建器代码

字段处理逻辑

  • 普通字段 - 在构建器中转换为 Option<T> 类型,提供基本 setter 方法
  • 已是 Option 的字段 - 保持 Option 类型,在构建时如果为 None 则保持 None
  • Vec 类型字段 - 如果设置了 each 属性,生成额外的单项添加方法

使用方法

基本用法

use my_builder_macro::Builder;

#[derive(Builder, Debug)]
pub struct Person {
    name: String,
    age: u32,
    address: Option<String>,
}

fn main() {
    let person = Person::builder()
        .name("张三".to_string())
        .age(30)
        .build()
        .unwrap();
    
    println!("{:?}", person); // Person { name: "张三", age: 30, address: None }
}

处理重复字段 (Vec)

使用 each 属性为 Vec 类型字段添加单项添加方法:

#[derive(Builder, Debug)]
pub struct Command {
    executable: String,
    #[builder(each = "arg")]
    args: Vec<String>,
}

fn main() {
    // 使用单项添加方法
    let cmd = Command::builder()
        .executable("cargo".to_string())
        .arg("build".to_string())
        .arg("--release".to_string())
        .build()
        .unwrap();
    
    // 或者一次性添加整个集合
    let cmd2 = Command::builder()
        .executable("cargo".to_string())
        .args(vec!["build".to_string(), "--release".to_string()])
        .build()
        .unwrap();
}

可选字段

对于 Option<T> 类型的字段,如果未设置则默认为 None

#[derive(Builder, Debug)]
pub struct Config {
    name: String,
    timeout: Option<u32>,
}

fn main() {
    // timeout 是可选的,不设置也能成功构建
    let config = Config::builder()
        .name("app".to_string())
        .build()
        .unwrap();
    
    assert_eq!(config.timeout, None);
}

实现细节

处理必填字段

在构建过程中,对于非 Option 类型的字段,如果未设置则会返回错误:

impl #builder_name {
    pub fn build(&self) -> Result<#name, String> {
        Ok(#name {
            #(#builder_build_fields,)*
        })
    }
}

其中,对于非 Option 类型的字段,生成类似于:

field_name: self.field_name.clone().ok_or("field_name is not set")?

处理 Vec 类型和 each 属性

对于 Vec 类型且有 each 属性的字段,生成额外的单项添加方法:

pub fn item_name(&mut self, item: ItemType) -> &mut Self {
    if self.field_name.is_none() {
        self.field_name = Some(Vec::new());
    }
    self.field_name.as_mut().unwrap().push(item);
    self
}

最佳实践

  1. 明确标记可选字段 - 使用 Option<T> 明确哪些字段是可选的
  2. 为集合类型添加 each 属性 - 对于 Vec 类型字段,使用 #[builder(each = "xxx")] 提供更灵活的接口
  3. 使用描述性名称 - 为 each 属性选择清晰、描述性的名称
  4. 处理所有错误 - 在使用 builder 时,适当处理 build() 可能返回的错误

高级定制

这个实现可以进一步扩展支持以下功能:

  1. 默认值 - 添加 default 属性,为字段提供默认值
  2. 验证函数 - 添加属性来指定验证函数,在构建时验证字段值
  3. 自定义错误类型 - 使用自定义错误类型而不是简单的字符串
  4. 私有字段访问 - 处理结构体中的私有字段