読者です 読者をやめる 読者になる 読者になる

自分用の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でも使いたい。でも両者はわりと独立しているような気がする。一方で新しいパッケージは作りたくない)。