proc_macro_workshopでRustの手続き的マクロに入門する 後編

はじめに

前編では、setterメソッドによる値の設定やbuildメソッドによる構造体の生成などの基本的な機能を持った手続き的マクロを実装しました。後編では以下の機能を実装していきます。

  • Optional な値を構造体のフィールドとして持てるようにする
  • 以下の 2 つの方法で Vec 型のフィールドを更新できるようにする
    • ベクタを与えて一括で更新する
    • ベクタの要素を与えて 1 つずつフィールドに要素を追加する
  • コンパイルエラーが発生した際にわかりやすいメッセージを表示する

builder マクロを作る(続き)

06-optional-field

目標

  • 構造体のフィールドとして Optional な値を持てるようにする
    • Optional なフィールドには値が入っていなくてもbuildメソッドで構造体を生成できる
    • Optional でないフィールドは値が入っていないと build メソッドで構造体を生成できない
    • Optional なフィールドはSomeでラップせずに中身の値をそのまま使って初期化できる

最後の項目についてですが、たとえばCommand構造体が以下のようになっている場合、

pub struct Command {
    executable: String,
    args: Vec<String>,
    env: Vec<String>,
    current_dir: Option<String>,
}

current_dirは以下のようにStringを渡すだけでよいということです。

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

実装方針

目標とする機能を実現するために実装する必要があるのは以下の項目です。まずはこれらの機能を実装していきましょう。

  • ガード節で Optional でない型のみエラーを出すようにする
  • Optionでラップされた型はアンラップしてCommandBuilder構造体のフィールドで保持する
    • 今は元の型が何であってもOptionでラップするようになっています。そのため、Optional な型はOption<Option<_>>のようになります。このままだと扱いづらいので、Optional な型はいったんアンラップして、すべてのフィールドの型がOption<_>になるようにします
  • Optional な型の setter メソッドはラップされた中身の型を引数として受け付けるようにする

実装

実装方針で説明した機能を実装していきます。

ガード節で Optional でない型のみエラーを出すようにする

ガード節を生成する部分の実装は以下のようになっています。今はすべてのフィールドに対してNoneであるかのチェックを生成しています。これを Optional でない型のみガード節を生成するように変更します。

let checks = idents.iter().map(|ident| {
    let err = format!("Required field '{}' is missing", ident.to_string());
    quote! {
        if self.#ident.is_none() {
            return Err(#err.into())
        }
    }
});

Optional でないフィールドのみガード節を生成するためには、各フィールドの型を見て Optional でないフィールドのみをフィルタすれば良さそうです。typesに各フィールドの型が格納されているので、以下のようにfilterを用いて Optional でないフィールドの識別子についてだけガード節を生成します。is_optionは与えられた型が Optional であるかどうかを判定する何らかの関数です。のちほど実装します。

let checks = idents
    .iter()
    .zip(&types)
    .filter(|(_, ty)| !is_option(ty))
    .map(|(ident, _)| {
        let err = format!("Required field '{}' is missing", ident.to_string());
        quote! {
            if self.#ident.is_none() {
                return Err(#err.into())
            }
        }
    });
Optionでラップされた型はアンラップしてCommandBuilder構造体のフィールドで保持する

今の Builder 構造体の定義は以下のようになっています。すべてのフィールドをOptionでラップしています。Optional なフィールドについては、あらかじめOptionでラップされた型を取り出しておけば、あとの処理は今までと同じ内容になります。

#vis struct #builder_name {
   #(#idents: Option<#types>),*
}

実際に実装していきます。Optionの中身の型を取り出すなどの処理が追加されるので、生成された Builder 構造体のフィールドをbuilder_fieldsにいったん保持してあとで展開しましょう。 builder_fieldsの実装は以下のようになります。unwrap_option関数でOptionにラップされている型を取り出している以外は今までと同じです。unwrap_optionis_optionと同じくのちほど実装します。

let builder_fields = idents.iter().zip(&types).map(|(ident, ty)| {
    let t = unwrap_option(ty).unwrap_or(ty);
    quote! {
        #ident: Option<#t>
    }
});

builder_fieldsは Builder 構造体の定義を生成する部分で展開します。

#vis struct #builder_name {
    #(#builder_fields),*
}

Builder 構造体の定義が変わったためbuild関数も変える必要があります。今は以下のように目的の構造体を生成する際にすべてのフィールドをunwrapしていますが、Optional なフィールドは unwrap する必要がないのでそのまま返すようにしましょう。

pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> {
    #(#checks)*
    Ok(#ident {
        #(#idents: self.#idents.clone().unwrap()),*
    })
}

Optional なフィールドかどうかを判定する処理が追加されるため、builder_fieldsと同様に別の変数に格納してのちほど展開します。具体的な実装は以下の通りです。is_optionで Optional なフィールドかどうかを判定して、Optional であれば値をそのまま使用し、Optional でなければ unwrap して得られた値を使用します。

let struct_fields = idents.iter().zip(&types).map(|(ident, ty)| {
    if is_option(ty) {
        quote! {
            #ident: self.#ident.clone()
        }
    } else {
        quote! {
            #ident: self.#ident.clone().unwrap()
        }
    }
});

struct_fieldsbuild関数の中で展開します。

pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> {
    #(#checks)*
    Ok(#ident {
        #(#struct_fields),*
    })
}
Optional な型の setter メソッドはラップされた中身の型を引数として受け付けるようにする

今の setter の実装は以下のようになっています。Optional なフィールドかどうかは関係なく、すべてのフィールドについてそのフィールドの型をそのまま受け付けるようになっています。

impl #builder_name {
    #(pub fn #idents(&mut self, #idents: #types) -> &mut Self {
        self.#idents = Some(#idents);
        self
    })*
    ...

つまり Optional なフィールドについては以下のような setter が生成されます。これでは Builder 構造体のフィールド定義と矛盾します。そのため、Optional なフィールドの setter 関数は引数としてOptionの中身の型を受け取るようにします。

pub fn current_dir(&mut self, current_dir: Option<String>) -> &mut Self {
    self.current_dir = Some(current_dir);
    self
}

具体的には以下のような setter を作って展開するように書き換えます。Builder 構造体のフィールドと同様、unwrap_option関数を使ってOptionでラップされた中身の型を取り出します。

let setters = idents.iter().zip(&types).map(|(ident, ty)| {
    let t = unwrap_option(ty).unwrap_or(ty);
    quote! {
        pub fn #ident(&mut self, #ident: #t) -> &mut Self {
            self.#ident = Some(#ident);
            self
        }
    }
});

...

impl #builder_name {
    #(#setters)*
    ...
is_optionunwrap_optionの実装

is_optionunwrap_optionを実装していきます。まずはis_option関数から実装していきましょう。

is_option関数

