proc-macro_workshop:builder-7

godme發表於2022-06-29
// The std::process::Command builder handles args in a way that is potentially
// more convenient than passing a full vector of args to the builder all at
// once.
//
// Look for a field attribute #[builder(each = "...")] on each field. The
// generated code may assume that fields with this attribute have the type Vec
// and should use the word given in the string literal as the name for the
// corresponding builder method which accepts one vector element at a time.
//
// In order for the compiler to know that these builder attributes are
// associated with your macro, they must be declared at the entry point of the
// derive macro. Otherwise the compiler will report them as unrecognized
// attributes and refuse to compile the caller's code.
//
//     #[proc_macro_derive(Builder, attributes(builder))]
//
// These are called inert attributes. The word "inert" indicates that these
// attributes do not correspond to a macro invocation on their own; they are
// simply looked at by other macro invocations.
//
// If the new one-at-a-time builder method is given the same name as the field,
// avoid generating an all-at-once builder method for that field because the
// names would conflict.
//
//
// Resources:
//
//   - Relevant syntax tree types:
//     https://docs.rs/syn/1.0/syn/struct.Attribute.html
//     https://docs.rs/syn/1.0/syn/enum.Meta.html

use derive_builder::Builder;

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

fn main() {
    let command = Command::builder()
        .executable("cargo".to_owned())
        .arg("build".to_owned())
        .arg("--release".to_owned())
        .build()
        .unwrap();

    assert_eq!(command.executable, "cargo");
    assert_eq!(command.args, vec!["build", "--release"]);
}

這道題相較於之前的題目,有了一定的昇華。
相較於之前的欄位型別檢測,這道題的每一個欄位,都可能有額外的屬性標記。
這是由於rust宏發展造就的派生宏和屬性宏的交叉。
算是派生宏下屬的過程宏,但是這種屬性,只能繫結在派生宏下。

觀察題目,其中主要的目的就是透過解析Vec<T>欄位上的#[builder(each = "xx")],然後改造具體的setter方法,使得能夠單獨的新增屬性,而不是一次性的設定整個Vec

Option<T>類似,我們要提取Vec<T>中的T,並建立對應的setter方法。
這裡需要說明特殊的一點,如果each指定的名稱和變數名相同,那就只能透過each單獨設定,否則我們還是需要提供一個整體的Vecsetter方法。

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

除了基礎的Builder,我們還需要宣告下屬的屬性標記,增加
attributes(builder)描述,這樣,在Builder標記的結構體內,才能夠正常解析builder

如何提取欄位上的屬性標記呢,可以參考文件中的提示

其中比較特殊的是syn:: Meta::List,它裡面的(A,B,C)作為list中的每一項的結構都是syn:: NestedMeta::Meta,而這個資料結構,有可以針對的解析為上述三種結構。

分析一下#[builder(each = "arg")]不難發現,它其實就是一個syn:: Meta::List內部再解析為syn:: Meta::NameValue
這樣,我們就可以提取出指定的方法名稱了。

pub(crate) fn each_method(field: &syn::Field) ->  std::option::Option<syn::Ident> {
    for attr in &field.attrs {
        // 匹配MetaList
        if  let  std::result::Result::Ok(
            syn::Meta::List(syn::MetaList {
                ref  path,
                ref  nested,
                ..
            }
        )) =  attr.parse_meta() {
            if  let  Some(p) =  path.segments.first() {
                // 指定builder
                if  p.ident  ==  "builder" {
                    if  let  Some(syn::NestedMeta::Meta(
                        syn::Meta::NameValue(kv)
                    )) =  nested.first() {
                        // 解析each
                        if  kv.path.is_ident("each") {
                            if  let  syn::Lit::Str(ref  ident_str) =  kv.lit {
                                return std::option::Option::Some(
                                    syn::Ident::new(
                                        ident_str.value().as_str(),
                                        attr.span()));
                            }
                        }
                    }
                }
            }
        }
    }
    std::option::Option::None
}

基礎方法完成,接下來就是如何作答了。

還是按照之前的分步驟答題,先思考一下我們需要改動哪些地方

  • XBuilder:如果指定了each的,我們直接使用Vec<T>替換Option<T>
  • setter:單獨設定
  • build:對於指定each的,直接複製Vec<T>就好了

和第六題一樣,我們基本都要全部進行實現

