proc_macro_workshopでRustの手続き的マクロに入門する 後編
はじめに
前編では、setter
メソッドによる値の設定やbuild
メソッドによる構造体の生成などの基本的な機能を持った手続き的マクロを実装しました。後編では以下の機能を実装していきます。
- Optional な値を構造体のフィールドとして持てるようにする
- 以下の 2 つの方法で Vec 型のフィールドを更新できるようにする
- ベクタを与えて一括で更新する
- ベクタの要素を与えて 1 つずつフィールドに要素を追加する
- コンパイルエラーが発生した際にわかりやすいメッセージを表示する
builder マクロを作る(続き)
06-optional-field
目標
- 構造体のフィールドとして Optional な値を持てるようにする
- Optional なフィールドには値が入っていなくても
build
メソッドで構造体を生成できる - Optional でないフィールドは値が入っていないと
build
メソッドで構造体を生成できない - Optional なフィールドは
Some
でラップせずに中身の値をそのまま使って初期化できる
- Optional なフィールドには値が入っていなくても
最後の項目についてですが、たとえば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_option
はis_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_fields
はbuild
関数の中で展開します。
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_option
とunwrap_option
の実装
is_option
とunwrap_option
を実装していきます。まずはis_option
関数から実装していきましょう。
is_option
関数
is_option
はType
型を受け取ってそれがOption
かどうかを判定する関数なのでシグネチャは以下のようにすれば良さそうです。
fn is_option(ty: &Type) -> bool
ty
がOption
かどうか判定するのに使えそうな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
はどのバリアントに分類されるのでしょうか。ドキュメントを一見してもそれらしいものは見当たりませんが、Option
はType::Path(syn::TypePath)
に分類されます。TypePath
はstd::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
かそれ以外かを判定すれば良いでしょうか。
先ほども説明したように、TypePath
はstd::iter::Iter
のようにコロン 2 つで分割されたセグメントの集合でした。つまり、セグメントの集合の最後の要素がOption
であるかどうかを判断すれば良さそうです。
では、どのようにしてセグメントの集合の最後の要素を取得すれば良いのでしょうか。syn::TypePath
は以下のような構造体で、path
に Path
の情報を保持しています。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),
}
PathArguments
は enum のようです。バリアント名を眺めてみるとAngleBracketed
というものがあります。これが関係ありそうです。実際、ドキュメントのAngleBracketed
の項目には以下のように記載されており、このバリアントがジェネリック引数の情報を持っていることがわかります。
AngleBracketed(AngleBracketedGenericArguments)
The
<'a, T>
instd::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_option
とunwrap_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>,
}
実装方針
目標とする機能を実現するために実装する必要があるのは以下の項目です。
- フィールドに付与されたアトリビュートを取得する
builder
アトリビュートを付与できるようにする- 要素を 1 つずつ追加する関数名は
build
アトリビュートのeach
キーに指定する - アトリビュートのキー名(
each
)のバリデーションは次のステップで実装する
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_vector
とunwrap_vector
の実装
ここからはis_vector
とunwrap_vector
を実装していきます。ここまでの実装ではunwrap_vector
はでてきませんが、今後使うのでここで実装しておきます。
Vec
もOption
と同様Type::Path
に分類されるので、以下の項目は 06 で実装したis_option
やunwrap_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,
}
DeriveInput
はattrs
フィールドを持っていますが、以下のDeriveInput
のattrs
の説明にあるように、これは構造体自体に付与されたアトリビュートです。(引用元)
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
関数の最初の方で取得しているので、これを処理してアトリビュートを取得していきます。attrs
はAttribute
のベクタになっていますが、今回は 1 つのアトリビュートしか使わないのでfirst
で先頭のアトリビュートだけ取得すれば良いでしょう。
let ident_each_name = field
.attrs
.first()
.map(|attr| todo!());
Attribute
のparse_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),
}
Meta
のList
バリアントの説明に
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"
を例にとると、MetaNameValue
はpath
にeach
を、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::new
でIdent
を生成する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::Error
のto_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();
アトリビュートのキーはMetadataNameValue
のpath
に格納されています。path
はPath
型なので 06-optional-field で処理したのと同様にして識別子を取得します。Ident
のto_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::Error
はsyn::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::Error
のto_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
でインポートされる型(Option
やVector
)などがユーザーによって再定義されても正しく使えるようにする
実装方針
Option
やVec
などを名前空間を指定して使うようにすればよいです。テストコードでチェックされているのは以下の 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
を付与した場合などを考慮しておらず、実装した機能は十分ではありません。テストも十分なケースを網羅しているとは言えません。
しかし今回の記事で手続き的マクロをどのようにして作るかは一通り理解でき、今後やる時もどこから手をつければよいかだいたい感覚がつかめたかと思います。この記事が今後みなさんがマクロを実装するときの助けになれば幸いです。