is_optionType型を受け取ってそれがOptionかどうかを判定する関数なのでシグネチャは以下のようにすれば良さそうです。

fn is_option(ty: &Type) -> bool

tyOptionかどうか判定するのに使えそうなTypeのメソッドがあるか確認してみましょう。syn クレートのドキュメントを確認したところTypeは以下の enum のようです。

pub enum Type {
    Array(TypeArray),
    BareFn(TypeBareFn),
    Group(TypeGroup),
    ImplTrait(TypeImplTrait),
    Infer(TypeInfer),
    Macro(TypeMacro),
    Never(TypeNever),
    Paren(TypeParen),
    Path(TypePath),
    Ptr(TypePtr),
    Reference(TypeReference),
    Slice(TypeSlice),
    TraitObject(TypeTraitObject),
    Tuple(TypeTuple),
    Verbatim(TokenStream),
    // some variants omitted
}

Optionがどのバリアントに分類されるかはまだわかりませんが、とりあえずパターンマッチで処理すれば良さそうです。

fn is_option(ty: &Type) -> bool {
    match ty {
        todo!()
    }
}

パターンマッチで分類できそうだというところまで方針を立てられましたが、Optionはどのバリアントに分類されるのでしょうか。ドキュメントを一見してもそれらしいものは見当たりませんが、OptionType::Path(syn::TypePath)に分類されます。TypePathstd::iter::IterのようなPathをパースして得られる構造体です。Type::Pathにマッチしないバリアントについてはこの時点でfalseを返してしまって問題ないでしょう。

fn is_option(ty: &Type) -> bool {
    match ty {
        Type::Path(path) => todo!(),
        _ => false
    }
}

TypePathにはOption以外にも上述のstd::iter::Iterのようなものも含まれます。どのようにOptionかそれ以外かを判定すれば良いでしょうか。

先ほども説明したように、TypePathstd::iter::Iterのようにコロン 2 つで分割されたセグメントの集合でした。つまり、セグメントの集合の最後の要素がOptionであるかどうかを判断すれば良さそうです。

では、どのようにしてセグメントの集合の最後の要素を取得すれば良いのでしょうか。syn::TypePathは以下のような構造体で、pathPath の情報を保持しています。syn::Pathは以下のようにsegmentsにセグメントの集合を保持しており、segments.last()で最後の要素にアクセスできます。

pub struct TypePath {
    pub qself: Option<QSelf>,
    pub path: Path
}

pub struct Path {
    pub leading_colon: Option<Colon2>,
    pub segments: Punctuated<PathSegment, Colon2>,
}

上記を踏まえると、現時点での実装は以下のようになります。

fn is_option(ty: &Type) -> bool {
    match ty {
        Type::Path(path) => path.path.segments.last(),
        _ => false
    }
}

最後に、セグメントの最後の要素の識別子がOptionと一致するか比較する必要があります。segments.last()の戻り値はPathSegmentであり、identフィールドから識別子にアクセスできます。この識別子がOptionかどうかを比較すれば良さそうです。

最終的なis_option関数の実装は以下のようになります。

fn is_option(ty: &Type) -> bool {
    match ty {
        Type::Path(path) => match path.path.segments.last().unwrap() {
            Some(seg) => seg.ident == "Option",
            None => false
        },
        _ => false
    }
}
unwrap_option関数

次はunwrap_option関数を実装してきます。unwrap_option関数は与えられた型がOptionであればSome(型)を返し、そうでなければNoneを返す関数なので、シグネチャは以下のようにすれば良いでしょう。

fn unwrap_option(ty: &Type) -> Option<&Type>

引数がOptionでない場合はNoneを返します。引数がOptionかどうかは先ほど実装したis_option関数が使えます。

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
     }
     todo!()
}

次にOptionにラップされた中身の型を取り出す処理を実装します。中身の型はどうやって取り出せば良いでしょうか。 is_optionの実装ででてきたPathSegmentのフィールドにはident以外にもう 1 つargumentsというのがありました。これが関係ありそうなので、PathArgumentsの定義を見て見ましょう。

pub enum PathArguments {
    None,
    AngleBracketed(AngleBracketedGenericArguments),
    Parenthesized(ParenthesizedGenericArguments),
}

PathArgumentsenum のようです。バリアント名を眺めてみるとAngleBracketedというものがあります。これが関係ありそうです。実際、ドキュメントAngleBracketedの項目には以下のように記載されており、このバリアントがジェネリック引数の情報を持っていることがわかります。

AngleBracketed(AngleBracketedGenericArguments)

The <'a, T> in std::slice::iter<'a, T>.

syn::AngleBracketedGenericArgument型の定義を見てみましょう。

pub struct AngleBracketedGenericArguments {
    pub colon2_token: Option<Colon2>,
    pub lt_token: Lt,
    pub args: Punctuated<GenericArgument, Comma>,
    pub gt_token: Gt,
}

フィールド名を眺めてみるとジェネリック引数の型はargsに入っていそうです。syn::Punctuatedなので複数の要素がありそうですが、Optionは引数を 1 つしか取らないのでfirst()で取得すれば良いでしょう。

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
     }
    match ty {
        Type::Path(path) => path.path.segments.last().map(|seg| {
            match seg.arguments {
                PathArguments::AngleBracketed(ref args) => args.args.first(),
                _ => None
            }
        }),
        _ => None
    }
}

args.first()の戻り値はOption<GenericArgument>です。GenericArgumentは以下のような enum で、型が含まれる場合はGenericArgument::Typeバリアントが使用されるので、Typeバリアントにマッチさせれば良いでしょう。

pub enum GenericArgument {
    Lifetime(Lifetime),
    Type(Type),
    Binding(Binding),
    Constraint(Constraint),
    Const(Expr),
}

上記を踏まえると、unwrap_optionの最終的な実装は以下のようになります。

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
     }
    match ty {
        Type::Path(path) => path.path.segments.last().map(|seg| {
            match seg.arguments {
                PathArguments::AngleBracketed(ref args) => {
                    args.args.first().and_then(|arg| match arg {
                        &GenericArgument::Type(ref ty) => Some(ty),
                        _ => None,
                    })
                }
                _ => None
            }
        }),
        _ => None
    }
}

Path の最後の要素を持ってくる処理はまとめられるので別の関数として外に切り出します。これを用いてis_optionunwrap_optionを書き直すと、最終的には以下のようになります。

fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        Type::Path(path) => path.path.segments.last(),
        _ => None,
    }
}

fn is_option(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Option",
        _ => false,
    }
}

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
    }
    match get_last_path_segment(ty) {
        Some(seg) => match seg.arguments {
            PathArguments::AngleBracketed(ref args) => {
                args.args.first().and_then(|arg| match arg {
                    &GenericArgument::Type(ref ty) => Some(ty),
                    _ => None,
                })
            }
            _ => None,
        },
        None => None,
    }
}

