自分用のj2cliをkamidanaという名前で作りはじめた
何で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_filter
や kamidana.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のコードを生成する例(わりとよい)
もうちょっと複雑な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を作っても良いかもしれない。