1. Qiita
  2. Items
  3. Go

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

  • 11
    Like
  • 0
    Comment

こんにちは、この記事は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さんです。

This post is the No.11 article of Go Advent Calendar 2016
Comments Loading...