自分用のj2cliをkamidanaという名前で作りはじめた

github.com

何でj2cliを使わないの?

何でj2cliを使わないのかというと、以下の様な理由。

  • j2cliのforkがいっぱいあってカオス
  • (一番star数が多いforkは)python3.xに対応していない
  • おもったよりも機能が多くない
  • (正直そんなに良いコードに見えない)
  • (init.pyで import pkg_resources とかつらい)

そんなわけで自分用の物を作り始めた。

何でkamidanaなの?

jinja2は神社なので、もう少し手軽なお社なら、家のどこかに存在する(事もある)神棚かなーと。

どういうことしたいの?

設定ファイル中の値をループして何かしたいということがけっこうあった。例えば以前のswagger specからgoのmodelを生成するスクリプトのswagger2goがあるとして。生成先のファイルがたくさんある場合に一度に生成できるようにしたい。例えばMakefileを書くなどでも良いのだけれど。そのMakefileを書くのが面倒くさい。

以下のような設定ファイルは存在するとする。

apps:
  fooAppX:
    port: 8000
  barAppX:
    port: 8001
  booAppX:
    port: 8002
  beeAppY:
    port: 8003

この設定ファイルから以下のようなMakefileを生成したい。

genFoo:
  swagger2go swagger/foo_app_x.yaml --package github.com/podhmo/foo/model --ref="#/definitions/fooAppData" --file ${GOPATH}/src/github.com/podhmo/model/gen_x_foo.go

genBar:
  swagger2go swagger/bar_app_x.yaml --package github.com/podhmo/foo/model --ref="#/definitions/barAppData" --file ${GOPATH}/src/github.com/podhmo/model/gen_x_bar.go

genBoo:
  swagger2go swagger/boo_app_x.yaml --package github.com/podhmo/foo/model --ref="#/definitions/booAppData" --file ${GOPATH}/src/github.com/podhmo/model/gen_x_boo.go

正確にいうと末尾がXで終わっているものだけをコード生成の対象にしたい。末尾がyで終わっているものはコード生成の対象から除外したい。

設定ファイルの名前をあれこれいじれば生成できるもののこれをただのpythonスクリプトにしてしまうとだるさが一気に増してしまう。 これをどうにか綺麗なjinja2テンプレートを使って生成したい。

どういう風に使うの?

コマンドラインでkamidanaを使う。 --data には設定ファイル を --additionals には追加したい述語(test)や出力形式(filter)を定義したファイルを渡せる。そんなわけで以下の様なjinja2テンプレートで十分になる。

Makefile.jinja2

{% for name in apps.keys() %}{% if name is x %}
gen{{name|prefix}}:
    swagger2go {{name|swagger_path}} --package github.com/podhmo/foo/model --ref="{{name|swagger_ref}}" --file {{name|model_path}}
{% endif %}{% endfor %}

まだまだ読める範囲なのではという感じ。実際以下の様な形で使う。

$ kamidana Makefile.jinja2 --data apps.yaml --additionals=additionals.py

genFoo:
    swagger2go swagger/foo_app_x.yaml --package github.com/podhmo/foo/model --ref="#/definitions/fooAppData" --file ${GOPATH}/src/github.com/podhmo/model/gen_x_foo.go

genBar:
    swagger2go swagger/bar_app_x.yaml --package github.com/podhmo/foo/model --ref="#/definitions/barAppData" --file ${GOPATH}/src/github.com/podhmo/model/gen_x_bar.go

genBoo:
    swagger2go swagger/boo_app_x.yaml --package github.com/podhmo/foo/model --ref="#/definitions/booAppData" --file ${GOPATH}/src/github.com/podhmo/model/gen_x_boo.go

ここでそれぞれ渡したファイルは以下の様な感じ。

apps.yaml(再掲)

apps:
  fooAppX:
    port: 8000
  barAppX:
    port: 8001
  booAppX:
    port: 8002
  beeAppY:
    port: 8003

kamidana.as_filterkamidana.as_test のデコレータがついた関数が自動的にテンプレート上で使えるようになる。

additionals.py

from kamidana import as_filter, as_test
import re


@as_filter
def prefix(v):
    return titleize(snakecase(v).split("_", 1)[0])


@as_filter
def swagger_path(v):
    return "swagger/{}.yaml".format(snakecase(v))


@as_filter
def swagger_ref(v):
    name = snakecase(v).rsplit("_", 1)[0]
    return "#/definitions/{}Data".format(camelcase(name))


