Goのコード生成のためのテンプレートエンジン seyfert を書いてみた

こんにちは、この記事は[Go Advent Calendar 2016](http://qiita.com/advent-calendar/2016/go)の11日目の記事です。

## TL;DR

- Goのコード生成のためにSprintf的なやつとかtext/templateでGoのコードを書くのがキツイ
- 文法はGoとしてそのまま解釈できるようにして補完やシンタックスハイライトなどを効くようにし、一部の文字を入れ替えればいいのでは？　と思った
- [github.com/mackee/seyfert](https://github.com/mackee/seyfert)

## これまでのあらすじ

ちょっと前に[builderscon tokyo 2016](https://builderscon.io/builderscon/tokyo/2016)というカンファレンスで、[世の中の困り事はだいたいGoのコード自動生成で解決する](http://go-talks.appspot.com/github.com/mackee/sandbox/builderscon2016/talk.slide)というトークをしたのですが、その中で「**コード生成のために書くテンプレートがGoとしてエディタが解釈されないのでキツイ**」という話がありました。

Goのコード生成のために利用する手段としては

- プレースホルダを埋め込んだテキストをコードに埋め込んでSprintfなどで置換する
- `text/template`などのテンプレートエンジンを利用してGoのコードを組み立てる
- `go/ast.Node`を直接いじって`go/format.Node`で書き込む

というのが考えられます。
以前私は[github.com/mackee/go-sqlla](https://github.com/mackee/go-sqlla)というコード生成を用いたクエリビルダーで`text/template`を用いて書きましたが、テンプレート内では補完やシンタックスハイライトが効かないため、Go目パーサーが鍛えられていない状態では厳しいということがわかりました。
私がGoを書いていて楽しいと思う点の一つが、シンプルな型によって受けられる高速かつ安全な補完やコンパイル時型チェックです。
コード生成という特殊な状況であっても恩恵を受けたい！　そう思って物思いに耽るのでした。

## gorename

前から[gorename](https://godoc.org/golang.org/x/tools/cmd/gorename)というツールの存在は知ってはいましたが使っていませんでした。
だいたいエディタでシュッと変えてしまう事が多く……。

ですが、先日、[Okinawa.go Advent Calendar 2016](http://qiita.com/advent-calendar/2016/okinawago)で@tenntennさんが[gorenameをライブラリとして使う #golang](http://qiita.com/tenntenn/items/b1bb5eb4d372bd2d936f)という記事を投稿されているのを発見しました。

そこでひらめきました。

**structとかfuncの名前にプレースホルダっぽいやつを含めておいてgorenameで置換すればええんやないのか？**

## seyfertの使い方

以下のようなリクエストを受け取るAPIを考えてみます。パラメータの受け取り方はquery stringです。

```
http://example.com/hoge?fuga_id=1234&page=1
```

Go側の理想としては、

```go
type HogeRequest struct {
    FugaID int
    Page   int
}

func HogeHandler(w http.ResponseWriter, req HogeRequest) {
}
```

みたいなので受け取れると楽です。実際にこの形式で受け取れるフレームワークはいくつかありますが、`reflect`を使わざるを得ないので速度面でのデメリットを受けます。
これをコード生成で解決してみます。

以下のようなGoのコードっぽいテンプレートを書きます。

```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)
    })
}
```

そんでもって、このテンプレートを使うコードを書きましょう

```go
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](http://qiita.com/tenntenn/items/b1bb5eb4d372bd2d936f)内のコードを拝借してやっています。置換すべき定義を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が結構遅いのでちょっとイライラはしますね。

## 名前の由来

[セイファート銀河](https://ja.wikipedia.org/wiki/%E3%82%BB%E3%82%A4%E3%83%95%E3%82%A1%E3%83%BC%E3%83%88%E9%8A%80%E6%B2%B3)

## まとめ

サクッと書いてみましたが、よいアイディア！　一声かけてもらえればちゃんとしていく腹積もりです。sqllaのtext/template部分を置き換えるのがよさそうですが。あとテストとドキュメント（いつものやつだ
あとテンプレートみたいに関数内で分岐してレンダリングするものを変えるとかは出来ないのでどないしうようかなーとなっております。

不安なのが私が調査不足でもうやっている人がいるかも……というのがあるので似たようなのがアレば教えていただけると幸いです！

明日は、@tenntennさんです。