最終的な実装は以下のようになります。

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Data, DeriveInput, Fields, GenericArgument, Ident, PathArguments,
    PathSegment, Type,
};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let vis = input.vis;

    let builder_name = format_ident!("{}Builder", ident);
    let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields
                .named
                .into_iter()
                .map(|field| {
                    let ident = field.ident;
                    let ty = field.ty;
                    (ident.unwrap(), ty)
                })
                .unzip(),
            _ => panic!("no unnamed fields are allowed"),
        },
        _ => panic!("expects struct"),
    };

    let builder_fields = idents.iter().zip(&types).map(|(ident, ty)| {
        let t = unwrap_option(ty).unwrap_or(ty);
        quote! {
            #ident: Option<#t>
        }
    });

    let checks = idents
        .iter()
        .zip(&types)
        .filter(|(_, ty)| !is_option(ty))
        .map(|(ident, _)| {
            let err = format!("Required field '{}' is missing", ident.to_string());
            quote! {
                if self.#ident.is_none() {
                    return Err(#err.into())
                }
            }
        });

    let setters = idents.iter().zip(&types).map(|(ident, ty)| {
        let t = unwrap_option(ty).unwrap_or(ty);
        quote! {
            pub fn #ident(&mut self, #ident: #t) -> &mut Self {
                self.#ident = Some(#ident);
                self
            }
        }
    });

    let struct_fields = idents.iter().zip(&types).map(|(ident, ty)| {
        if is_option(ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    let expand = quote! {
        #vis struct #builder_name {
            #(#builder_fields),*
        }

        impl #builder_name {
            #(#setters)*

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

        impl #ident {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#idents: None),*
                }
            }
        }
    };
    proc_macro::TokenStream::from(expand)
}

fn is_option(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Option",
        _ => false,
    }
}

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
    }
    match get_last_path_segment(ty) {
        Some(seg) => match seg.arguments {
            PathArguments::AngleBracketed(ref args) => {
                args.args.first().and_then(|arg| match arg {
                    &GenericArgument::Type(ref ty) => Some(ty),
                    _ => None,
                })
            }
            _ => None,
        },
        None => None,
    }
}

fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        Type::Path(path) => path.path.segments.last(),
        _ => None,
    }
}

リファクタリング

derive関数が肥大化してきたので内部の処理を関数として切り出しました。主な変更点は以下の通りです。

  • Builder 構造体の定義を生成する部分を関数化(build_builder_struct
  • Builder 構造体の実装(setter 関数、build関数)を生成する部分を関数化(build_builder_impl
  • builder関数を生成する部分を関数化(build_struct_impl

これらの関数はすべて戻り値としてproc_macro2::TokenStreamを返していますが、これはquoteマクロがproc_macro2::TokenStreamを返すためです。

これらの処理を関数化したのに伴い、もともと以下のようにフィールド名と型を最初に取得していたのを、

let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data {
    Data::Struct(data) => match data.fields {
        Fields::Named(fields) => fields
            .named
            .into_iter()
            .map(|field| {
                let ident = field.ident;
                let ty = field.ty;
                (ident.unwrap(), ty)
            })
            .unzip(),
        _ => panic!("no unnamed fields are allowed"),
    },
    _ => panic!("expects struct"),
};

以下のようにNamedFieldsを取得して各関数にわたすように変更しています。

let fields = match input.data {
    Data::Struct(data) => match data.fields {
        Fields::Named(fields) => fields,
        _ => panic!("no unnamed fields are allowed"),
    },
    _ => panic!("this macro can be applied only to structaa"),
};

リファクタリング後の実装は以下の通りです。

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Ident,
    PathArguments, PathSegment, Type, Visibility,
};

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let vis = input.vis;
    let builder_name = format_ident!("{}Builder", ident);

    let fields = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields,
            _ => panic!("no unnamed fields are allowed"),
        },
        _ => panic!("this macro can be applied only to structaa"),
    };

    let builder_struct = build_builder_struct(&fields, &builder_name, &vis);
    let builder_impl = build_builder_impl(&fields, &builder_name, &ident);
    let struct_impl = build_struct_impl(&fields, &builder_name, &ident);

    let expand = quote! {
        #builder_struct
        #builder_impl
        #struct_impl
    };
    proc_macro::TokenStream::from(expand)
}

fn build_builder_struct(
    fields: &FieldsNamed,
    builder_name: &Ident,
    visibility: &Visibility,
) -> TokenStream {
    let (idents, types): (Vec<&Ident>, Vec<&Type>) = fields
        .named
        .iter()
        .map(|field| {
            let ident = field.ident.as_ref();
            let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
            (ident.unwrap(), ty)
        })
        .unzip();
    quote! {
        #visibility struct #builder_name {
            #(#idents: Option<#types>),*
        }
    }
}

fn build_builder_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let checks = fields
        .named
        .iter()
        .filter(|field| !is_option(&field.ty))
        .map(|field| {
            let ident = field.ident.as_ref();
            let err = format!("Required field '{}' is missing", ident.unwrap().to_string());
            quote! {
                if self.#ident.is_none() {
                    return Err(#err.into());
                }
            }
        });

    let setters = fields.named.iter().map(|field| {
        let ident = &field.ident;
        let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
        quote! {
            pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                self.#ident = Some(#ident);
                self
            }
        }
    });

    let struct_fields = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        if is_option(&field.ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    quote! {
        impl #builder_name {
            #(#setters)*

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

fn build_struct_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let field_defaults = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        quote! {
            #ident: None
        }
    });
    quote! {
        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#field_defaults),*
                }
            }
        }
    }
}

fn is_option(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Option",
        _ => false,
    }
}

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
    }
    match get_last_path_segment(ty) {
        Some(seg) => match seg.arguments {
            PathArguments::AngleBracketed(ref args) => {
                args.args.first().and_then(|arg| match arg {
                    &GenericArgument::Type(ref ty) => Some(ty),
                    _ => None,
                })
            }
            _ => None,
        },
        None => None,
    }
}

fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        Type::Path(path) => path.path.segments.last(),
        _ => None,
    }
}

07-repeated-field

目標

  • 以下の 2 つの方法でベクタを値としてもつフィールドを更新できるようにする
    • ベクタを与えて一括で更新する
    • ベクタの要素を 1 つずつ追加する
  • 一括で更新するための関数名はフィールド名と同じにする
  • ベクタの要素を 1 つずつ追加する関数の名前は以下のようにアトリビュートを用いて指定する
    • 1 つずつ追加するための関数名として一括で更新するための関数名と同じ名前が指定された場合は 1 つずつ追加するための関数を優先する