@as_filter
def model_path(v):
    xs = snakecase(v).split("_")
    tag = xs[-1]
    name = xs[0]
    return "${{GOPATH}}/src/github.com/podhmo/model/gen_{}_{}.go".format(tag, name)


@as_test
def x(v):
    return "x" == snakecase(v).rsplit("_", 1)[1].lower()


# このあたりのコードはどこかライブラリに持っておきたい
def snakecase(
    name, rx0=re.compile('(.)([A-Z][a-z]+)'), rx1=re.compile('([a-z0-9])([A-Z])'), separator="_"
):
    pattern = r'\1{}\2'.format(separator)
    return rx1.sub(pattern, rx0.sub(pattern, name)).lower()


def camelcase(name):
    return untitleize(pascalcase(name))


def pascalcase(name, rx=re.compile("[\-_ ]")):
    return "".join(titleize(x) for x in rx.split(name))


def titleize(name):
    if not name:
        return name
    name = str(name)
    return "{}{}".format(name[0].upper(), name[1:])


def untitleize(name):
    if not name:
        return name
    return "{}{}".format(name[0].lower(), name[1:])

全然関係ないけれど、上で使っているcamelcaseやkebabcaseなどに変換する関数群をどこに置こうか迷ったりしている(zenmaiでも使いたいし。kamidanaでも使いたい。でも両者はわりと独立しているような気がする。一方で新しいパッケージは作りたくない)。

もうちょっと複雑なpythonでgoのコードを生成する例(わりとよい)

github.com

もうちょっと複雑なpythonでgoのコードを生成する例を紹介して見ることにした。具体的にはswagger specを見てgoのstructを生成する処理。 go-swaggerなどswagger specからgoのコードを生成するツールは既にあったりはするのだけれど。これと似たようなことを自分でもやってみる(正確にはswagger specに愚直に従ったコードを生成するとほとんどがpointerになってしまうという問題があり。それを避けたコードを生成したいという気持ちがあった)。

swagger2go

swagger2goというコマンドを作ってみた。

これは、例えば、以下の様なswagger specから

definitions:
  person:
    description: ヒト
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
swagger2go src/00*.yaml --package=github.com/podhmo/person --position=./dst --file=person.go --ref "#/definitions/person"
goimports -w dst/person/person.go

以下の様なgoのコードが生成される。

package person

// Person : ヒト
type Person struct {
    Name string `json:"name"`
    Age  int64  `json:"age"`
}

もう少し複雑な構造

もう少し複雑な構造としてarrayの定義とstructの参照をしてみる。例えば以下のようなyamlから。

definitions:
  people:
    type: array
    items:
      $ref: "#/definitions/person"
  person:
    description: ヒト
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
  group:
    type: object
    properties:
      name:
        type: string
      members:
        $ref: "#/definitions/people"

(--ref を外すと全てが出力される)

swagger2go src/01*.yaml --package=github.com/podhmo/person --position=./dst/01 --file=person.go

以下のようなgoのコードが生成される。

package person

// People :
type People []Person

// Person : ヒト
type Person struct {
    Name string `json:"name"`
    Age  int64  `json:"age"`
}

// Group :
type Group struct {
    Name    string `json:"name"`
    Members People `json:"members"`
}

出力されるfieldにpointerを含めてみる

pointerの扱いは明示的に行うようにした。ただ自身への参照はdefaultでpointer扱いになっている。おせっかいかも知れない。defaultは値でx-go-pointer をつけるとpointer扱いになる(このあたりはswaggerへの完全な追随を止めている)。

definitions:
  info:
    type: object
    properties:
      description:
        type: string
  person:
    description: ヒト
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
      father:
        $ref: "#/definitions/person"
      mother:
        $ref: "#/definitions/person"
      info:
        $ref: "#/definitions/info"
      info2:
        x-go-pointer: true
        $ref: "#/definitions/info"

こういう感じ(面倒になったので、もはや生成用のコマンドは併記しない)。

package person

// Info :
type Info struct {
    Description string `json:"description"`
}

// Person : ヒト
type Person struct {
    Name   string  `json:"name"`
    Age    int64   `json:"age"`
    Father *Person `json:"father"`
    Mother *Person `json:"mother"`
    Info   Info    `json:"info"`
    Info2  Info    `json:"info2"`
}

別の型を参照するように変える

