Open22

proc-macro-workshop/builder に取り組む

hpphpp

参考:
https://github.com/dtolnay/proc-macro-workshop
https://zenn.dev/magurotuna/articles/bab4db5999ebfa

proc-macro-workshop/builderからやる、Builderパターンを実装するderiveマクロを作るやつ。

各ステップごとのコードを残す、ポイントとかも書いたりするかもしれない。
GitHubでcommit logを辿るよりも、zennで縦長にみた方がわかりやすい気がするので、後進のために残しておく。

一応GitHubも貼っておく、commitは細かく切ったりしていないので注意。

https://github.com/hppRC/proc-macro-workshop

hpphpp

準備

まずはこれらをインストールしないと始まらない。

cargo add syn quote proc_macro2  

nightlyを使うと、マクロについてのエラーメッセージをわかりやすくみることができるので、切り替えておくことを推奨。

rustup install nightly

以下のコマンドでテストを走らせる。
テストしたいテストケースについて、事前にtests/progress.rsのコメントアウトを外しておく必要がある。

マクロのバックトレースをみたい場合は、フラグを付け足してやる。
-Z macro-backtraceはcargoのフラグではなく、rustcに渡されるべきフラグなので、コマンドの前に付け足しておく。

cargo test
# または
RUSTFLAGS="-Z macro-backtrace" cargo +nightly test

他に、cargo-expandを入れておくと便利そう。

cargo install cargo-expand

cargo-expandを使う場合、testsディレクトリのファイルを対象としたマクロの展開はできないみたいなので、testsディレクトリをコピってexamplesディレクトリを作り、以下のように叩くといい。

mkdir debug
cargo expand --example 03-call-setters > debug/03-call-setters.rs 

参考:
https://users.rust-lang.org/t/how-to-run-cargo-expand-on-a-single-macro/31224

hpphpp

01-parse.rs

lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _ = input;

    let expanded = quote! {};
    // Hand the output tokens back to the compiler
    TokenStream::from(expanded)
}
hpphpp

02-create-builder.rs

lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _ = input;

    let expanded = quote! {
        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,
                }
            }
        }
    };
    // Hand the output tokens back to the compiler
    TokenStream::from(expanded)
}


hpphpp

03-call-setters.rs

lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _ = input;

    let expanded = quote! {
        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,
                }
            }
        }

        impl CommandBuilder {
            fn executable(&mut self, executable: String) -> &mut Self {
                self.executable = Some(executable);
                self
            }
            fn args(&mut self, args: Vec<String>) -> &mut Self {
                self.args = Some(args);
                self
            }
            fn env(&mut self, env: Vec<String>) -> &mut Self {
                self.env = Some(env);
                self
            }
            fn current_dir(&mut self, current_dir: String) -> &mut Self {
                self.current_dir = Some(current_dir);
                self
            }
        }
    };
    // Hand the output tokens back to the compiler
    TokenStream::from(expanded)
}
hpphpp

04-call-build.rs

lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _ = input;

    let expanded = quote! {
        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,
                }
            }
        }

        impl CommandBuilder {
            fn executable(&mut self, executable: String) -> &mut Self {
                self.executable = Some(executable);
                self
            }
            fn args(&mut self, args: Vec<String>) -> &mut Self {
                self.args = Some(args);
                self
            }
            fn env(&mut self, env: Vec<String>) -> &mut Self {
                self.env = Some(env);
                self
            }
            fn current_dir(&mut self, current_dir: String) -> &mut Self {
                self.current_dir = Some(current_dir);
                self
            }

            pub fn build(&mut self) -> Result<Command, Box<dyn std::error::Error>> {
                if self.executable.is_none() || self.args.is_none() || self.env.is_none() || self.current_dir.is_none() {
                    Err("Error occured!".into())
                } else {
                    Ok(Command {
                        executable: self.executable.clone().unwrap(),
                        args: self.args.clone().unwrap(),
                        env: self.env.clone().unwrap(),
                        current_dir: self.current_dir.clone().unwrap(),
                    })
                }
            }
        }
    };
    // Hand the output tokens back to the compiler
    TokenStream::from(expanded)
}
hpphpp