#[derive(Builder)]
pub struct Command {
   executable: String,
   #[builder(each = "arg")]
   args: Vec<String>,
   #[builder(each = "env")]
   env: Vec<String>,
   current_dir: Option<String>,
}

実装方針

目標とする機能を実現するために実装する必要があるのは以下の項目です。

  • フィールドに付与されたアトリビュートを取得する
  • Vec型のフィールドの setter を一括更新用と要素追加用の 2 種類生成する

また、要素を 1 つずつ追加できるようにするためには以下の機能の実装も必要です。

  • Builder 構造体のフィールドではVec型の変数はOptionでラップしない
    • フィールドの型がVecかどうか判定できるようにする
    • Vecをアンラップして中身の型を取得できるようにする

実装

Builder 構造体のフィールドではVec型の変数はOptionでラップしない

Builder 構造体の定義を生成しているのはbuild_builder_struct関数です。今の実装では入力の型に関わらずOptionでラップしています。これをVecのみラップしないように変更すれば良さそうです。

fn build_builder_struct(
    fields: &FieldsNamed,
    builder_name: &Ident,
    visibility: &Visibility,
) -> TokenStream {
    let (idents, types): (Vec<&Ident>, Vec<&Type>) = fields
        .named
        .iter()
        .map(|field| {
            let ident = field.ident.as_ref();
            let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
            (ident.unwrap(), ty)
        })
        .unzip();
    quote! {
        #visibility struct #builder_name {
            #(#idents: Option<#types>),*
        }
    }
}

is_vector関数はis_optionと似た関数で、与えられた型がVec型かどうかを判定する関数です。実装は後述します。

fn build_builder_struct(
    fields: &FieldsNamed,
    builder_name: &Ident,
    visibility: &Visibility,
) -> TokenStream {
    let struct_fields = fields
        .named
        .iter()
        .map(|field| {
            let ident = field.ident.as_ref();
            let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
            (ident.unwrap(), ty)
        })
        .map(|(ident, ty)| {
            if is_vector(&ty) {
                quote! {
                    #ident: #ty
                }
            } else {
                quote! {
                    #ident: Option<#ty>
                }
            }
        });
    quote! {
        #visibility struct #builder_name {
            #(#struct_fields),*
        }
    }
}

Builder 構造体の定義が変わったので、Builder 構造体を返すbuilder関数の実装を生成するbuild_struct_impl関数も修正が必要です。Vec型のフィールドのみVec::new()を返すようにすれば良さそうです。

fn build_struct_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let field_defaults = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        quote! {
            #ident: None
        }
    });
    quote! {
        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#field_defaults),*
                }
            }
        }
    }
}

こちらもbuild_builder_struct関数と同様、is_vector関数を使って条件分岐を記述しています。

fn build_struct_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let field_defaults = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        let ty = &field.ty;
        if is_vector(&ty) {
            quote! {
                #ident: Vec::new()
            }
        } else {
            quote! {
                #ident: None
            }
        }
    });
    quote! {
        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#field_defaults),*
                }
            }
        }
    }
}

また、Vec型のフィールドは要素を含まなくても問題ないので、Vec型のフィールドについてもガード節を生成しないように変更します。今はOptionのみをフィルタしていますが、追加でVecもフィルタするように変更します。

    let checks = fields
        .named
        .iter()
        .filter(|field| !is_option(&field.ty))
        .filter(|field| !is_vector(&field.ty))
        .map(|field| {
            let ident = field.ident.as_ref();
            let err = format!("Required field '{}' is missing", ident.unwrap().to_string());
            quote! {
                if self.#ident.is_none() {
                    return Err(#err.into());
                }
            }
        });
is_vectorunwrap_vectorの実装

ここからはis_vectorunwrap_vectorを実装していきます。ここまでの実装ではunwrap_vectorはでてきませんが、今後使うのでここで実装しておきます。 VecOptionと同様Type::Pathに分類されるので、以下の項目は 06 で実装したis_optionunwrap_optionを流用できます。

fn is_vector(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Vec",
        _ => false,
    }
}

fn unwrap_vector(ty: &Type) -> Option<&Type> {
    if !is_vector(ty) {
        return None;
    }
    match get_last_path_segment(ty) {
        Some(seg) => match seg.arguments {
            PathArguments::AngleBracketed(ref args) => {
                args.args.first().and_then(|arg| match arg {
                    &GenericArgument::Type(ref ty) => Some(ty),
                    _ => None,
                })
            }
            _ => None,
        },
        None => None,
    }
}

unwrap_vectorのパターンマッチの部分はunwrap_optionと同様の処理をしているので関数化できそうです。これをunwrap_generic_typeという関数にくくり出すと以下のようになります。

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
    }
    unwrap_generic_type(ty)
}

fn unwrap_vector(ty: &Type) -> Option<&Type> {
    if !is_vector(ty) {
        return None;
    }
    unwrap_generic_type(ty)
}

fn unwrap_generic_type(ty: &Type) -> Option<&Type> {
    match get_last_path_segment(ty) {
        Some(seg) => match seg.arguments {
            PathArguments::AngleBracketed(ref args) => {
                args.args.first().and_then(|arg| match arg {
                    &GenericArgument::Type(ref ty) => Some(ty),
                    _ => None,
                })
            }
            _ => None,
        },
        None => None,
    }
}
フィールドに付与されたアトリビュートを取得する

フィールドに付与されたアトリビュートを取得する処理を実装する前に、まずアトリビュートを付与できるようにする必要があります。

アトリビュートを付与できるようにするためには、以下のようにderive関数にattributes(builder)というアトリビュートを追加します(参考)。これでフィールドに#[builder(...)]のようなアトリビュートを付与できます。

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

アトリビュートを付与できるようになったので、それを取得する処理を実装します。 アトリビュートを取得するにはどのデータを処理すれば良いでしょうか。まずはDeriveInputをみてみましょう。

pub struct DeriveInput {
    pub attrs: Vec<Attribute>,
    pub vis: Visibility,
    pub ident: Ident,
    pub generics: Generics,
    pub data: Data,
}

DeriveInputattrsフィールドを持っていますが、以下のDeriveInputattrsの説明にあるように、これは構造体自体に付与されたアトリビュートです。(引用元

Attributes tagged on the whole struct or enum.

構造体のフィールドはdataフィールドに格納されているのでDataの定義をみてみましょう。Dataの構造を下っていくと最終的に構造体の各フィールドの情報を保持しているField構造体が得られます。Dataの構造の詳細については前編を参考にしてください。この中にあるattrsがフィールドに付与されたアトリビュートです。

pub struct Field {
    pub attrs: Vec<Attribute>,
    pub vis: Visibility,
    pub ident: Option<Ident>,
    pub colon_token: Option<Colon>,
    pub ty: Type,
}

構造体のフィールドはderive関数の最初の方で取得しているので、これを処理してアトリビュートを取得していきます。attrsAttributeのベクタになっていますが、今回は 1 つのアトリビュートしか使わないのでfirstで先頭のアトリビュートだけ取得すれば良いでしょう。

        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| todo!());

