proc-macro-workshop:builder-2

结构

为了方便针对性的进行解答和归纳,后续结构目录会定义成如下的方式

proc-macro-workshop:builder-2

  • common:定义通用的方法
  • solutionX:每一道题的题解

因此,原来的基础方法变成了这样

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    solution1(input)
}

后续根据在针对的地方进行修改,以便于表明每道题的用意

审题

// Have the macro produce a struct for the builder state, and a `builder`
// function that creates an empty instance of the builder.
//
// As a quick start, try generating the following code (but make sure the type
// name matches what is in the caller's input).
//
//     impl Command {
//         pub fn builder() {}
//     }
//
// At this point the test should pass because it isn't doing anything with the
// builder yet, so `()` as the builder type is as good as any other.
//
// Before moving on, have the macro also generate:
//
//     pub struct CommandBuilder {
//         executable: Option<String>,
//         args: Option<Vec<String>>,
//         env: Option<Vec<String>>,
//         current_dir: Option<String>,
//     }
//
// and in the `builder` function:
//
//     impl Command {
//         pub fn builder() -> CommandBuilder {
//             CommandBuilder {
//                 executable: None,
//                 args: None,
//                 env: None,
//                 current_dir: None,
//             }
//         }
//     }
//
//
// Resources:
//
//   - The Quote crate for putting together output from a macro:
//     https://github.com/dtolnay/quote
//
//   - Joining together the type name + "Builder" to make the builder's name:
//     https://docs.rs/syn/1.0/syn/struct.Ident.html

use derive_builder::Builder;

#[derive(Builder)]
pub struct Command {
    executable: String,
    args: Vec<String>,
    env: Vec<String>,
    current_dir: String,
}

fn main() {
    let builder = Command::builder();

    let _ = builder;
}

从用例上面能够简单的看到,主要是提供了Command::Builder的方法实现。
但是根据提示,让我们最好实现

pub struct CommandBuilder {
    executable: Option<String>,
    args: Option<Vec<String>>,
    env: Option<Vec<String>>,
    current_dir: Option<String>,
}
impl Command {
    pub fn builder() -> CommandBuilder {
        CommandBuilder {
            executable: None,
            args: None,
            env: None,
            current_dir: None,
        }
    }
}

最开始的时候,我按照题目示意的方式进行书写,但是没有使用模板,但模板才是精髓所在。
其中参考链接如下

因此,首要的任务就是识别全部的字段,然后按照模板生成代码。

字段提取

// common.rs

// 字段类型简化定义
pub(crate) type FieldsType = syn::punctuated::Punctuated<syn::Field, syn::Token!(,)>;
// 字段提取方法
pub(super) fn parse_fields(ast: &syn::DeriveInput) -> syn::Result<&FieldsType> {
    // 必须是struct
    if let syn::Data::Struct(
        // 结构解析
        syn::DataStruct {
            // 命名字段枚举匹配
            fields: syn::Fields::Named(
                // 命名字段结构
                syn::FieldsNamed { 
                    ref named, 
                    .. 
                }
            ),
            ..
        }
    ) = ast.data {
        return syn::Result::Ok(named);
    }
    // 结果不匹配,返回错误
    let err = syn::Error::new_spanned(ast, "parse fields error");
    syn::Result::Err(err)
}

在解析语法节点的时候,span是一个关键的信息,虽然感觉毫无意义,但是报错的时候能够针对性的在关键位置进行错误提示。在后续的题目中,核对错误十分依靠span进行定位。

问题作答

pub(super) fn solution(
    fields: &crate::common::FieldsType,
    origin_ident: &syn::Ident,
    builder_ident: &syn::Ident,
) -> proc_macro2::TokenStream {
    // 遍历fields,获取ident标识符
    let idents: Vec<_> = fields.iter().map(|f| &f.ident).collect();
    // 遍历fields,获取指定类型
    let tys: Vec<_> = fields.iter().map(|f| &f.ty).collect();
    quote::quote! {
        // 定义XXBuilder
        pub struct #builder_ident {
            // 重复 // option包装类型
            #(
                pub #idents: std::option::Option<#tys>
            ),*
        }

        // 实现builder方法
        impl #origin_ident {
            pub fn builder() -> #builder_ident {
                #builder_ident {
                    #(
                        #idents: std::option::Option::None
                    ),*
                }
            }
        }
    }
}

因为相关的fieldsorigin_identbuilder_ident很常用,因此通过外部传入。

完整作答

// lib.rs
mod common;
mod solution2;

#[proc_macro_derive(Builder)]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    solution1(input)
}

fn solution1(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = syn::parse_macro_input!(input as syn::DeriveInput);
    let fields = {
        match common::parse_fields(&ast) {
            Ok(f) => f,
            Err(_e) => std::panic!(std::stringify!(_e)),
        }
    };

    let origin_ident = &ast.ident;
    let builder_ident = &quote::format_ident!("{}Builder", origin_ident);
    let mut token_stream = proc_macro2::TokenStream::new();

    // solution2
    let solution2_stream = solution2::solution(fields, origin_ident, builder_ident);
    token_stream.extend(solution2_stream);

    proc_macro::TokenStream::from(token_stream)

小结

  • 解析: 结构解析大多是先枚举匹配,然后再解析具体结构获取其中的定义
  • 拼接:模板中使用#进行变量的读取,主要依赖外部变量进行铺开

具体的解析字段最好详读文档,或者死记硬背(我是这样做的),逐步熟悉之后慢慢理解。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!