06-optional-field.rs

これに取り組む上で、ここまで書いてきたような構造体のフィールド名と型をハードコードするような書き方では対応できなくなるので、汎用的なderiveマクロとして使えるように書き直す。

hpphpp

Update: 02-create-builder.rs

lib.rs
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Builder)]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse the input tokens into a syntax tree
    let item = parse_macro_input!(input as DeriveInput);
    let struct_name = item.ident;
    let builder_name = format_ident!("{}Builder", struct_name);

    let expanded = quote! {
        pub struct #builder_name {}

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {}
            }
        }
    };

    proc_macro::TokenStream::from(expanded)
}
hpphpp

structの名前を使ってBuilder structの名前を作っている部分、format!マクロだと、マクロの展開時に以下のようになってしまうので不適。identifierではなく文字列として埋め込まれてしまう。

impl Command {
  pub fn builder() -> "CommandBuilder" {
    "CommnadBuilder" {}
  }
}

正しくはこうなってほしい。

impl Command {
  pub fn builder() -> CommandBuilder {
    CommnadBuilder {}
  }
}
hpphpp

Update: 03-call-setters.rs

lib.rs
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, FieldsNamed};

#[proc_macro_derive(Builder)]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse the input tokens into a syntax tree
    let item = parse_macro_input!(input as DeriveInput);
    let struct_name = item.ident;
    let builder_name = format_ident!("{}Builder", struct_name);
    let fields = extract_struct_fields(&item.data);

    let wrapped_fields_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        quote! {
            #ident: Option<#ty>
        }
    });
    let initial_fileds_stream_iter = fields.named.iter().map(|field| {
        let ident = &field.ident;
        quote! {
            #ident: None
        }
    });
    let builder_fields_setter_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        quote! {
            fn #ident(&mut self, #ident: #ty) -> &mut Self {
                self.#ident = Some(#ident);
                self
            }
        }
    });

    let expanded = quote! {
        pub struct #builder_name {
            #(#wrapped_fields_stream_iter),*
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#initial_fileds_stream_iter),*
                }
            }
        }

        impl #builder_name {
            #(#builder_fields_setter_stream_iter)*
        }
    };

    // Hand the output tokens back to the compiler
    proc_macro::TokenStream::from(expanded)
}

fn extract_struct_fields(data: &Data) -> &FieldsNamed {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => fields,
            _ => panic!("invalid fields"),
        },
        _ => panic!("invalid data"),
        // Data::Enum(_) => {}
        // Data::Union(_) => {}
    }
}

hpphpp

Update: 04-call-build.rs

lib.rs
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, FieldsNamed};

#[proc_macro_derive(Builder)]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse the input tokens into a syntax tree
    let item = parse_macro_input!(input as DeriveInput);
    let struct_name = item.ident;
    let builder_name = format_ident!("{}Builder", struct_name);
    let fields = extract_struct_fields(&item.data);

    let wrapped_fields_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        quote! {
            #ident: Option<#ty>
        }
    });
    let initial_fileds_stream_iter = fields.named.iter().map(|field| {
        let ident = &field.ident;
        quote! {
            #ident: None
        }
    });
    let builder_fields_setter_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        quote! {
            fn #ident(&mut self, #ident: #ty) -> &mut Self {
                self.#ident = Some(#ident);
                self
            }
        }
    });
    let builder_build_stream_iter = fields.named.iter().map(|field| {
        let ident = &field.ident;
        quote! {
            #ident: self.#ident.clone().unwrap()
        }
    });

    let expanded = quote! {
        pub struct #builder_name {
            #(#wrapped_fields_stream_iter),*
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#initial_fileds_stream_iter),*
                }
            }
        }

        impl #builder_name {
            #(#builder_fields_setter_stream_iter)*

            pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> {
                Ok(#struct_name {
                    #(#builder_build_stream_iter),*
                })
            }
        }
    };

    // Hand the output tokens back to the compiler
    proc_macro::TokenStream::from(expanded)
}