Attributeparse_meta関数アトリビュートをパースした結果が得られます。

let ident_each_name = field
    .attrs
    .first()
    .map(|attr| attr.parse_meta());

parse_meta()関数の戻り値はResult<Meta>です。Meta型は以下のような enum です。

pub enum Meta {
    Path(Path),
    List(MetaList),
    NameValue(MetaNameValue),
}

MetaListバリアントの説明

List

A meta list is like the derive(Copy) in #[derive(Copy)].

とあるように、今回取得したいのはMeta::Listなのでパターンマッチで処理します。

        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(_)) => todo!(),
                _ => None,
            });

MetaListは以下のような構造体です。

pub struct MetaList {
    pub path: Path,
    pub paren_token: Paren,
    pub nested: Punctuated<NestedMeta, Comma>,
}

MetaList型のpathアトリビュート名(#[builder(each="foo")]builderの部分)を、nestedアトリビュートの値(#[builder(each="foo")]each="foo"の部分)を保持しています。今必要なのはアトリビュートの値を保持するnestedの部分です。nestedアトリビュート値を複数保持していますが、今回は複数の値をもつことは想定していないのでfirstで先頭を取得すれば良さそうです。

        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(list)) => list.nested.first(),
                _ => None,
            });

nested.first()Option<NestedMeta>を返します。NestedMetaは以下のような enum です。

pub enum NestedMeta {
    Meta(Meta),
    Lit(Lit),
}

NestedMetaのフィールドに関する以下の記述からわかるように、Litは Rust のリテラルを保持します。この段階ではnested.first()からSome(each="foo")のような形式が返ってくることを期待しているのでLitではなくMetaにマッチするようにします。

Meta(Meta)

A structured meta item, like the Copy in #[derive(Copy)] which would be a nested Meta::Path.

Lit(Lit)

A Rust literal, like the "new_name" in #[rename("new_name")].

        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(list)) => match list.nested.first() {
                    Some(NestedMeta::Meta(_)) => todo!(),
                    _ => None,
                },
                _ => None,
            });

先ほどはパターンマッチを用いてMeta::Listを取得しましたが、上述のように今度はeach="foo"のようなキーとバリューのペアが取得されることを期待しているので、Meta::NameValue(MetaNameValue)をマッチして処理します。

        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(list)) => match list.nested.first() {
                    Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue{}))) => todo!(),
                    _ => None,
                },
                _ => None,
            });

MetaNameValueは以下のような構造体です。

pub struct MetaNameValue {
    pub path: Path,
    pub eq_token: Eq,
    pub lit: Lit,
}

each = "foo"を例にとると、MetaNameValuepatheachを、lit"foo"を格納します。今回欲しいのは"foo"の方なのでlitだけを取得すれば良さそうです。

        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(list)) => match list.nested.first() {
                    Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue{
                        path: _,
                        eq_token: _,
                        lit
                    }))) => todo!(),
                    _ => None,
                },
                _ => None,
            });

Litは以下のような enum です。each = "foo"のような形式からもわかるように、文字列リテラルLit::Str)が得られることを期待しています。

pub enum Lit {
    Str(LitStr),
    ByteStr(LitByteStr),
    Byte(LitByte),
    Char(LitChar),
    Int(LitInt),
    Float(LitFloat),
    Bool(LitBool),
    Verbatim(Literal),
}

リテラルが表す値はvalueメソッドで取得できるので、

        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(list)) => match list.nested.first() {
                    Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue{
                        path: _,
                        eq_token: _,
                        lit: Lit::Str(ref s)
                    }))) => {
                        Some(s.value())
                    },
                    _ => None,
                },
                _ => None,
            })
            .flatten();

以上で各フィールドの要素追加用のメソッド名を取得できました。

Vec型のフィールドの setter を一括更新用と要素追加用の 2 種類生成する

今まではフィールドの型によらず、単純に以下のように setter を生成していました。

       quote! {
            pub fn #ident(&mut self, #ident: #t) -> &mut Self {
                self.#ident = Some(#ident);
                self
            }
        }

まずアトリビュートが付与されているかどうかで分岐が発生します。アトリビュートが付与されていると要素追加用のメソッドが必要になります。

match ident_each_name {
    Some(name) => todo!(),
    None => todo!(),
}

まずは要素追加用のメソッドがいらない方を実装します。Vec型のフィールドは Option でラップされないのでVec型かどうかで setter の実装が変わります。

match ident_each_name {
    Some(name) => todo!(),
    None => {
        if is_vector(&ty) {
            quote! {
                pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                    self.#ident = #ident;
                    self
                }
            }
        } else {
            quote! {
                pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                    self.#ident = Some(#ident);
                    self
                }
            }
        }
    },
}

次に要素追加用のメソッドが必要なパターンを実装します。こっちは以下の 2 つのパターンで処理が分岐します。

  • 要素追加用のメソッド名がフィールド名と 同じ
    • 要素追加用のメソッドのみ生成する
  • 要素追加用のメソッド名がフィールド名と 異なる
    • 要素追加用のメソッドと一括更新用のメソッドを両方生成する

実装は以下のようになります。実装のポイントは以下の通りです。

  • 要素追加用の関数の引数の型として使用するためにunwrap_vectorで中身の型を取り出す
  • 要素追加用の関数の名前(name)はStringなのでIdent::newIdentを生成する
    • Ident::newの第二引数にはSpan構造体を指定する必要がある。Span構造体はマクロの展開先で識別子が誤って捕捉されないようにするために必要(参考
match ident_each_name {
    Some(name) => {
        let ty_each = unwrap_vector(ty).unwrap();
        let ident_each = Ident::new(name.as_str(), Span::call_site());
        if ident.unwrap().to_string() == name {
            quote! {
                pub fn #ident_each(&mut self, #ident_each:#ty_each) -> &mut Self {
                    self.#ident.push(#ident_each);
                    self
                }
            }
        } else {
            quote! {
                pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                    self.#ident = #ident;
                    self
                }
                pub fn #ident_each(&mut self, #ident_each: #ty_each) -> &mut Self {
                    self.#ident.push(#ident_each);
                    self
                }
            }
        }
    }
    None => {
        (略)
    },
}

最終的な実装は以下のようになります。