// solution7.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 solution2_stream = solution2(fields, builder_ident, origin_ident);
    token_stream.extend(solution2_stream);

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

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

    token_stream
}

// builder構造
fn solution2(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    origin_ident: &syn::Ident,
) -> proc_macro2::TokenStream {
    let struct_field_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            // option
            if let std::option::Option::Some(_) =
                crate::common::option_type_with_ident(ty, "Option")
            {
                quote::quote! {
                    pub #ident: #ty
                }
            // vec
            } else if let std::option::Option::Some(_) =
                crate::common::option_type_with_ident(ty, "Vec")
            {
                quote::quote! {
                    pub #ident: #ty
                }
            // 其他型別
            } else {
                quote::quote! {
                    pub #ident: std::option::Option<#ty>
                }
            }
        })
        .collect();

    let construct_field_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            // vec
            if let std::option::Option::Some(_) = crate::common::option_type_with_ident(ty, "Vec") {
                quote::quote! {
                    #ident: vec![]
                }
            } else {
                quote::quote! {
                    #ident: std::option::Option::None
                }
            }
        })
        .collect();
    quote::quote! {
        pub struct #builder_ident {
            #(
                #struct_field_stream_vec
            ),*
        }

        impl #origin_ident {
            pub fn builder() -> #builder_ident {
                #builder_ident {
                    #(
                        #construct_field_stream_vec
                    ),*
                }
            }
        }
    }
}

// setter
fn solution35(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    _: &syn::Ident,
) -> proc_macro2::TokenStream {
    let setter_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            // option
            if let std::option::Option::Some(inner_type) =
                crate::common::option_type_with_ident(ty, "Option")
            {
                quote::quote! {
                    pub fn #ident(&mut self, #ident: #inner_type) -> &mut Self {
                        self.#ident = std::option::Option::Some(#ident);
                        self
                    }
                }
            // vec
            } else if let std::option::Option::Some(inner_type) =
                crate::common::option_type_with_ident(ty, "Vec")
            {
                // 指定了each
                if let std::option::Option::Some(ref each_method_ident) =
                    crate::common::each_method(f)
                {
                    let mut each_method_stream = quote::quote! {
                        pub fn #each_method_ident(&mut self, #ident: #inner_type) -> &mut Self {
                            self.#ident.push(#ident);
                            self
                        }
                    };
                    // 不重名新增額外方法
                    if ident.as_ref().unwrap() != each_method_ident {
                        let origin_setter_stream = quote::quote! {
                            pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                                self.#ident = #ident;
                                self
                            }
                        };
                        each_method_stream.extend(origin_setter_stream);
                    }
                    each_method_stream
                } else {
                    // 沒有指定each
                    quote::quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = #ident;
                            self
                        }
                    }
                }
            // 其他
            } else {
                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_stream_vec
            )*
        }
    }
}

// builder
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()
        .filter(|f| {
            // option和vec的不檢查
            let option_field = crate::common::option_type_with_ident(&f.ty, "Option");
            let vec_field = crate::common::option_type_with_ident(&f.ty, "Vec");
            option_field.is_none() && vec_field.is_none()
        })
        .map(|f| {
            let ident = &f.ident;
            quote::quote! {
                if self.#ident.is_none() {
                    let err = format!("field {} is missing", stringify!(#ident));
                    return std::result::Result::Err(err.into());
                }
            }
        })
        .collect();

    let construct_stream: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            // option直接複製
            if let std::option::Option::Some(_) =
                crate::common::option_type_with_ident(ty, "Option")
            {
                quote::quote! {
                    #ident: self.#ident.clone()
                }
            // vec直接複製
            } else if let std::option::Option::Some(_) =
                crate::common::option_type_with_ident(ty, "Vec")
            {
                quote::quote! {
                    #ident: self.#ident.clone()
                }
            } else {
                // 其他型別unwrap
                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_stream
                    ),*
                };
                std::result::Result::Ok(res)
            }
        }
    }
}

至此,第七題完成。

因為,又重寫了一遍,因此前面的全部作廢,按照慣例,照舊保留

// lib.rs
mod  common;
mod  solution2;
mod  solution35;
mod  solution4;
mod  solution6;
mod  solution7;

#[proc_macro_derive(Builder, attributes(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);

    // 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 _ = solution6::solution(fields, builder_ident, origin_ident);

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

    proc_macro::TokenStream::from(token_stream)
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結