fn extract_struct_fields(data: &Data) -> &FieldsNamed {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => fields,
            _ => panic!("invalid fields"),
        },
        _ => panic!("invalid data"),
        // Data::Enum(_) => {}
        // Data::Union(_) => {}
    }
}

hpphpp

06-optional-field.rs

lib.rs
use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Path,
    PathArguments, PathSegment, Type, TypePath,
};

#[proc_macro_derive(Builder)]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse the input tokens into a syntax tree
    let item = parse_macro_input!(input as DeriveInput);
    let struct_name = item.ident;
    let builder_name = format_ident!("{}Builder", struct_name);
    let fields = extract_struct_fields(&item.data);

    let wrapped_fields_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        if is_option_type(&ty) {
            quote! {
                #ident: #ty
            }
        } else {
            quote! {
                #ident: Option<#ty>
            }
        }
    });

    let initial_fileds_stream_iter = fields.named.iter().map(|field| {
        let ident = &field.ident;
        quote! {
            #ident: None
        }
    });

    let builder_fields_setter_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        if is_option_type(&ty) {
            let inner_type = option_inner_type(&ty);
            quote! {
                fn #ident(&mut self, #ident: #inner_type) -> &mut Self {
                    self.#ident = Some(#ident);
                    self
                }
            }
        } else {
            quote! {
                fn #ident(&mut self, #ident: #ty) -> &mut Self {
                    self.#ident = Some(#ident);
                    self
                }
            }
        }
    });

    let builder_build_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;

        if is_option_type(&ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    let expanded = quote! {
        pub struct #builder_name {
            #(#wrapped_fields_stream_iter),*
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#initial_fileds_stream_iter),*
                }
            }
        }

        impl #builder_name {
            #(#builder_fields_setter_stream_iter)*

            pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> {
                Ok(#struct_name {
                    #(#builder_build_stream_iter),*
                })
            }
        }
    };

    // Hand the output tokens back to the compiler
    proc_macro::TokenStream::from(expanded)
}

fn extract_struct_fields(data: &Data) -> &FieldsNamed {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => fields,
            _ => panic!("invalid fields"),
        },
        _ => panic!("invalid data"),
        // Data::Enum(_) => {}
        // Data::Union(_) => {}
    }
}

fn is_option_type(ty: &Type) -> bool {
    match last_path_segment(&ty) {
        Some(path_seg) => path_seg.ident == "Option",
        None => false,
    }
}

fn option_inner_type(ty: &Type) -> &GenericArgument {
    match last_path_segment(&ty) {
        Some(PathSegment {
            ident: _,
            arguments: PathArguments::AngleBracketed(ref gen_arg),
        }) => gen_arg.args.first(),
        _ => None,
    }
    .expect("invalid option type")
}

fn last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        &Type::Path(TypePath {
            qself: None,
            path:
                Path {
                    segments: ref seg,
                    leading_colon: _,
                },
        }) => seg.last(),
        _ => None,
    }
}

hpphpp

07-repeated-field.rs