use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Ident, Lit, Meta,
    MetaList, MetaNameValue, NestedMeta, PathArguments, PathSegment, Type, Visibility,
};

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let vis = input.vis;
    let builder_name = format_ident!("{}Builder", ident);

    let fields = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields,
            _ => panic!("no unnamed fields are allowed"),
        },
        _ => panic!("this macro can be applied only to struct"),
    };

    let builder_struct = build_builder_struct(&fields, &builder_name, &vis);
    let builder_impl = build_builder_impl(&fields, &builder_name, &ident);
    let struct_impl = build_struct_impl(&fields, &builder_name, &ident);

    let expand = quote! {
        #builder_struct
        #builder_impl
        #struct_impl
    };
    proc_macro::TokenStream::from(expand)
}

fn build_builder_struct(
    fields: &FieldsNamed,
    builder_name: &Ident,
    visibility: &Visibility,
) -> TokenStream {
    let struct_fields = fields
        .named
        .iter()
        .map(|field| {
            let ident = field.ident.as_ref();
            let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
            (ident.unwrap(), ty)
        })
        .map(|(ident, ty)| {
            if is_vector(&ty) {
                quote! {
                    #ident: #ty
                }
            } else {
                quote! {
                    #ident: Option<#ty>
                }
            }
        });
    quote! {
        #visibility struct #builder_name {
            #(#struct_fields),*
        }
    }
}

fn build_builder_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let checks = fields
        .named
        .iter()
        .filter(|field| !is_option(&field.ty))
        .filter(|field| !is_vector(&field.ty))
        .map(|field| {
            let ident = field.ident.as_ref();
            let err = format!("Required field '{}' is missing", ident.unwrap().to_string());
            quote! {
                if self.#ident.is_none() {
                    return Err(#err.into());
                }
            }
        });

    let setters = fields.named.iter().map(|field| {
        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(list)) => match list.nested.first() {
                    Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                        path: _,
                        eq_token: _,
                        lit: Lit::Str(ref str),
                    }))) => Some(str.value()),
                    _ => None,
                },
                _ => None,
            })
            .flatten();

        let ident = field.ident.as_ref();
        let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
        match ident_each_name {
            Some(name) => {
                let ty_each = unwrap_vector(ty).unwrap();
                let ident_each = Ident::new(name.as_str(), Span::call_site());
                if ident.unwrap().to_string() == name {
                    quote! {
                        pub fn #ident_each(&mut self, #ident_each:#ty_each) -> &mut Self {
                            self.#ident.push(#ident_each);
                            self
                        }
                    }
                } else {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = #ident;
                            self
                        }
                        pub fn #ident_each(&mut self, #ident_each: #ty_each) -> &mut Self {
                            self.#ident.push(#ident_each);
                            self
                        }
                    }
                }
            }
            None => {
                if is_vector(&ty) {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = #ident;
                            self
                        }
                    }
                } else {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = Some(#ident);
                            self
                        }
                    }
                }
            }
        }
    });

    let struct_fields = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        if is_option(&field.ty) || is_vector(&field.ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    quote! {
        impl #builder_name {
            #(#setters)*

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

fn build_struct_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let field_defaults = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        let ty = &field.ty;
        if is_vector(&ty) {
            quote! {
                #ident: Vec::new()
            }
        } else {
            quote! {
                #ident: None
            }
        }
    });
    quote! {
        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#field_defaults),*
                }
            }
        }
    }
}

fn is_option(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Option",
        _ => false,
    }
}

fn is_vector(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Vec",
        _ => false,
    }
}

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
    }
    unwrap_generic_type(ty)
}

fn unwrap_vector(ty: &Type) -> Option<&Type> {
    if !is_vector(ty) {
        return None;
    }
    unwrap_generic_type(ty)
}

fn unwrap_generic_type(ty: &Type) -> Option<&Type> {
    match get_last_path_segment(ty) {
        Some(seg) => match seg.arguments {
            PathArguments::AngleBracketed(ref args) => {
                args.args.first().and_then(|arg| match arg {
                    &GenericArgument::Type(ref ty) => Some(ty),
                    _ => None,
                })
            }
            _ => None,
        },
        None => None,
    }
}

fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        Type::Path(path) => path.path.segments.last(),
        _ => None,
    }
}

08-unrecognized-attribute

目標

  • attribute に間違った識別子が与えられた際に適切なコンパイルエラーを表示する

実装方針

  • アトリビュートのキーとしてeach以外のものが与えられた場合にエラーを表示する
    • エラーを発生させたい箇所でsyn::Errorto_compile_errorメソッドでTokenStreamを返すようにする
      • 単純にpanic!させるだけよりも詳細なエラーメッセージを表示させられる

実装

アトリビュートのキーが正しいか判定する必要があるので、アトリビュートを処理している以下の箇所を変更します。

let ident_each_name = field
    .attrs
    .first()
    .map(|attr| match attr.parse_meta() {
        Ok(Meta::List(list)) => match list.nested.first() {
            Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                path: _,
                eq_token: _,
                lit: Lit::Str(ref str),
            }))) => Some(str.value()),
            _ => None,
        },
        _ => None,
    })
    .flatten();

アトリビュートのキーはMetadataNameValuepathに格納されています。pathPath型なので 06-optional-field で処理したのと同様にして識別子を取得します。Identto_stringメソッドで識別子の名前を取得できるので、これがeachと一致しなければエラーを返せば良さそうです。

let ident_each_name = field
    .attrs
    .first()
    .map(|attr| match attr.parse_meta() {
        Ok(Meta::List(list)) => match list.nested.first() {
            Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                ref path,
                eq_token: _,
                lit: Lit::Str(ref str),
            }))) => {
                if let Some(name) = path.segments.first() {
                    if name.ident.to_string() != "each" {
                        todo!()
                    }
                }
                Some(str.value())
            }
            _ => None,
        },
        _ => None,
    })
    .flatten();

syn::Errorsyn::Error::new_spanned()を使って生成します。エラーメッセージ("expected ...")はテストケースに記載されたメッセージをそのまま使います。以下のようにしたいところですが、このままでは型が合わないのでコンパイルできません。

let ident_each_name = field
    .attrs
    .first()
    .map(|attr| match attr.parse_meta() {
        Ok(Meta::List(list)) => match list.nested.first() {
            Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                ref path,
                eq_token: _,
                lit: Lit::Str(ref str),
            }))) => {
                if let Some(name) = path.segments.first() {
                    if name.ident.to_string() != "each" {
                        return Some(syn::Error::new_spanned(
                            list,
                            "expected `builder(each = \"...\")`",
                        ));
                    }
                }
                Some(str.value())
            }
            _ => None,
        },
        _ => None,
    })
    .flatten();

そこで、以下のような enum を返すようにして、後でパターンマッチで処理しましょう。

enum LitOrError {
    Lit(String),
    Error(syn::Error),
}

LitOrErrorを使って先ほどの箇所を次のように書き換えます。

