はじめに 前編 では、 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_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> 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_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 を付与した場合などを考慮しておらず、実装した機能は十分ではありません。テストも十分なケースを網羅しているとは言えません。 しかし今回の記事で手続き的マクロをどのようにして作るかは一通り理解でき、今後やる時もどこから手をつければよいかだいたい感覚がつかめたかと思います。この記事が今後みなさんがマクロを実装するときの助けになれば幸いです。 参考文献 syn – Docs.rs The post proc_macro_workshopでRustの手続き的マクロに入門する 後編 appeared first on CADDi Tech Blog .