lib.rs
use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Attribute, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Lit,
    Meta, MetaList, MetaNameValue, NestedMeta, Path, PathArguments, PathSegment, Type, TypePath,
};

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse the input tokens into a syntax tree
    let item = parse_macro_input!(input as DeriveInput);
    let struct_name = item.ident;
    let builder_name = format_ident!("{}Builder", struct_name);
    let fields = extract_struct_fields(&item.data);

    let wrapped_fields_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;

        if is_option_type(&ty) {
            quote! {
                #ident: #ty
            }
        } else {
            quote! {
                #ident: Option<#ty>
            }
        }
    });

    let initial_fileds_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        let attrs = &field.attrs;
        let each_string_literal = extract_string_literal_of_attr_each(&attrs);

        if is_vec_type(&ty) && each_string_literal.is_some() {
            quote! {
                #ident: Some(vec![])
            }
        } else {
            quote! {
                #ident: None
            }
        }
    });

    let builder_fields_setter_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        let attrs = &field.attrs;
        let each_string_literal = extract_string_literal_of_attr_each(&attrs);

        if is_vec_type(&ty) && each_string_literal.is_some() {
            let inner_type = extract_inner_type(&ty);
            let lit = each_string_literal.unwrap();
            let lit_ident = format_ident!("{}", lit);

            if lit == ident.clone().unwrap().to_string() {
                let ref_ident = format_ident!("ref_{}", lit);
                quote! {
                    fn #ident(&mut self, #lit_ident: #inner_type) -> &mut Self {
                        if let Some(ref mut #ref_ident) = self.#ident {
                            #ref_ident.push(#lit_ident);
                        } else {
                            self.#ident = Some(vec![#lit_ident]);
                        };
                        self
                    }
                }
            } else {
                quote! {
                    fn #lit_ident(&mut self, #lit_ident: #inner_type) -> &mut Self {
                        if let Some(ref mut #ident) = self.#ident {
                            #ident.push(#lit_ident);
                        } else {
                            self.#ident = Some(vec![#lit_ident]);
                        };
                        self
                    }

                    fn #ident(&mut self, #ident: #ty) -> &mut Self {
                        self.#ident = Some(#ident);
                        self
                    }
                }
            }
        } else {
            if is_option_type(&ty) {
                let inner_type = extract_inner_type(&ty);
                quote! {
                    fn #ident(&mut self, #ident: #inner_type) -> &mut Self {
                        self.#ident = Some(#ident);
                        self
                    }
                }
            } else {
                quote! {
                    fn #ident(&mut self, #ident: #ty) -> &mut Self {
                        self.#ident = Some(#ident);
                        self
                    }
                }
            }
        }
    });

    let builder_build_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;

        if is_option_type(&ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    let expanded = quote! {
        pub struct #builder_name {
            #(#wrapped_fields_stream_iter),*
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#initial_fileds_stream_iter),*
                }
            }
        }

        impl #builder_name {
            #(#builder_fields_setter_stream_iter)*

            pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> {
                Ok(#struct_name {
                    #(#builder_build_stream_iter),*
                })
            }
        }
    };

    // Hand the output tokens back to the compiler
    proc_macro::TokenStream::from(expanded)
}

fn extract_struct_fields(data: &Data) -> &FieldsNamed {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => fields,
            _ => panic!("invalid fields"),
        },
        _ => panic!("invalid data"),
        // Data::Enum(_) => {}
        // Data::Union(_) => {}
    }
}

fn is_option_type(ty: &Type) -> bool {
    match last_path_segment(&ty) {
        Some(path_seg) => path_seg.ident == "Option",
        None => false,
    }
}

fn is_vec_type(ty: &Type) -> bool {
    match last_path_segment(&ty) {
        Some(path_seg) => path_seg.ident == "Vec",
        None => false,
    }
}

fn extract_inner_type(ty: &Type) -> &GenericArgument {
    match last_path_segment(&ty) {
        Some(PathSegment {
            ident: _,
            arguments: PathArguments::AngleBracketed(ref gen_arg),
        }) => gen_arg.args.first(),
        _ => None,
    }
    .expect("invalid option type")
}

fn last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        &Type::Path(TypePath {
            qself: None,
            path:
                Path {
                    segments: ref seg,
                    leading_colon: _,
                },
        }) => seg.last(),
        _ => None,
    }
}

fn extract_string_literal_of_attr_each(attrs: &[Attribute]) -> Option<String> {
    attrs.iter().find_map(|attr| match attr.parse_meta() {
        Ok(Meta::List(MetaList {
            ref path,
            paren_token: _,
            ref nested,
        })) => {
            (path.get_ident()? == "builder").then(|| ())?;

            if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                path,
                eq_token: _,
                lit: Lit::Str(ref litstr),
            })) = nested.first()?
            {
                if path.get_ident()?.to_string() == "each" {
                    Some(litstr.value())
                } else {
                    None
                }
            } else {
                None
            }
        }
        _ => None,
    })
}

hpphpp

頑張って場合分けを書くのと、eachアトリビュートが有効なフィールドについてはデフォルト値をセットしておくのがポイント。

hpphpp

08-unrecognized-attribute.rs

lib.rs

