proc-macro-workshop:builder-6

godme發表於2022-06-29
// Some fields may not always need to be specified. Typically these would be
// represented as Option<T> in the struct being built.
//
// Have your macro identify fields in the macro input whose type is Option and
// make the corresponding builder method optional for the caller. In the test
// case below, current_dir is optional and not passed to one of the builders in
// main.
//
// Be aware that the Rust compiler performs name resolution only after macro
// expansion has finished completely. That means during the evaluation of a
// procedural macro, "types" do not exist yet, only tokens. In general many
// different token representations may end up referring to the same type: for
// example `Option<T>` and `std::option::Option<T>` and `<Vec<Option<T>> as
// IntoIterator>::Item` are all different names for the same type. Conversely,
// a single token representation may end up referring to many different types in
// different places; for example the meaning of `Error` will depend on whether
// the surrounding scope has imported std::error::Error or std::io::Error. As a
// consequence, it isn't possible in general for a macro to compare two token
// representations and tell whether they refer to the same type.
//
// In the context of the current test case, all of this means that there isn't
// some compiler representation of Option that our macro can compare fields
// against to find out whether they refer to the eventual Option type after name
// resolution. Instead all we get to look at are the tokens of how the user has
// described the type in their code. By necessity, the macro will look for
// fields whose type is written literally as Option<...> and will not realize
// when the same type has been written in some different way.
//
// The syntax tree for types parsed from tokens is somewhat complicated because
// there is such a large variety of type syntax in Rust, so here is the nested
// data structure representation that your macro will want to identify:
//
//     Type::Path(
//         TypePath {
//             qself: None,
//             path: Path {
//                 segments: [
//                     PathSegment {
//                         ident: "Option",
//                         arguments: PathArguments::AngleBracketed(
//                             AngleBracketedGenericArguments {
//                                 args: [
//                                     GenericArgument::Type(
//                                         ...
//                                     ),
//                                 ],
//                             },
//                         ),
//                     },
//                 ],
//             },
//         },
//     )

use derive_builder::Builder;

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

fn main() {
    let command = Command::builder()
        .executable("cargo".to_owned())
        .args(vec!["build".to_owned(), "--release".to_owned()])
        .env(vec![])
        .build()
        .unwrap();
    assert!(command.current_dir.is_none());

    let command = Command::builder()
        .executable("cargo".to_owned())
        .args(vec!["build".to_owned(), "--release".to_owned()])
        .env(vec![])
        .current_dir("..".to_owned())
        .build()
        .unwrap();
    assert!(command.current_dir.is_some());
}

這一關主要表明的就是所謂的預設欄位,這一下基本上把前面的實現都已經打亂了

  • XBuilder-fields:不能簡單包裹,否則會產生Option<Option<T>>
  • setter:設定的是T,而不是Option<T>
  • build:欄位檢查可以為空

由此看來,我們基本上要把之前的方法全部實現一遍,並且要把Option<T>解析出來。
透過相關的提示,就是透過解析欄位型別的Path,獲取Option<T>中的T

// common.rs
// 解析X::Y::ty<T>中的T
pub(crate) fn option_type_with_ident<'a>(
    ty: &'a syn::Type,
    ident: &str,
) -> std::option::Option<&'a syn::Type> {
    if let syn::Type::Path(
        syn::TypePath { 
            ref path,
            .. 
        }
    ) = ty {
        // 專門匹配路徑的最後一個
        if let std::option::Option::Some(seg) = path.segments.last() {
            // 特徵符號
            if seg.ident == ident {
                // <?> 中的?
                if let syn::PathArguments::AngleBracketed(
                    syn::AngleBracketedGenericArguments {
                        ref args,
                        ..
                    }
                ) = seg.arguments{
                    // 匹配提取T
                    if let Some(syn::GenericArgument::Type(inner_type)) = args.first() {
                        return std::option::Option::Some(inner_type);
                    }
                }
            }
        }
    }
    std::option::Option::None
}

// 提取指定的XX::YY::Option<T>中的T
pub(crate) fn option_type(ty: &syn::Type) -> std::option::Option<&syn::Type> {
    option_type_with_ident(ty, "Option".into())
}

這樣,我們後續就好改寫之前的方法了

按照之前的分類,我們還是分步作答,不過基礎方法都要進行重寫

