proc_macro_workshopでRustの手続き的マクロに入門する 前編
はじめに
この記事では proc_macro_workshop というリポジトリを使って Rust の手続き的マクロの作り方を学んでいきます。想定している読者は以下のような方です。
- Rust の基本的な文法や概念(トレイトや所有権、ライフタイムなど)を知っている
- 手続き的マクロの作り方について知りたい
この記事では以下のことを説明します。
- Rust のマクロの概要
- 手続き的マクロ(
derive
マクロ)の作り方 - proc_macro_workshop の進め方
また、この記事では以下のことは説明しません。
- Rust の基本的な文法や概念
- 宣言的マクロの作り方
Rust のマクロ
Rust のマクロには宣言的マクロと手続き的マクロの 2 種類があります。
宣言的マクロ
宣言的マクロは macro_rules!
構文を使用して定義されるマクロで、match 式に似た形で処理内容を定義します。 vec!
や println!
など普段良く使うマクロも宣言的マクロです。
宣言的マクロについてはThe Rust Programming Language 日本語版にvec!
マクロを例にした解説があるので、そちらをご覧ください。
手続き的マクロ
手続き的マクロはトレイトのデフォルト実装の導出に用いる derive
マクロなどに代表されるマクロです。宣言的マクロよりも複雑な処理を記述できることが特徴で、以下の 3 種類があります。
- derive マクロ
- 構造体やenumに付与することでその構造体やenumに追加の処理を実装できます
- 関数風マクロ
- 関数風マクロは宣言的マクロのように関数呼び出しと似た形で使用できるマクロです。宣言的マクロと比較するとより複雑な処理を記述できます
- 属性風マクロ
- deriveマクロと同様に付与した対象に対して追加の処理を実装できますが、こちらは構造体やenumだけでなく関数などにも適用できます。テストを書くときに用いる
#[test]
アトリビュートなどがこれにあたります
手続き的マクロについてはThe Rust Programming Language 日本語版により詳しい説明があるのですが、これだけで実際に使えるような手続き的マクロを作り始めるのは難しいかと思います。この記事ではproc_macro_workshop というリポジトリを使って実際に手を動かしながら手続的マクロの作り方を学んでいきます。
proc_macro_workshop
proc_macro_workshop について
proc_macro_workshop には作成するマクロの種類によって 5 つのプロジェクトがあります。
- derive マクロ
derive(Builder)
- derive マクロ
derive(CustomDebug)
- 関数風マクロ
seq!
- 属性風マクロ
#[sorted]
- 属性風マクロ
#[bitfield]
これらのマクロの内容についてはproc_macro_workshop リポジトリをご覧ください。
この記事では、Builder パターンに必要な実装を導出する derive(Builder)
プロジェクトを進めつつ手続き的マクロの作成方法を学んでいきます。
derive(Builder)
プロジェクトを進めると最終的には以下のような使い方のできるマクロが完成します。
use derive_builder::Builder;
#[derive(Builder)]
pub struct Command {
executable: String,
#[builder(each = "arg")]
args: Vec<String>,
#[builder(each = "env")]
env: Vec<String>,
current_dir: Option<String>,
}
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.arg("build".to_owned())
.arg("--release".to_owned())
.build()
.unwrap();
}
proc_macro_workshop の進め方
proc_macro_workshop の各プロジェクトは以下の流れで進めていきます。
- テストケースを追加する
tests/progress.rs
に記載されているテストケースのコメントアウトを解除する
- 追加したテストケースをパスするようなマクロを実装する
src/lib.rs
に実装します
- テストをパスしたら 1.に戻る
proc_macro_workshop の各プロジェクトにはそれぞれのマクロがパスすべきテストが記載されており、これらのテストに通過するようなマクロを実装することが目標となります。derive(Builder)
プロジェクトの場合はbuilder/tests
ディレクトリにテストケースが記載されたファイルが格納されています。各ステップでテストケースを 1 つずつ増やしていき、これに対応した機能を実装してきます。
プロジェクトの進捗はtests/progress.rs
を用いて管理します。tests/progress.rs
は以下のようになっており、各行がそれぞれのテストケースに対応しています。各ステップでテストケースのコメントアウトを解除し、それぞれのテストを通過するように実装を進めていきます。
#[test]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/01-parse.rs");
//t.pass("tests/02-create-builder.rs");
//t.pass("tests/03-call-setters.rs");
//t.pass("tests/04-call-build.rs");
//t.pass("tests/05-method-chaining.rs");
//t.pass("tests/06-optional-field.rs");
//t.pass("tests/07-repeated-field.rs");
//t.compile_fail("tests/08-unrecognized-attribute.rs");
//t.pass("tests/09-redefined-prelude-types.rs");
}
各テストケースにおける目標はテストケースが記載されているファイルのコメントに書いてあります。実際に作業する際はこれらのコメントを参考にしつつ実装していくことになります。
テストは builder ディレクトリでcargo test
コマンドを実行することで実行できます。
derive(Builder)
マクロを作る
derive(Builder)
プロジェクトでは大まかに以下のような流れでマクロを作成していきます。
- マクロのひな型を用意する
- 01-parse で作業します
- builder パターンを実現するために必要な機能(setter メソッドなど)を実装する
- 02-create-builder ~ 05-method-chaining で作業します
- エラーハンドリングなどを実装する
- 06-optional-field ~ 09-redefined-prelude-types で作業します
それでは実際にマクロを書いていきましょう。以降の各節の名前はテストファイルの名称と対応しています。以降の各節では以下の流れで進めていきます。
- ステップの目標を確認する
- 実際には具体的なテストケースがありますが、すべて記載すると煩雑なので各ステップのゴールのみ記載します
- 実装方針の説明
- 実装
- 各ステップの最後にテストケースをパスする実装例を提示します
マクロの処理の流れ
Rust の手続き型マクロの処理は基本的に以下のようになっています。本記事ではこの内容を具体的に実装していきます。
- トークン列を入力として受け取る
- 受け取ったトークン列を構文木に変換する
- 変換した構文木をもとに処理を行い所望の構文木を得る
- 得られた構文木をトークン列に変換して返す
トークン列と構文木の相互変換にはsyn
クレートとquote
クレートがよく使われます。この記事でもこれらのクレートを用いてマクロを実装します。
syn
クレートはトークン列から構文木への変換に使用します。また、syn
クレートには構文木に関する構造体が定義されており、パースして得られた構文木はこれらの構造体のインスタンスとして保持されます。quote
クレートは構文木からトークン列への変換に使用します。
マクロを開発する上での Tips
dbg!
マクロの使用
マクロのデバッグをする際などには構文木の中身が実際にどうなっているかを知りたくなることがあるかもしれません。syn
クレートのextra-traits
フィーチャーを有効にするとこれらの構造体にDebug
トレイトが実装されてdbg!
マクロを使ったデバッグができます。開発する上で便利なので開発中は有効にしておくと良いでしょう。
syn = { version = "1.0.86", features = ["extra-traits"] }
cargo-expand
cargo expand を使うとマクロによって生成されたコードを出力できます。これもデバッグに便利なので詰まった時は使うと良いでしょう。proc-macro-workshop の GitHub リポジトリとcargo-expand の GitHub リポジトリに使い方の説明があるので、使う場合はこちらを参照してください。
01-parse
目標
- 空の derive マクロを作る
実装方針
テストが実行されるのはtests/01-parse.rs
のmain
関数ですが、このステップでは何も記載がありません。ですので、特に何もしなくてもテストをパスする…かというとそうではなく、#[derive(Builder)]
が使われた際に呼ばれる derive マクロが存在している必要があります。
なぜかというと、テストコード中で以下のように構造体に対して derive マクロが呼ばれているためです。
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
マクロの基本的な処理内容についてはすでに説明しましたが、より具体的には以下のような流れになっています。
- トークン列
proc_macro::TokenStream
(以降では基本的にTokenStream
と記載します)を引数として受け取る syn::parse_macro_input!
マクロでパースして構文木にする- パースした構文木を元に所望の構文木を生成する
- 生成した構文木を
TokenStream
に変換して返す
このステップではマクロが存在していればよいので、1.と 4.の処理を実装すればテストをパスします。
実装
それでは実際に実装をしていきます。
まずは derive マクロの定義から始めます。関数にproc_macro_derive
アトリビュートをつけることでその関数内に記載された処理が derive マクロの実装になります。
derive マクロのシグネチャは(TokenStream) -> TokenStream
になっており、実態は Rust のトークンをTokenStream
という構造体で受け取り、内部で処理した後にTokenStream
という構造体として返す関数です。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {}
では関数の中身を実装してきましょう。最低限テストが通るためにはこの関数からTokenStream
を返せば良いので、以下のようにTokenStream
を新しく作って返してやれば OK です。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
proc_macro::TokenStream::new()
}
このままでも良いのですが、次のステップへの準備として入力をパースできるようにもしておきましょう。入力のパースにはsyn::parse_macro_input!
マクロを使います。derive マクロの入力はsyn::DeriveInput
で定義される構造をしているのでDeriveInput
としてパースします。入力をパースする処理を追加すると最終的なマクロの実装は以下のようになります。
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _input = parse_macro_input!(input as DeriveInput);
proc_macro::TokenStream::new()
}
02-create-builder
目標
CommandBuilder
構造体のインスタンスを返すbuilder
関数をCommand
構造体に実装する
実装方針
最終的には以下のようなCommandBuilder
構造体のインスタンスを返すbuilder
関数をCommand
構造体に実装するのが目的です。
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<Vec<String>>,
}
必要な作業は大きく分けて以下の 2 つです。
builder
関数をCommand
構造体に実装するCommandBuilder
構造体の定義を実装する- 汎用的に使えるようにするには Builder 構造体の構造体名やフィールドの名前、型は derive マクロが適用される構造体に応じたものにする必要がある
これを同時に全部進めるのはたいへんですので、以下の順に進めていきます。
- 空の
builder
関数をCommand
構造体に実装する CommandBuilder
を返すようにbuilder
関数の実装を変更する- 構造体名、フィールド名に応じた Builder 構造体を生成する
実装
空のbuilder
関数をCommand
構造体に実装する
前のステップで説明したように、手続き的マクロの処理の概要は以下の通りです。1.と 2.は前のステップで実装しました。このステップでは 3.と 4.の処理を実装していきます。
まずは空のbuilder
関数を実装する処理を書いていきましょう。この段階では入力について気にする必要はありません。
proc_macro::TokenStream
(以降では基本的にTokenStream
と記載します)を引数として受け取るsyn::parse_macro_input!
マクロでパースして構文木にする- パースした構文木を元に所望の構文木を生成する
- 生成した構文木を
TokenStream
に変換して返す
トークン列を生成するためにはquote
クレートのquote!
マクロを使います。以下のように Rust のコードと(ほぼ)同じ構文で記述でき、この内容がトークン列に変換されます。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _input = parse_macro_input!(input as DeriveInput);
let expand = quote! {
impl Command {
pub fn builder() {}
}
};
proc_macro::TokenStream::new()
}
このままだとproc_macro::TokenStream::new()
によって生成された空のトークン列が戻り値として返るので、生成したトークン列を返すようにしましょう。quote!
マクロを使うとproc_macro::TokenStream
ではなくproc_macro2::TokenStream
が生成されます。proc_macro2::TokenStream
はproc_macro2
クレートによって提供されるトークン列です。proc_macro::TokenStream
が Rust コンパイラの使用するトークン列です。
proc_macro2::TokenStream
はproc_macro::TokenStream::from
でproc_macro::TokenStream
に変換できるので、以下のようにして生成したトークン列を返します。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _input = parse_macro_input!(input as DeriveInput);
let expand = quote! {
impl Command {
pub fn builder() {}
}
};
proc_macro::TokenStream::from(expand)
}
この段階でのマクロの実装は以下のようになります。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _input = parse_macro_input!(input as DeriveInput);
let expand = quote! {
impl Command {
pub fn builder() {}
}
};
proc_macro::TokenStream::from(expand)
}
CommandBuilder
を返すようにbuilder
関数の実装を変更する
前のステップで実装したbuilder
関数は空ですので、このままでは何もしてくれません。次の段階として、ハードコードされたCommandBuilder
構造体を返すようにbuilder
関数の実装を変更してみましょう。
まずはCommandBuilder
構造体の定義をquote!
マクロ内に追加します。
let expand = quote! {
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<Vec<String>>,
}
impl Command {
pub fn builder() {}
}
};
次にbuilder
関数内からデフォルト値でフィールドを埋めたCommandBuilder
を返すように変更します。CommandBuilder
のフィールドはすべて Optional なのでNone
で埋めておきます。
let expand = quote! {
...略...
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder {
executable: None,
args: None,
env: None,
current_dir: None,
}
}
}
};
最終的な実装は以下のようになります。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _input = parse_macro_input!(input as DeriveInput);
let expand = quote! {
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<Vec<String>>,
}
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder {
executable: None,
args: None,
env: None,
current_dir: None,
}
}
}
};
proc_macro::TokenStream::from(expand)
}
構造体名、フィールド名に応じた Builder 構造体を生成する
さて、ここまででCommand
構造体用の Builder 構造体を返すマクロができました。ただ、このままだとほかの構造体に対して使用できません。derive マクロが適用された構造体の名前やフィールドに応じて適した実装を与えるようにしたいです。Builder 構造体の要件をまとめると以下のようになります。
- 構造体の名前は derive マクロの適用された構造体名の末尾に
Builder
を付けたものとする - フィールド名は derive マクロの適用された構造体のフィールド名と同じものを使う
- フィールドの型は derive マクロの適用された構造体のフィールドの型を Optional にしたものとする
このような要件を満たすためには derive マクロが適用された構造体の情報を取得する必要があります。では、どこから取得すれば良いのでしょうか。derive マクロの入力をパースすると以下のようなsyn::DeriveInput
構造体が得られるのですが、実はこの構造体の中に必要な情報が入っています。
pub struct DeriveInput {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub ident: Ident,
pub generics: Generics,
pub data: Data,
}
このステップに関係する部分は以下の 3 つです。
vis
は構造体の可視性の情報を保持していますident
は identifier の略で、変数名などの Rust コード中の識別子の情報を保持しています。ここでは derive マクロが付与された構造体/enum の名前を保持していますdata
は構造体の保持するフィールドの情報を持っています
構造体名はident
を使えば取得できそうです。derive マクロの使われた構造体名の末尾にBuilder
を付けたものを Builder 構造体の名前にしたいので、format_ident!
マクロを使って末尾にBuilder
を結合します。format_ident!
マクロはこのように新たな識別子を作るときに使用します。詳しくはquote
クレートのGitHub リポジトリをご覧ください。
let ident = input.ident;
let builder_name = format_ident!("{}Builder", ident);
構造体名についてはこれで良さそうです。次はフィールド名と型を取得しましょう。フィールドの情報はdata
フィールドに入っているのでした。syn::Data
は以下のような enum です。今回扱うのは構造体なのでDataStruct
構造体の中身を見てみましょう。
pub enum Data {
Struct(DataStruct),
Enum(DataEnum),
Union(DataUnion),
}
syn::DataStruct
はこうなっています。fields
がフィールドの情報を持っています。
pub struct DataStruct {
pub struct_token: Struct,
pub fields: Fields,
pub semi_token: Option<Semi>,
}
Fields
は以下のような enum です。Named
が名前のついたフィールドです。今回は Unit 構造体やタプル構造体はサポートしないのでNamed
だけ見てみましょう。
pub enum Fields {
Named(FieldsNamed),
Unnamed(FieldsUnnamed),
Unit,
}
FieldsNamed
はフィールド情報を持っているField
構造体を複数保持しており、named.iter()
でField
構造体のインスタンスをイテレートできます。
pub struct FieldsNamed {
pub brace_token: Brace,
pub named: Punctuated<Field, Comma>,
}
syn::Field
構造体はこうなっています。ident
がフィールド名でty
が型の情報を持っています。ようやく必要な情報までたどり着きました。
pub struct Field {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub ident: Option<Ident>,
pub colon_token: Option<Colon>,
pub ty: Type,
}
パターンマッチを使って必要な情報だけを取り出します。サポートしないケースについては今はpanic!
させておきましょう(後のステップでちゃんとコンパイルエラーが出るようにします)。この後で使いやすいようにident
とty
はVec
に格納しておきます。
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"),
};
さて、必要なものはすべて準備できました。これらを使ってマクロの出力として返すトークン列を生成しましょう。
この前のステップでquote!
マクロについて
Rust のコードと(ほぼ)同じ構文で記述することができ
と説明しましたが、quote!
マクロの内部では特殊な記法を使うことができます。たとえば、quote::ToTokens
トレイトを実装する変数にquote!
マクロの中で#
をつけるとその変数の内容が挿入されます。これを使って適切な名前や型のフィールドを持った構造体を動的に生成します。今までに出てきたsyn::Ident
などの構造体はquote::ToTokens
トレイトを実装しているので特に気にせず使うことができます。
これを使ってCommandBuilder
構造体の構造体名を書き直すと以下のようになります。
quote!{
#vis struct #builder_name {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<Vec<String>>,
}
}
また、quote!
マクロの中では#(...)*
のような形でIntoIterator
を実装した型の変数を繰り返して挿入できます。これを使ってCommandBuilder
構造体のフィールド名も書き換えると以下のようになります。idents
とtypes
はそれぞれフィールド名と型を格納したベクタですので、以下のようにしてベクタの各値について繰り返し展開できます。)
と*
の間に文字を記述するとその文字で区切って展開してくれるので、以下では#(...),*
のようにして構造体のフィールドをベクタから生成しています。
quote!{
#vis struct #builder_name {
#(#idents: Option<#types>),*
}
}
さて、これでハードコードされた構造体名やフィールド名などをすべて取り除くことができました。
同じようにbuilder
関数も書き換えると以下のようになります。
quote! {
#vis struct #builder_name {
#(#idents: Option<#types>),*
}
impl #ident {
pub fn builder() -> #builder_name {
#builder_name {
#(#idents: None),*
}
}
}
}
以上をまとめると最終的なマクロの実装は以下のようになります。
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, 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 expand = quote! {
#vis struct #builder_name {
#(#idents: Option<#types>),*
}
impl #ident {
pub fn builder() -> #builder_name {
#builder_name {
#(#idents: None),*
}
}
}
};
proc_macro::TokenStream::from(expand)
}
03-call-setters
目標
CommandBuilder
構造体の各フィールドに対する setter メソッドを作る
実装方針
具体的な実装から考えるとやりやすいです。Command
関数のexecutable
フィールドを例にとると setter は以下のようになるので、これを前のステップと同様、変数の展開や繰り返しを使って書き直しましょう。
pub fn executable(&mut self, executable: String) -> &mut Self {
self.executable = executable
self
}
実装
実装方針で書いた具体的なフィールドの実装を書き直しましょう。変数の展開や繰り返しの書き方はすでに説明した通りです。#(...)*
の括弧内には変数以外を入れることもできます。今回は関数の実装自体を#(...)*
で繰り返してやれば良いでしょう。
impl #builder_name {
#(pub fn #idents(&mut self, #idents: #types) -> &mut Self {
self.#idents = Some(#idents);
self
})*
}
最終的な実装は以下の通りです。
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, 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 expand = quote! {
#vis struct #builder_name {
#(#idents: Option<#types>),*
}
impl #builder_name {
#(pub fn #idents(&mut self, #idents: #types) -> &mut Self {
self.#idents = Some(#idents);
self
})*
}
impl #ident {
pub fn builder() -> #builder_name {
#builder_name {
#(#idents: None),*
}
}
}
};
proc_macro::TokenStream::from(expand)
}
04-call-build
目標
Command
構造体のインスタンスを返すbuild
メソッドをCommandBuilder
構造体に実装するCommandBuilder
の各フィールドがNone
の場合はエラーを返すようにする
実装方針
これも具体的な実装から考えるとわかりやすいです。Command
構造体の場合は以下のようになるので、これを変数の展開や繰り返しを使って書き直しましょう。
impl CommandBuilder {
...略...
pub fn build() -> Result<Command, Box<dyn Error>> {
if self.executable.is_none() {
return Err(...略...)
}
...略...
if self.current_dir.is_none() {
return Err(...略...)
}
Command {
executable: self.executable.clone().unwrap(),
args: self.args.clone().unwrap(),
env: self.env.clone().unwrap(),
current_dir: self.current_dir.clone().unwrap(),
}
}
}
実装
ガード節をまず作ります。このようにquote!
を使って部分的にトークン列を作って後で組み合わせることもできます。
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())
}
}
});
前のステップと同じようにしてbuild
関数を作ります。上で作ったガード節を#(#checks)*
で展開しています。
let expand = quote! {
...略...
pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> {
#(#checks)*
Ok(#ident {
#(#idents: self.#idents.clone().unwrap()),*
})
}
...略...
}
最終的な実装は以下の通りです。
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, 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 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())
}
}
});
let expand = quote! {
#vis struct #builder_name {
#(#idents: Option<#types>),*
}
impl #builder_name {
#(pub fn #idents(&mut self, #idents: #types) -> &mut Self {
self.#idents = Some(#idents);
self
})*
pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> {
#(#checks)*
Ok(#ident {
#(#idents: self.#idents.clone().unwrap()),*
})
}
}
impl #ident {
pub fn builder() -> #builder_name {
#builder_name {
#(#idents: None),*
}
}
}
};
proc_macro::TokenStream::from(expand)
}
05-method-chaining
目標
CommandBuilder
構造体でメソッドチェーンを使えるようにする
実装方針
03-call-setters ステップで実装した setter は&mut Self
を返すようになっているので、実はすでにメソッドチェーンを使えるようになっています。そのため、このステップでは追加の実装をする必要はありません。
実装
このステップでは追加の実装は必要ありません。このステップ用のテストを実行して変更なしでテストをパスすることをチェックしてみましょう。
まとめ
前編では Builder パターンを実現するのに最低限必要な機能を持ったマクロを作りました。
しかし、まだ Optional なフィールドの取り扱いに不十分な点があったり、ベクタ型のフィールドの取り扱いに改善の余地があったりします。後編ではそのあたりの機能を実装していきます。
後編へ続く。