こんにちは、この記事はGo Advent Calendar 2016の11日目の記事です。
TL;DR
- Goのコード生成のためにSprintf的なやつとかtext/templateでGoのコードを書くのがキツイ
- 文法はGoとしてそのまま解釈できるようにして補完やシンタックスハイライトなどを効くようにし、一部の文字を入れ替えればいいのでは? と思った
- github.com/mackee/seyfert
これまでのあらすじ
ちょっと前にbuilderscon tokyo 2016というカンファレンスで、世の中の困り事はだいたいGoのコード自動生成で解決するというトークをしたのですが、その中で「コード生成のために書くテンプレートがGoとしてエディタが解釈されないのでキツイ」という話がありました。
Goのコード生成のために利用する手段としては
- プレースホルダを埋め込んだテキストをコードに埋め込んでSprintfなどで置換する
-
text/template
などのテンプレートエンジンを利用してGoのコードを組み立てる -
go/ast.Node
を直接いじってgo/format.Node
で書き込む
というのが考えられます。
以前私はgithub.com/mackee/go-sqllaというコード生成を用いたクエリビルダーでtext/template
を用いて書きましたが、テンプレート内では補完やシンタックスハイライトが効かないため、Go目パーサーが鍛えられていない状態では厳しいということがわかりました。
私がGoを書いていて楽しいと思う点の一つが、シンプルな型によって受けられる高速かつ安全な補完やコンパイル時型チェックです。
コード生成という特殊な状況であっても恩恵を受けたい! そう思って物思いに耽るのでした。
gorename
前からgorenameというツールの存在は知ってはいましたが使っていませんでした。
だいたいエディタでシュッと変えてしまう事が多く……。
ですが、先日、Okinawa.go Advent Calendar 2016で@tenntennさんがgorenameをライブラリとして使う #golangという記事を投稿されているのを発見しました。
そこでひらめきました。
structとかfuncの名前にプレースホルダっぽいやつを含めておいてgorenameで置換すればええんやないのか?
seyfertの使い方
以下のようなリクエストを受け取るAPIを考えてみます。パラメータの受け取り方はquery stringです。
http://example.com/hoge?fuga_id=1234&page=1
Go側の理想としては、
type HogeRequest struct {
FugaID int
Page int
}
func HogeHandler(w http.ResponseWriter, req HogeRequest) {
}
みたいなので受け取れると楽です。実際にこの形式で受け取れるフレームワークはいくつかありますが、reflect
を使わざるを得ないので速度面でのデメリットを受けます。
これをコード生成で解決してみます。
以下のようなGoのコードっぽいテンプレートを書きます。
package tmpl
import (
"net/http"
)
//+seyfert
type T_Request struct {
//+expand RequestFields
}
//+seyfert
type T_Handler func (w http.ResponseWriter, req T_Request)
//+seyfert
func Register_T_Handler(h T_Handler) {
http.HandleFunc("_PATH_", func (w http.ResponseWriter, r *http.Request) {
var req T_Request
// 型変換とかやってurl.Valuesからreqに値を詰める
h(w, req)
})
}
そんでもって、このテンプレートを使うコードを書きましょう
package main
import (
"github.com/mackee/seyfert"
)
func main() {
var tmpl []byte
// tmplに上のコードを読んだやつを詰め込んでおく
// _T_とかにバインドする値
// +seyfert アノテーションのついてるstructと関数と関数内の文字列リテラルで展開される
binds := seyfert.Binds{
"T": "Hoge",
"PATH": "/hoge",
}
// structのfieldの定義 expandで展開される
fieldsSet := seyfert.FieldsSet{
"RequestFields": seyfert.Fields{
seyfert.Field{
Name: "HogeID",
Type: "int",
},
seyfert.Field{
Name: "Page",
Type: "int",
},
},
}
seyfert.Render(tmpl, "./parser/hoge.gen.go", binds, fieldsSet, "main")
}
これでいい感じのやつが生成されて、関数を書いてregisterHogeHandler
をmainあたりで読んで渡すだけで型付きの状態でリクエストがやってきて便利です。
レスポンスまで含めた生成を https://github.com/mackee/seyfert/tree/master/_example/genreqparser でやっています。
seyfertの機能
3つやっていることがあります
- gorenameをつかって
_T_
みたいなのを指定されたものに置換する -
//+expand ナントカ
とやったらfieldsSet
から指定されたFields
定義を抜いてきて展開する - 文字列リテラルを探し出して置換する
gorenameで置換
これは上述したgorenameをライブラリとして使う #golang内のコードを拝借してやっています。置換すべき定義をannotationから絞って定義内の型名をbinds
で置換してみて置換されたらpositionなどを取得するという形ですね。
単純にastをいじって置換も出来るのですが、gorenameは定義だけではなく参照している部分に関しても置換をしてもらえます。なのでメソッドのレシーバーや引数で使っている場合でもちゃんと追従します。
置換ルールとしては
-
<BIND>_***
のように先頭にバインドするキーがありアンダースコアで区切られている場合に置換する -
***_<BIND>_***
のように途中にアンダースコアで囲まれている場合に置換する
という動作になっています。由来としてはGoは先頭にアンダースコアあるのgo vetで怒られそうと思ったからですが、別に今となってはもういいかなとなりかけていますね。ちなみに頭に_<BIND>_***
でもたぶん置換されます。
expandでstruct field展開
gorenameで置換だけでいいかなと思ったんですが、fieldもこっちで埋め込めないと実用的ではないなと気づいて作りました。
コメントのポジションをgo/ast.CommentMap
を用いて絞ってさらに//+expand
がprefixにあるものを探し出しています。
コメントはファイル内のポジションが取れるのですが、これをast上で置換するのはかなり骨が折れそうだったので、一旦ファイルに書き込んだやつを[]byte
にして素朴に展開するフィールドを埋め込んでいってます。
文字列リテラルを探し出して置換する
要らないかなと思ったんですが、コントローラ生成器を作る場合、IDLで定義したパスとか埋め込みたいなとなったので作りました。
これはgo/ast.Inspect
で//+seyfert
アノテーションの付いたfuncのbodyの中の、go/ast.BasicLit
を探し出してその中で置換を試みています。これはトークンをそのまま入れ替えるだけなので、そこ場で入れ替えてしまってgo/format.Node
で書き戻しています。
go/ast.BasicLit
は他にも数字なんかもやってきますが、_
で囲まれた物になるので文字列しか対象にならなさそうです。
というわけで
Proof of Conceptだと割り切って雑に三回書き込んでいます。あとgorenameが結構遅いのでちょっとイライラはしますね。
名前の由来
まとめ
サクッと書いてみましたが、よいアイディア! 一声かけてもらえればちゃんとしていく腹積もりです。sqllaのtext/template部分を置き換えるのがよさそうですが。あとテストとドキュメント(いつものやつだ
あとテンプレートみたいに関数内で分岐してレンダリングするものを変えるとかは出来ないのでどないしうようかなーとなっております。
不安なのが私が調査不足でもうやっている人がいるかも……というのがあるので似たようなのがアレば教えていただけると幸いです!
明日は、@tenntennさんです。