use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, GenericArgument,
    Lit, Meta, MetaList, MetaNameValue, NestedMeta, Path, PathArguments, PathSegment, Type,
    TypePath,
};

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse the input tokens into a syntax tree
    let item = parse_macro_input!(input as DeriveInput);
    let struct_name = item.ident;
    let builder_name = format_ident!("{}Builder", struct_name);
    let fields = extract_struct_fields(&item.data);

    let wrapped_fields_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;

        if is_option_type(&ty) {
            quote! {
                #ident: #ty
            }
        } else {
            quote! {
                #ident: Option<#ty>
            }
        }
    });

    let initial_fileds_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        let attrs = &field.attrs;
        let attr_each = parse_attr_each(&attrs);

        if is_vec_type(&ty) && attr_each.is_some() {
            quote! {
                #ident: Some(vec![])
            }
        } else {
            quote! {
                #ident: None
            }
        }
    });

    let builder_fields_setter_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        let attrs = &field.attrs;
        let attr_each = parse_attr_each(&attrs);

        if is_vec_type(&ty) && attr_each.is_some() {
            match attr_each {
                Some(AttrParseResult::InvalidKey(meta)) => {
                    return Error::new_spanned(meta, "expected `builder(each = \"...\")`")
                        .to_compile_error()
                }
                Some(AttrParseResult::Value(lit)) => {
                    let inner_type = extract_inner_type(&ty);
                    let lit_ident = format_ident!("{}", lit);

                    if lit == ident.clone().unwrap().to_string() {
                        let ref_ident = format_ident!("ref_{}", lit);
                        quote! {
                            fn #ident(&mut self, #lit_ident: #inner_type) -> &mut Self {
                                if let Some(ref mut #ref_ident) = self.#ident {
                                    #ref_ident.push(#lit_ident);
                                } else {
                                    self.#ident = Some(vec![#lit_ident]);
                                };
                                self
                            }
                        }
                    } else {
                        quote! {
                            fn #lit_ident(&mut self, #lit_ident: #inner_type) -> &mut Self {
                                if let Some(ref mut #ident) = self.#ident {
                                    #ident.push(#lit_ident);
                                } else {
                                    self.#ident = Some(vec![#lit_ident]);
                                };
                                self
                            }

                            fn #ident(&mut self, #ident: #ty) -> &mut Self {
                                self.#ident = Some(#ident);
                                self
                            }
                        }
                    }
                }
                None => unreachable!(),
            }
        } else {
            if is_option_type(&ty) {
                let inner_type = extract_inner_type(&ty);
                quote! {
                    fn #ident(&mut self, #ident: #inner_type) -> &mut Self {
                        self.#ident = Some(#ident);
                        self
                    }
                }
            } else {
                quote! {
                    fn #ident(&mut self, #ident: #ty) -> &mut Self {
                        self.#ident = Some(#ident);
                        self
                    }
                }
            }
        }
    });

    let builder_build_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;

        if is_option_type(&ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    let expanded = quote! {
        pub struct #builder_name {
            #(#wrapped_fields_stream_iter),*
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#initial_fileds_stream_iter),*
                }
            }
        }

        impl #builder_name {
            #(#builder_fields_setter_stream_iter)*

            pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> {
                Ok(#struct_name {
                    #(#builder_build_stream_iter),*
                })
            }
        }
    };

    // Hand the output tokens back to the compiler
    proc_macro::TokenStream::from(expanded)
}

fn extract_struct_fields(data: &Data) -> &FieldsNamed {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => fields,
            _ => panic!("invalid fields"),
        },
        _ => panic!("invalid data"),
        // Data::Enum(_) => {}
        // Data::Union(_) => {}
    }
}

fn is_option_type(ty: &Type) -> bool {
    match last_path_segment(&ty) {
        Some(path_seg) => path_seg.ident == "Option",
        None => false,
    }
}

fn is_vec_type(ty: &Type) -> bool {
    match last_path_segment(&ty) {
        Some(path_seg) => path_seg.ident == "Vec",
        None => false,
    }
}

fn extract_inner_type(ty: &Type) -> &GenericArgument {
    match last_path_segment(&ty) {
        Some(PathSegment {
            ident: _,
            arguments: PathArguments::AngleBracketed(ref gen_arg),
        }) => gen_arg.args.first(),
        _ => None,
    }
    .expect("invalid option type")
}