既に定義されている別の型を参照したい場合があるかもしれない。例えば上の例のInfoが既に定義されているというような状態。これは x-go-type を指定してあげれば良いことにした。

diff -u 02* 03*
--- 02person.yaml 2017-05-10 05:43:42.000000000 +0900
+++ 03person.yaml 2017-05-10 05:49:02.000000000 +0900
@@ -1,5 +1,6 @@
 definitions:
   info:
+    x-go-type: "github.com/podhmo/person.Info"
     type: object
     properties:
       description:

Infoは既に手動で定義されている場合など。

package person

// Person : ヒト
type Person struct {
    Name   string  `json:"name"`
    Age    int64   `json:"age"`
    Father *Person `json:"father"`
    Mother *Person `json:"mother"`
    Info   Info    `json:"info"`
    Info2  Info    `json:"info2"`
}

ところで、yaml自身の定義を省略して書いちゃっても良い(swagger specとしてはinvalid)。

definitions:
  person:
    description: ヒト
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
      father:
        $ref: "#/definitions/person"
      mother:
        $ref: "#/definitions/person"
      info:
        x-go-type: "github.com/podhmo/person.Info"
        type: object
      info2:
        x-go-type: "github.com/podhmo/person.Info"
        x-go-pointer: true
        type: object

異なるpackageのものを参照する場合

異なるpackageのものを参照するようにしても上手くやってくれる。このあたりの対応もほぼほぼ自動なのでgoawayを作ってよかったという感じ。例えば、time.Timeを追加してみたり。上の例のInfoを別のpackageにしてみる。

definitions:
  person:
    description: ヒト
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
      birth:
        x-go-type: "time.Time"
      father:
        $ref: "#/definitions/person"
      mother:
        $ref: "#/definitions/person"
      info:
        x-go-type: "github.com/podhmo/message.Info"
        type: object
      info2:
        x-go-type: "github.com/podhmo/message.Info"
        x-go-pointer: true

BirthとInfoの型にはprefixがついている。やりましたね。import部分も自動で挿入されてはいるのだけれど。このあたりはフォーマットにgoimportsを使ってしまえばすむ話しではあるかもしれない。

package person

import (
    "time"

    "github.com/podhmo/message"
)

// Person : ヒト
type Person struct {
    Name   string       `json:"name"`
    Age    int64        `json:"age"`
    Birth  time.Time    `json:"birth"`
    Father *Person      `json:"father"`
    Mother *Person      `json:"mother"`
    Info   message.Info `json:"info"`
    Info2  message.Info `json:"info2"`
}

ついでにenumも出力可能に

ついでにenumも出力可能に。genderというenumを追加。

definitions:
  gender:
    type: string
    enum:
      - notKnown
      - male
      - female
      - notApplicable
  people:
    type: array
    items:
      $ref: "#/definitions/person"
  person:
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
      gender:
        $ref: "#/definitions/gender"
      father:
        $ref: "#/definitions/person"
      mother:
        $ref: "#/definitions/person"

はい。

package person

import (
    "fmt"
)

// People :
type People []Person

// Gender :
type Gender string

const (
    // GenderNotKnown :
    GenderNotKnown = Gender("notKnown")
    // GenderMale :
    GenderMale = Gender("male")
    // GenderFemale :
    GenderFemale = Gender("female")
    // GenderNotApplicable :
    GenderNotApplicable = Gender("notApplicable")
)

// String : stringer implementation
func (g Gender) String() string {
    switch g {
    case GenderNotKnown:
        return "notKnown"
    case GenderMale:
        return "male"
    case GenderFemale:
        return "female"
    case GenderNotApplicable:
        return "notApplicable"
    default:
        panic(fmt.Sprintf("unexpected Gender %s, in string()", string(g)))
    }

}

// ParseGender : parse
func ParseGender(g string) Gender {
    switch g {
    case "notKnown":
        return GenderNotKnown
    case "male":
        return GenderMale
    case "female":
        return GenderFemale
    case "notApplicable":
        return GenderNotApplicable
    default:
        panic(fmt.Sprintf("unexpected Gender %v, in parse()", g))
    }

}

// Person :
type Person struct {
    Name   string  `json:"name"`
    Age    int64   `json:"age"`
    Gender Gender  `json:"gender"`
    Father *Person `json:"father"`
    Mother *Person `json:"mother"`
}

enumの出力結果を変えてみる