let ident_each_name = field
    .attrs
    .first()
    .map(|attr| match attr.parse_meta() {
        Ok(Meta::List(list)) => match list.nested.first() {
            Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                ref path,
                eq_token: _,
                lit: Lit::Str(ref str),
            }))) => {
                if let Some(name) = path.segments.first() {
                    if name.ident.to_string() != "each" {
                        return Some(LitOrError::Error(syn::Error::new_spanned(
                            list,
                            "expected `builder(each = \"...\")`",
                        )));
                    }
                }
                Some(LitOrError::Lit(str.value()))
            }
            _ => None,
        },
        _ => None,
    })
    .flatten();

パターンマッチで処理していた部分も enum に合わせて変更します。LitOrError::Errorにマッチする場合はコンパイルエラーを生じさせる必要があるので、to_compile_error().into()でエラーを返します。

match ident_each_name {
    Some(LitOrError::Lit(name)) => {
        (略)
    }
    Some(LitOrError::Error(err)) => err.to_compile_error().into(),
    None => {
        (略)
    }
}

今までpanic!していた場所もsyn::Errorto_compile_errorメソッドを使ってTokenStreamを返すように変更しておきます。

let fields = match input.data {
    Data::Struct(data) => match data.fields {
        Fields::Named(fields) => fields,
        _ => {
            return syn::Error::new(ident.span(), "expects named fields")
                .to_compile_error()
                .into()
        }
    },
    _ => {
        return syn::Error::new(ident.span(), "expects struct")
            .to_compile_error()
            .into()
    }
};

最終的な実装は以下のようになります。

use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Ident, Lit, Meta,
    MetaNameValue, NestedMeta, PathArguments, PathSegment, Type, Visibility,
};

enum LitOrError {
    Lit(String),
    Error(syn::Error),
}

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let vis = input.vis;
    let builder_name = format_ident!("{}Builder", ident);

    let fields = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields,
            _ => {
                return syn::Error::new(ident.span(), "expects named fields")
                    .to_compile_error()
                    .into()
            }
        },
        _ => {
            return syn::Error::new(ident.span(), "expects struct")
                .to_compile_error()
                .into()
        }
    };

    let builder_struct = build_builder_struct(&fields, &builder_name, &vis);
    let builder_impl = build_builder_impl(&fields, &builder_name, &ident);
    let struct_impl = build_struct_impl(&fields, &builder_name, &ident);

    let expand = quote! {
        #builder_struct
        #builder_impl
        #struct_impl
    };
    proc_macro::TokenStream::from(expand)
}

fn build_builder_struct(
    fields: &FieldsNamed,
    builder_name: &Ident,
    visibility: &Visibility,
) -> TokenStream {
    let struct_fields = fields
        .named
        .iter()
        .map(|field| {
            let ident = field.ident.as_ref();
            let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
            (ident.unwrap(), ty)
        })
        .map(|(ident, ty)| {
            if is_vector(&ty) {
                quote! {
                    #ident: #ty
                }
            } else {
                quote! {
                    #ident: Option<#ty>
                }
            }
        });
    quote! {
        #visibility struct #builder_name {
            #(#struct_fields),*
        }
    }
}

fn build_builder_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let checks = fields
        .named
        .iter()
        .filter(|field| !is_option(&field.ty))
        .filter(|field| !is_vector(&field.ty))
        .map(|field| {
            let ident = field.ident.as_ref();
            let err = format!("Required field '{}' is missing", ident.unwrap().to_string());
            quote! {
                if self.#ident.is_none() {
                    return Err(#err.into());
                }
            }
        });

    let setters = fields.named.iter().map(|field| {
        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(list)) => match list.nested.first() {
                    Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                        ref path,
                        eq_token: _,
                        lit: Lit::Str(ref str),
                    }))) => {
                        if let Some(name) = path.segments.first() {
                            if name.ident.to_string() != "each" {
                                return Some(LitOrError::Error(syn::Error::new_spanned(
                                    list,
                                    "expected `builder(each = \"...\")`",
                                )));
                            }
                        }
                        Some(LitOrError::Lit(str.value()))
                    }
                    _ => None,
                },
                _ => None,
            })
            .flatten();

        let ident = field.ident.as_ref();
        let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
        match ident_each_name {
            Some(LitOrError::Lit(name)) => {
                let ty_each = unwrap_vector(ty).unwrap();
                let ident_each = Ident::new(name.as_str(), Span::call_site());
                if ident.unwrap().to_string() == name {
                    quote! {
                        pub fn #ident_each(&mut self, #ident_each:#ty_each) -> &mut Self {
                            self.#ident.push(#ident_each);
                            self
                        }
                    }
                } else {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = #ident;
                            self
                        }
                        pub fn #ident_each(&mut self, #ident_each: #ty_each) -> &mut Self {
                            self.#ident.push(#ident_each);
                            self
                        }
                    }
                }
            }
            Some(LitOrError::Error(err)) => err.to_compile_error().into(),
            None => {
                if is_vector(&ty) {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = #ident;
                            self
                        }
                    }
                } else {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = Some(#ident);
                            self
                        }
                    }
                }
            }
        }
    });

    let struct_fields = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        if is_option(&field.ty) || is_vector(&field.ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    quote! {
        impl #builder_name {
            #(#setters)*

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

fn build_struct_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let field_defaults = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        let ty = &field.ty;
        if is_vector(&ty) {
            quote! {
                #ident: Vec::new()
            }
        } else {
            quote! {
                #ident: None
            }
        }
    });
    quote! {
        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#field_defaults),*
                }
            }
        }
    }
}

fn is_option(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Option",
        _ => false,
    }
}

fn is_vector(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Vec",
        _ => false,
    }
}

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
    }
    unwrap_generic_type(ty)
}

fn unwrap_vector(ty: &Type) -> Option<&Type> {
    if !is_vector(ty) {
        return None;
    }
    unwrap_generic_type(ty)
}

fn unwrap_generic_type(ty: &Type) -> Option<&Type> {
    match get_last_path_segment(ty) {
        Some(seg) => match seg.arguments {
            PathArguments::AngleBracketed(ref args) => {
                args.args.first().and_then(|arg| match arg {
                    &GenericArgument::Type(ref ty) => Some(ty),
                    _ => None,
                })
            }
            _ => None,
        },
        None => None,
    }
}

fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        Type::Path(path) => path.path.segments.last(),
        _ => None,
    }
}

09-redefined-prelude-types

目標

  • std::preludeでインポートされる型(OptionVector)などがユーザーによって再定義されても正しく使えるようにする

実装方針

OptionVec などを名前空間を指定して使うようにすればよいです。テストコードでチェックされているのは以下の 5 つなので、今回はこれらの適切な名前空間を指定するように変更します。