fn last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        &Type::Path(TypePath {
            qself: None,
            path:
                Path {
                    segments: ref seg,
                    leading_colon: _,
                },
        }) => seg.last(),
        _ => None,
    }
}

enum AttrParseResult {
    Value(String),
    InvalidKey(Meta),
}

fn parse_attr_each(attrs: &[Attribute]) -> Option<AttrParseResult> {
    attrs.iter().find_map(|attr| match attr.parse_meta() {
        Ok(meta) => match meta {
            Meta::List(MetaList {
                ref path,
                paren_token: _,
                ref nested,
            }) => {
                (path.get_ident()? == "builder").then(|| ())?;

                if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                    path,
                    eq_token: _,
                    lit: Lit::Str(ref litstr),
                })) = nested.first()?
                {
                    if path.get_ident()?.to_string() == "each" {
                        Some(AttrParseResult::Value(litstr.value()))
                    } else {
                        Some(AttrParseResult::InvalidKey(meta))
                    }
                } else {
                    None
                }
            }
            _ => None,
        },
        _ => None,
    })
}

hpphpp

結構適当にやっている、本当は最初にバリデーションを一括でやった方がいい気がする。

hpphpp

09-redefined-prelude-types.rs

lib.rs

use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, GenericArgument,
    Lit, Meta, MetaList, MetaNameValue, NestedMeta, Path, PathArguments, PathSegment, Type,
    TypePath,
};

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse the input tokens into a syntax tree
    let item = parse_macro_input!(input as DeriveInput);
    let struct_name = item.ident;
    let builder_name = format_ident!("{}Builder", struct_name);
    let fields = extract_struct_fields(&item.data);

    let wrapped_fields_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;

        if is_option_type(&ty) {
            quote! {
                #ident: #ty
            }
        } else {
            quote! {
                #ident: std::option::Option<#ty>
            }
        }
    });

    let initial_fileds_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        let attrs = &field.attrs;
        let attr_each = parse_attr_each(&attrs);

        if is_vec_type(&ty) && attr_each.is_some() {
            quote! {
                #ident: std::option::Option::Some(vec![])
            }
        } else {
            quote! {
                #ident: std::option::Option::None
            }
        }
    });

    let builder_fields_setter_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;
        let attrs = &field.attrs;
        let attr_each = parse_attr_each(&attrs);

        if is_vec_type(&ty) && attr_each.is_some() {
            match attr_each {
                std::option::Option::Some(AttrParseResult::InvalidKey(meta)) => {
                    return Error::new_spanned(meta, "expected `builder(each = \"...\")`")
                        .to_compile_error()
                }
                std::option::Option::Some(AttrParseResult::Value(lit)) => {
                    let inner_type = extract_inner_type(&ty);
                    let lit_ident = format_ident!("{}", lit);

                    if lit == ident.clone().unwrap().to_string() {
                        let ref_ident = format_ident!("ref_{}", lit);
                        quote! {
                            fn #ident(&mut self, #lit_ident: #inner_type) -> &mut Self {
                                if let std::option::Option::Some(ref mut #ref_ident) = self.#ident {
                                    #ref_ident.push(#lit_ident);
                                } else {
                                    self.#ident = std::option::Option::Some(vec![#lit_ident]);
                                };
                                self
                            }
                        }
                    } else {
                        quote! {
                            fn #lit_ident(&mut self, #lit_ident: #inner_type) -> &mut Self {
                                if let std::option::Option::Some(ref mut #ident) = self.#ident {
                                    #ident.push(#lit_ident);
                                } else {
                                    self.#ident = std::option::Option::Some(vec![#lit_ident]);
                                };
                                self
                            }

                            fn #ident(&mut self, #ident: #ty) -> &mut Self {
                                self.#ident = std::option::Option::Some(#ident);
                                self
                            }
                        }
                    }
                }
                std::option::Option::None => unreachable!(),
            }
        } else {
            if is_option_type(&ty) {
                let inner_type = extract_inner_type(&ty);
                quote! {
                    fn #ident(&mut self, #ident: #inner_type) -> &mut Self {
                        self.#ident = std::option::Option::Some(#ident);
                        self
                    }
                }
            } else {
                quote! {
                    fn #ident(&mut self, #ident: #ty) -> &mut Self {
                        self.#ident = std::option::Option::Some(#ident);
                        self
                    }
                }
            }
        }
    });

    let builder_build_stream_iter = fields.named.iter().map(|field| {
        let ty = &field.ty;
        let ident = &field.ident;

        if is_option_type(&ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    let expanded = quote! {
        pub struct #builder_name {
            #(#wrapped_fields_stream_iter),*
        }

        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#initial_fileds_stream_iter),*
                }
            }
        }

        impl #builder_name {
            #(#builder_fields_setter_stream_iter)*

            pub fn build(&mut self) -> std::result::Result<#struct_name, std::boxed::Box<dyn std::error::Error>> {
                Ok(#struct_name {
                    #(#builder_build_stream_iter),*
                })
            }
        }
    };

    // Hand the output tokens back to the compiler
    proc_macro::TokenStream::from(expanded)
}