// solution6.rs
pub(super) fn solution(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    origin_ident: &syn::Ident,
) -> proc_macro2::TokenStream {
    let mut token_stream = proc_macro2::TokenStream::new();

    // solution2
    let refactor_solution2_stream = solution2(fields, builder_ident, origin_ident);
    token_stream.extend(refactor_solution2_stream);
    // solution35
    let refactor_solution35_stream = solution35(fields, builder_ident, origin_ident);
    token_stream.extend(refactor_solution35_stream);
    // solution4
    let refactor_solution4_stream = solution4(fields, builder_ident, origin_ident);
    token_stream.extend(refactor_solution4_stream);
    return token_stream;
}

// 重寫builder
fn solution2(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    origin_ident: &syn::Ident,
) -> proc_macro2::TokenStream {
    let builder_fields_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            match crate::common::option_type(ty) {
                // 如果是包裝型別Option<T>,直接使用T
                std::option::Option::Some(inner_type) => {
                    quote::quote! {
                        pub #ident: std::option::Option<#inner_type>
                    }
                },
                // 否則直接使用T
                std::option::Option::None => {
                    quote::quote! {
                        pub #ident: std::option::Option<#ty>
                    }
                }
            }
        })
        .collect();
    let idents: Vec<_> = fields.iter().map(|f| &f.ident).collect();
    quote::quote! {
        pub struct #builder_ident {
            #(
                #builder_fields_stream_vec
            ),*
        }

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

// 重寫setter
fn solution35(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    _origin_ident: &syn::Ident,
) -> proc_macro2::TokenStream {
    let setter_method_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            match crate::common::option_type(ty) {
                // 如果是Option<T>,使用T
                std::option::Option::Some(inner_type) => {
                    quote::quote! {
                        pub fn #ident(&mut self, #ident: #inner_type) -> &mut Self {
                            self.#ident = std::option::Option::Some(#ident);
                            self
                        }
                    }
                },
                // 直接使用T
                std::option::Option::None => {
                    quote::quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = std::option::Option::Some(#ident);
                            self
                        }
                    }
                }
            }
        })
        .collect();

    quote::quote! {
        impl #builder_ident {
            #(
                #setter_method_stream_vec
            )*
        }
    }
}

// 重寫build:檢查和回傳
fn solution4(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    origin_ident: &syn::Ident,
) -> proc_macro2::TokenStream {
    let construct_if_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            // 專門針對非Option<T>進行條件檢查
            // 對於預設欄位無需檢查
            if let std::option::Option::None = crate::common::option_type(ty) {
                return std::option::Option::Some(quote::quote! {
                    if self.#ident.is_none() {
                        let err = std::format!("field {} missing", std::stringify!(#ident));
                        return std::result::Result::Err(err.into());
                    }
                });
            }
            std::option::Option::None
        })
        .filter(|e| e.is_some())
        .collect();
    let construct_instance_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            // 如果是Option<T>,直接複製即可
            match crate::common::option_type(ty) {
                std::option::Option::Some(_) => {
                    quote::quote! {
                        #ident: self.#ident.clone()
                    }
                },
                // 如果是T,需要從builder中的Option<T>解出來進行設定
                std::option::Option::None => {
                    quote::quote! {
                        #ident: self.#ident.clone().unwrap()
                    }
                }
            }
        })
        .collect();
    quote::quote! {
        impl #builder_ident {
            pub fn build(&self) -> std::result::Result<#origin_ident, std::boxed::Box<dyn std::error::Error>> {
                #(
                    #construct_if_stream_vec
                )*
                let res = #origin_ident {
                    #(
                       #construct_instance_stream_vec
                    ),*
                };
                std::result::Result::Ok(res)
            }
        }
    }
}

其中需要注意的是#()*,後面的,if中是不需要的。

// lib.rs
mod solution2;
mod solution35;
mod solution4;
mod common;
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);

    // solution35
    let solution35_stream = solution35::soultion(fields, builder_ident);
    token_stream.extend(solution35_stream);

    // solution4
    let solution4_stream = solution4::solution(fields, builder_ident, origin_ident);
    token_stream.extend(solution4_stream);

    // solution6
    let token_stream = solution6::solution(fields, builder_ident, origin_ident);

    proc_macro::TokenStream::from(token_stream)
}

雖然前面的

  • solution2
  • solution35
  • solution4

都已經被重新實現了,完全不需要新增,但是為了清晰的還原解題步驟,還是原樣保留。

本作品採用《CC 協議》,轉載必須註明作者和本文連結