変更前 変更後
Option std::option::Option
Some std::option::Option::Some
None std::option::Option::None
Result std::result::Result
Box std::boxed::Box

再定義の影響を受けるのはマクロが呼び出されて展開される部分のみなので、直すのは quote マクロの中にある部分だけで十分です。

実装

上述の 5 つの名前空間を正しく指定するだけなので、今回は最終的な結果のみを記載します。最終的な実装は以下のようになります。

use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use syn::{
    parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Ident, Lit, Meta,
    MetaNameValue, NestedMeta, PathArguments, PathSegment, Type, Visibility,
};

enum LitOrError {
    Lit(String),
    Error(syn::Error),
}

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let vis = input.vis;
    let builder_name = format_ident!("{}Builder", ident);

    let fields = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields,
            _ => {
                return syn::Error::new(ident.span(), "expects named fields")
                    .to_compile_error()
                    .into()
            }
        },
        _ => {
            return syn::Error::new(ident.span(), "expects struct")
                .to_compile_error()
                .into()
        }
    };

    let builder_struct = build_builder_struct(&fields, &builder_name, &vis);
    let builder_impl = build_builder_impl(&fields, &builder_name, &ident);
    let struct_impl = build_struct_impl(&fields, &builder_name, &ident);

    let expand = quote! {
        #builder_struct
        #builder_impl
        #struct_impl
    };
    proc_macro::TokenStream::from(expand)
}

fn build_builder_struct(
    fields: &FieldsNamed,
    builder_name: &Ident,
    visibility: &Visibility,
) -> TokenStream {
    let struct_fields = fields
        .named
        .iter()
        .map(|field| {
            let ident = field.ident.as_ref();
            let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
            (ident.unwrap(), ty)
        })
        .map(|(ident, ty)| {
            if is_vector(&ty) {
                quote! {
                    #ident: #ty
                }
            } else {
                quote! {
                    #ident: std::option::Option<#ty>
                }
            }
        });
    quote! {
        #visibility struct #builder_name {
            #(#struct_fields),*
        }
    }
}

fn build_builder_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let checks = fields
        .named
        .iter()
        .filter(|field| !is_option(&field.ty))
        .filter(|field| !is_vector(&field.ty))
        .map(|field| {
            let ident = field.ident.as_ref();
            let err = format!("Required field '{}' is missing", ident.unwrap().to_string());
            quote! {
                if self.#ident.is_none() {
                    return Err(#err.into());
                }
            }
        });

    let setters = fields.named.iter().map(|field| {
        let ident_each_name = field
            .attrs
            .first()
            .map(|attr| match attr.parse_meta() {
                Ok(Meta::List(list)) => match list.nested.first() {
                    Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue {
                        ref path,
                        eq_token: _,
                        lit: Lit::Str(ref str),
                    }))) => {
                        if let Some(name) = path.segments.first() {
                            if name.ident.to_string() != "each" {
                                return Some(LitOrError::Error(syn::Error::new_spanned(
                                    list,
                                    "expected `builder(each = \"...\")`",
                                )));
                            }
                        }
                        Some(LitOrError::Lit(str.value()))
                    }
                    _ => None,
                },
                _ => None,
            })
            .flatten();

        let ident = field.ident.as_ref();
        let ty = unwrap_option(&field.ty).unwrap_or(&field.ty);
        match ident_each_name {
            Some(LitOrError::Lit(name)) => {
                let ty_each = unwrap_vector(ty).unwrap();
                let ident_each = Ident::new(name.as_str(), Span::call_site());
                if ident.unwrap().to_string() == name {
                    quote! {
                        pub fn #ident_each(&mut self, #ident_each:#ty_each) -> &mut Self {
                            self.#ident.push(#ident_each);
                            self
                        }
                    }
                } else {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = #ident;
                            self
                        }
                        pub fn #ident_each(&mut self, #ident_each: #ty_each) -> &mut Self {
                            self.#ident.push(#ident_each);
                            self
                        }
                    }
                }
            }
            Some(LitOrError::Error(err)) => err.to_compile_error().into(),
            None => {
                if is_vector(&ty) {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = #ident;
                            self
                        }
                    }
                } else {
                    quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = std::option::Option::Some(#ident);
                            self
                        }
                    }
                }
            }
        }
    });

    let struct_fields = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        if is_option(&field.ty) || is_vector(&field.ty) {
            quote! {
                #ident: self.#ident.clone()
            }
        } else {
            quote! {
                #ident: self.#ident.clone().unwrap()
            }
        }
    });

    quote! {
        impl #builder_name {
            #(#setters)*

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

fn build_struct_impl(
    fields: &FieldsNamed,
    builder_name: &Ident,
    struct_name: &Ident,
) -> TokenStream {
    let field_defaults = fields.named.iter().map(|field| {
        let ident = field.ident.as_ref();
        let ty = &field.ty;
        if is_vector(&ty) {
            quote! {
                #ident: std::vec::Vec::new()
            }
        } else {
            quote! {
                #ident: std::option::Option::None
            }
        }
    });
    quote! {
        impl #struct_name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(#field_defaults),*
                }
            }
        }
    }
}

fn is_option(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Option",
        _ => false,
    }
}

fn is_vector(ty: &Type) -> bool {
    match get_last_path_segment(ty) {
        Some(seg) => seg.ident == "Vec",
        _ => false,
    }
}

fn unwrap_option(ty: &Type) -> Option<&Type> {
    if !is_option(ty) {
        return None;
    }
    unwrap_generic_type(ty)
}

fn unwrap_vector(ty: &Type) -> Option<&Type> {
    if !is_vector(ty) {
        return None;
    }
    unwrap_generic_type(ty)
}

fn unwrap_generic_type(ty: &Type) -> Option<&Type> {
    match get_last_path_segment(ty) {
        Some(seg) => match seg.arguments {
            PathArguments::AngleBracketed(ref args) => {
                args.args.first().and_then(|arg| match arg {
                    &GenericArgument::Type(ref ty) => Some(ty),
                    _ => None,
                })
            }
            _ => None,
        },
        None => None,
    }
}

fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> {
    match ty {
        Type::Path(path) => path.path.segments.last(),
        _ => None,
    }
}

まとめ

builder マクロを題材にして前編と後編に分けて手続き的マクロの実装方法を説明してきました。

今回実装したマクロはフィールドの型がOption<Vec<_>>であるケースや、Vec型のフィールド以外にeachを付与した場合などを考慮しておらず、実装した機能は十分ではありません。テストも十分なケースを網羅しているとは言えません。

しかし今回の記事で手続き的マクロをどのようにして作るかは一通り理解でき、今後やる時もどこから手をつければよいかだいたい感覚がつかめたかと思います。この記事が今後みなさんがマクロを実装するときの助けになれば幸いです。

参考文献