davidpdrsn/juniper-eager-loading: Library for avoiding N+1 query bugs with Juniper
↑の実装を読んでいて、CustomDeriveってこうやって書くのかぁ、と思ったので、proc_macroでCustomDriveを書いてみる為にちょっと試したので、メモ。
環境
- Rust 1.41
構造体の名前を返すメソッドを定義するCustomDeriveを書いてみる
シンプルな例にする為に構造体名を文字列で返すメソッドを定義するCustomDriveを書いてみます。
[package]
name = "proc_macro_sample"
version = "0.1.0"
authors = ["yagince <xxxx@gmail.com>"]
edition = "2018"
publish = false
[lib]
proc-macro = true
[dependencies]
quote = "1.0.2"
syn = { version = "1.0.14", features = ["full", "extra-traits"] }
test
先にどんな感じになって欲しいかテストを書いてみます。
use proc_macro_sample::SelfName;
#[derive(SelfName)]
struct Hoge {}
#[test]
fn test_hoge() {
let hoge = Hoge {};
assert_eq!(hoge.self_name(), "Hoge");
}
derive(SelfName)
すると self_name
というメソッドが定義されるのを目指します。
実装
extern crate proc_macro;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};
#[proc_macro_derive(SelfName)]
pub fn derive_self_name(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(input as ItemStruct);
let struct_name = item.ident;
let gen = quote! {
impl #struct_name {
pub fn self_name(&self) -> &str {
stringify!(#struct_name)
}
}
};
gen.into()
}
-
proc_macro_derive(SelfName)
でderive(SelfName)
できるようにする - synのparse_macro_inputマクロで
syn::ItemStruct
にパースする- TokenStreamそのままだと直感的に扱えないので、もうちょい扱いやすい状態に変換している
- quoteのquoteマクロで定義したいメソッドを書く
ちなみに、TokenStreamとItemStructの状態はこんな状態。
[src/lib.rs:7] &input = TokenStream [
Ident {
ident: "struct",
span: #0 bytes(79..85),
},
Ident {
ident: "Hoge",
span: #0 bytes(86..90),
},
Group {
delimiter: Brace,
stream: TokenStream [],
span: #0 bytes(91..93),
},
]
[src/lib.rs:9] &item = ItemStruct {
attrs: [],
vis: Inherited,
struct_token: Struct,
ident: Ident {
ident: "Hoge",
span: #0 bytes(86..90),
},
generics: Generics {
lt_token: None,
params: [],
gt_token: None,
where_clause: None,
},
fields: Named(
FieldsNamed {
brace_token: Brace,
named: [],
},
),
semi_token: None,
}
構造体名はidentで取得できるし、fieldsからフィールド定義が取れるので、とても扱いやすい状態になっているように見えます。
オプションを設定できるようにしたい
何かしら設定を受け取れるようにしたいので、今回はシンプルにlowercaseにするオプションを設定出来る状態を目指すことにしました。
一旦設定を受け取ってみる
...
#[proc_macro_derive(SelfName, attributes(self_name))]
pub fn derive_self_name(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(input as ItemStruct);
let struct_name = dbg!(item).ident;
let gen = quote! {
impl #struct_name {
pub fn self_name(&self) -> &str {
stringify!(#struct_name)
}
}
};
gen.into()
}
attributes
で self_name
というオプションを受け取れるようにしてみた
test
#[derive(SelfName)]
#[self_name(lowercase)]
struct HogeLowercase {}
#[test]
fn test_hoge_lowercase() {
let hoge = HogeLowercase {};
assert_eq!(hoge.self_name(), "hogelowercase");
}
[src/lib.rs:17] item = ItemStruct {
attrs: [
Attribute {
pound_token: Pound,
style: Outer,
bracket_token: Bracket,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "self_name",
span: #0 bytes(186..195),
},
arguments: None,
},
],
},
tokens: TokenStream [
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
ident: "lowercase",
span: #0 bytes(196..205),
},
],
span: #0 bytes(195..206),
},
],
},
],
vis: Inherited,
struct_token: Struct,
ident: Ident {
ident: "HogeLowercase",
span: #0 bytes(215..228),
},
generics: Generics {
lt_token: None,
params: [],
gt_token: None,
where_clause: None,
},
fields: Named(
FieldsNamed {
brace_token: Brace,
named: [],
},
),
semi_token: None,
}
attrs
というフィールドにAttribute型のデータが入っていて
path
にattribute名
tokens
にattributeの中身が入っています
このままだとちょっと扱いづらいですね。
Attributeを構造体にマッピングしたい
davidpdrsn/juniper-eager-loadingでは
https://github.com/davidpdrsn/juniper-eager-loading/blob/590b93ee4d/juniper-eager-loading-code-gen/src/derive_eager_loading/field_args.rs#L18-L27
bae::FromAttributes
というのを使っているみたいですね。
bae - crates.io: Rust Package Registry ※同じ作者
juniper-eager-loading/derive_eager_loading.rs at 590b93ee4d0d7fe629b939bfc3f051458a98149d · davidpdrsn/juniper-eager-loading
EagarLoading::from_attributes(&attrs)
で構造体にマッピングしているようです。
bae is a crate for proc macro authors, which simplifies parsing of attributes. It is heavily inspired by darling but has a significantly simpler API.
inspired by darling
って書いてあるので darling
も見てみます。
darling - crates.io: Rust Package Registry
https://github.com/TedDriggs/darling/blob/57c4ef486c/examples/fallible_read.rs
FromDeriveInput
と Xx::from_derive_input
で行けそうです。
今回は darling
を使ってみることにしました。
Attributeを構造体にマッピングしてみる
darling::FromDeriveInput - Rust
from_derive_input
は syn::DeriveInput
型を引数に取ります。
今は ItemStruct
にパースしてるので、そのままだと渡せません。
なので、パースする型を DeriveInput
に変えてみました。
#[proc_macro_derive(SelfName, attributes(self_name))]
pub fn derive_self_name(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(input as DeriveInput);
let struct_name = dbg!(item).ident;
let gen = quote! {
impl #struct_name {
pub fn self_name(&self) -> &str {
stringify!(#struct_name)
}
}
};
gen.into()
}
[src/lib.rs:17] item = DeriveInput {
attrs: [
Attribute {
pound_token: Pound,
style: Outer,
bracket_token: Bracket,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "self_name",
span: #0 bytes(186..195),
},
arguments: None,
},
],
},
tokens: TokenStream [
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
ident: "lowercase",
span: #0 bytes(196..205),
},
],
span: #0 bytes(195..206),
},
],
},
],
vis: Inherited,
ident: Ident {
ident: "HogeLowercase",
span: #0 bytes(215..228),
},
generics: Generics {
lt_token: None,
params: [],
gt_token: None,
where_clause: None,
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Named(
FieldsNamed {
brace_token: Brace,
named: [],
},
),
semi_token: None,
},
),
}
あんまり変わってないのでDeriveInputで行けそうです。
構造体にマッピング
lowercase
というオプションを受け取りたいので、以下のように構造体を定義。
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(self_name))]
struct SelfNameOption {
#[darling(default)]
lowercase: bool,
}
-
self_name
というattributeで受け取る - lowercaseオプションはdefaultでfalseで構わないので、
darling(default)
でdefaultが設定されるように指定
やってみます
let item = parse_macro_input!(input as DeriveInput);
let option = SelfNameOption::from_derive_input(&item).unwrap();
use proc_macro_sample::SelfName;
#[derive(SelfName)]
struct Hoge {}
#[test]
fn test_hoge() {
let hoge = Hoge {};
assert_eq!(hoge.self_name(), "Hoge");
}
#[derive(SelfName)]
#[self_name(lowercase)]
struct HogeLowercase {}
#[test]
fn test_hoge_lowercase() {
let hoge = HogeLowercase {};
assert_eq!(hoge.self_name(), "hogelowercase");
}
[src/lib.rs:21] option = SelfNameOption {
lowercase: false,
}
[src/lib.rs:21] option = SelfNameOption {
lowercase: true,
}
オプション無しの場合は false
オプションありの場合は true
になりました。
最終形がこちら
extern crate proc_macro;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use darling::{FromDeriveInput};
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(self_name))]
struct SelfNameOption {
#[darling(default)]
lowercase: bool,
}
#[proc_macro_derive(SelfName, attributes(self_name))]
pub fn derive_self_name(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(input as DeriveInput);
let option = SelfNameOption::from_derive_input(&item).unwrap();
let struct_name = item.ident;
let mut name_str = struct_name.to_string();
if option.lowercase {
name_str = name_str.to_lowercase();
}
let gen = quote! {
impl #struct_name {
pub fn self_name(&self) -> &str {
#name_str
}
}
};
gen.into()
}
running 2 tests
test test_hoge ... ok
test test_hoge_lowercase ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
まとめ
-
proc_macro_derive
でCustomDeriveは定義できる - TokenStreamのままだと扱いづらいので、synや周辺ライブラリで扱いやすい形にパースすると良い
- quoteで直感的にTokenStreamを生成できる
TODO
-
proc-macro2 - crates.io: Rust Package Registryに触れてないので、触れたい
- proc_macroをより便利にするようなcrateなのか?
- まだ良くわかっていないので、次回チャレンジ