ちょっとここまで気合のは言ったコードは不要かもしれない。enumの出力結果を変えてみる。 ここはちょっとトリッキーなのだけれど。利用するwriterのクラスを変えてあげることができる。以下の様なコードを書く。元のコードの一部を使わないようなクラスを使うように変える。

# writer.py
from goaway.writer import Writer, EnumWriter


class TinyEnumWriter(EnumWriter):
    def write(self, enum, file, m):
        self.write_definition(enum, file, m)
        # self.write_string_method(enum, file, m)
        # self.write_parse_method(enum, file, m)
        return m


class MyWriter(Writer):
    enum_writer_factory = TinyEnumWriter

--writer オプションで定義したクラスを渡すようにする。相対パスでも渡せる

swagger2go --writer=./writer.py:MyWriter src/07*.yaml --package=github.com/podhmo/person --position=./dst/07 --file=person.go

出力が短くなった。

package person

// People :
type People []Person

// Gender :
type Gender string

const (
    // GenderNotKnown :
    GenderNotKnown = Gender("notKnown")
    // GenderMale :
    GenderMale = Gender("male")
    // GenderFemale :
    GenderFemale = Gender("female")
    // GenderNotApplicable :
    GenderNotApplicable = Gender("notApplicable")
)

// Person :
type Person struct {
    Name   string  `json:"name"`
    Age    int64   `json:"age"`
    Gender Gender  `json:"gender"`
    Father *Person `json:"father"`
    Mother *Person `json:"mother"`
}

一部のものだけ出力先のファイルを変えてみる

先のenumの例のように長ったらしいコードを生成する場合には出力先のファイルを変えたい事があるかもしれない。x-go-filename を付加すると出力先のファイル名を変えられる(同一パッケージに展開はされる)。

--- src/07person.yaml    2017-05-10 06:04:51.000000000 +0900
+++ src/08person.yaml 2017-05-10 06:08:52.000000000 +0900
@@ -1,5 +1,6 @@
 definitions:
   gender:
+    x-go-filename: "gender.go"
     type: string
     enum:
       - notKnown

person.goとgender.goが生成される。

swagger2go --writer=./writer.py:MyWriter src/08*.yaml --package=github.com/podhmo/person --position=./dst/08 --file=person.go
INFO:goaway.emitter:write: ./dst/08/person/person.go
INFO:goaway.emitter:write: ./dst/08/person/gender.go

こういう感じに分かれる。

person.go

package person

// People :
type People []Person

// Person :
type Person struct {
    Name   string  `json:"name"`
    Age    int64   `json:"age"`
    Gender Gender  `json:"gender"`
    Father *Person `json:"father"`
    Mother *Person `json:"mother"`
}

gender.go

package person

// Gender :
type Gender string

const (
    // GenderNotKnown :
    GenderNotKnown = Gender("notKnown")
    // GenderMale :
    GenderMale = Gender("male")
    // GenderFemale :
    GenderFemale = Gender("female")
    // GenderNotApplicable :
    GenderNotApplicable = Gender("notApplicable")
)

タグの形を変えたい

bsonタグも追加したくなったりする。

writerのときと同様の理屈でwalkerを変える。

walker.py

from goaway.commands.swagger2go import Walker


class MyWalker(Walker):
    def resolve_tag(self, name):
        # return ' `json:"{name}"`'.format(name=name)
        return ' `json:"{name}" bson:"{name}"`'.format(name=name)

--walker=./walker.py:MyWalker を一緒につけるとbsonタグも蒸される。

package person

// People :
type People []Person

// Person :
type Person struct {
    Name   string  `json:"name" bson:"name"`
    Age    int64   `json:"age" bson:"age"`
    Gender Gender  `json:"gender" bson:"gender"`
    Father *Person `json:"father" bson:"father"`
    Mother *Person `json:"mother" bson:"mother"`
}

まとめ

swagger spec(もどき)からgoのコードを生成。なかなか柔軟に色々出来る感じになっている。

  • 異なるpackageの型を参照できる
  • 出力する型をpointerにするか決められる
  • 出力先のファイルを分割できる
  • 出力形式の変更が出来る(--writer)
  • 出力形式の変更が出来る(--walker)

出力先のファイルの分割に限っていうと、x-go-filename を自動で付加するtransformerを書いたり、あるいはschema名からファイルを見るみたいなように書き換えたwalkerを作っても良いかもしれない。