fn extract_struct_fields(data: &Data) -> &FieldsNamed {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => fields,
            _ => panic!("invalid fields"),
        },
        _ => panic!("invalid data"),
        // Data::Enum(_) => {}
        // Data::Union(_) => {}
    }
}

fn is_option_type(ty: &Type) -> bool {
    match last_path_segment(&ty) {
        std::option::Option::Some(path_seg) => path_seg.ident == "Option",
        std::option::Option::None => false,
    }
}

fn is_vec_type(ty: &Type) -> bool {
    match last_path_segment(&ty) {
        std::option::Option::Some(path_seg) => path_seg.ident == "Vec",
        std::option::Option::None => false,
    }
}

fn extract_inner_type(ty: &Type) -> &GenericArgument {
    match last_path_segment(&ty) {
        std::option::Option::Some(PathSegment {
            ident: _,
            arguments: PathArguments::AngleBracketed(ref gen_arg),
        }) => gen_arg.args.first(),
        _ => std::option::Option::None,
    }
    .expect("invalid option type")
}

fn last_path_segment(ty: &Type) -> std::option::Option<&PathSegment> {
    match ty {
        &Type::Path(TypePath {
            qself: std::option::Option::None,
            path:
                Path {
                    segments: ref seg,
                    leading_colon: _,
                },
        }) => seg.last(),
        _ => std::option::Option::None,
    }
}

enum AttrParseResult {
    Value(String),
    InvalidKey(Meta),
}

fn parse_attr_each(attrs: &[Attribute]) -> std::option::Option<AttrParseResult> {
    attrs.iter().find_map(|attr| match attr.parse_meta() {
        Ok(meta) => match meta {
            Meta::List(MetaList {
                ref path,
                paren_token: _,
                ref nested,
            }) => {
                (path.get_ident()? == "builder").then(|| ())?;

                if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                    path,
                    eq_token: _,
                    lit: Lit::Str(ref litstr),
                })) = nested.first()?
                {
                    if path.get_ident()?.to_string() == "each" {
                        std::option::Option::Some(AttrParseResult::Value(litstr.value()))
                    } else {
                        std::option::Option::Some(AttrParseResult::InvalidKey(meta))
                    }
                } else {
                    std::option::Option::None
                }
            }
            _ => std::option::Option::None,
        },
        _ => std::option::Option::None,
    })
}

hpphpp

比較的安全なRustのマクロだけど、流石に名前空間とかは気を付けなきゃいけないみたい。

hpphpp

全体を通して、attributeさえなければ結構綺麗に書けるなという印象。
attributeももっと上手くハンドリングできるような気がするが、いろいろなケースを処理しようとするとどうしても本質的な複雑さを隠せない感じになる。

hpphpp

synはデフォルトだとdbg!マクロが使えないので、

Cargo.toml
[dependencies]
kuon = "0.0.22"
proc-macro2 = "1.0.24"
quote = "1.0.9"
syn = {version = "1.0.64", features = ["extra-traits"]}

として、Debugをimplしてあげるといい。