ちょっとしたswagger spec(yaml, json)の取り扱いというか変換というか

過去に自分が作ったやつの使い方について。もう少しマシなものを考えてみたのでメモ。

github.com

yaml上のデータから一部分を取り出したい

こういうyamlがあるとする。swagger specの一部のようなもの。people,person,name,ageが定義されている。

# definitions.yaml

definitions:
  people:
    items:
      $ref: "#/definitions/person"
  person:
    properties:
      name:
        $ref: "#/definitions/name"
      age:
        $ref: "#/definitions/age"
  name:
    type: string
    description: 名前
  age:
    type: integer
    maximum: 100

この定義に$refによる参照が使われている。この$refを取り除くためにderefというコマンドを作っていた。

$ jsonknife deref --src definitions.yaml

結果はこうなる。

definitions:
  people:
    items:
      properties:
        name:
          type: string
          description: 名前
        age:
          type: integer
          maximum: 100
  person:
    properties:
      name:
        type: string
        description: 名前
      age:
        type: integer
        maximum: 100
  name:
    type: string
    description: 名前
  age:
    type: integer
    maximum: 100

全てが展開される。この内personだけが欲しい場合には --ref を使う。

$ jsonknife deref --src definitions.yaml --ref "#/definitions/person"

こういう結果になる。

properties:
  name:
    type: string
    description: 名前
  age:
    type: integer
    maximum: 100

取り出した結果をwrapしたい場合

--ref によりyamlのデータの中から一部だけ取り出せはしたものの。 #/definitions/person の位置にあるデータを取り出すということになってしまう。 今までこういう時に再度 definitions/person でラップするために、別のコマンドに渡していた。以下の様な形。

$ jsonknife deref --src definitions.yaml --ref "#/definitions/person" | dictknife transform --code 'lambda d: {"definitions": {"PERSON": d}}'

結果はこう。

definitions:
  PERSON:
    properties:
      name:
        type: string
        description: 名前
      age:
        type: integer
        maximum: 100

もちろん、結果は期待通りなのだけれど。わざわざこの種の変換を手書きするのも面倒くさい。単に包むだけなのだからオプションにしてしまえば良い。元の階層に復元する処理をwrapと呼ぶとすればrefで取り出すことはunwrapにほかならない。そんなわけでwrapとunwrapで指定できるようにしてみると良いかもしれない。

$ jsonknife deref --src definitions.yaml --unwrap "#/definitions/person" --wrap "#/definitions/PERSON"
definitions:
  PERSON:
    properties:
      name:
        type: string
        description: 名前
      age:
        type: integer
        maximum: 100

複数の値を同時に返す場合

全体に対するある部分の内、複数の部分の値が欲しい場合がある。例えば、nameとageを名前付きで欲しい場合。すごく真面目にやるならconcatを使う形になる。 プロセス置換なども使ってすごく頑張った見た目になるけれど。これで上手くいく。

$ dictknife concat <(jsonknife deref --src definitions.yaml --unwrap "#/definitions/name" --wrap "name")  <(jsonknife deref --src definitions.yaml --unwrap "#/definitions/age" --wrap "age")
name:
  type: string
  description: 名前
age:
  type: integer
  maximum: 100

ところで、--ref は複数渡せた。過去には --with-name というオプションがあり。これによって名前でラッピングされた結果がマージされて返されていた(今は存在していないオプション)。上と同様のものを以下の様に書けた。ただ元の名前でラップするだけなので全くおんなじというわけではない。

# 今は動かない
jsonknife deref --src definitions.yaml --ref "#/definitions/name" --ref "#/definitions/age" --with-name

ここでdefinitionsで包みたい場合に結局またdictknife transformによる同様の変換が必要になる。またwrap,unwrapの対応を数を数えてペアにするというのも馬鹿馬鹿しい。結局以下の様に書けるようにした。unwrapとwrapのペアを@でつなげるという記法。

jsonknife deref --src definitions.yaml --ref "#/definitions/name@#/definitions/NAME" --ref "#/definitions/age@#/definitions/AGE"

結果はこう。

definitions:
  AGE:
    type: integer
    maximum: 100
  NAME:
    type: string
    description: 名前

packageを指定してのgoコードの生成に対するgofmt(goimports)について

github.com

おそらくニッチな話になってしまっているけれど。メモ。

はじめに

goawayを使ってコード生成をしたときの典型的なコードの利用方法は以下の様な形になる。

$ python myscript.py --package=github.com/podhmo/myscript --position=.
# 実際にGOPATHに対応した位置に生成したい場合には --positionを外す
# python myscript.py --package=github.com/podhmo/myscript

ここで生成されるファイルのパスはpythonスクリプトに委ねられる。一方で生成されるファイルに対してgofmtやgoimportsを適用したい。 解決策は2つ

  • 指定したpackageのディレクトリに対して雑に gofmt -w *.go とかする
  • python側にgofmtを実行するコードを生成してもらう

今回は後者について考えてみた

典型的なコード

goを生成する典型的なコードは以下の様な感じになる。

import logging
from goaway import get_repository


def run(package_path, position):
    r = get_repository()

    package = r.package(package_path)

    f = package.file("person.go")
    person = f.struct("person")
    person.define_field("name", f.string)

    f = package.file("group.go")
    group = f.struct("group")
    group.define_field("name", f.string)
    group.define_field("members", person.slice)

    d = r.resolve_package_path(position, package)
    r.emitter.emit_package(package, d=d)


def main():
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--package", default=None)
    parser.add_argument("--position", default=None)
    args = parser.parse_args()

    logging.basicConfig(level=logging.INFO)
    run(args.package, args.position)


if __name__ == "__main__":
    main()

これは実行すると以下の様なperson.goとgroup.goを生成する。

$ python main.py --package="github.com/podhmo/goaway/onemit" --position=.
INFO:goaway.emitter:write: ./onemit/person.go
INFO:goaway.emitter:write: ./onemit/group.go

person.go

package onemit

type person struct {
    name string
}

group.go

package onemit

type group struct {
    name    string
    members []person
}

このpythonに委ねられて生成されるgoのファイルにどうやってgofmt(goimports)を適用しようかという話。

on emit hook

とりあえず雑な対応としてonemitというhookを取るようにした。

@@ -2,6 +2,10 @@
 from goaway import get_repository
 
 
+def onemit(f, fname):
+    print("gofmt -w {}".format(fname))
+
+
 def run(package_path, position):
     r = get_repository()
 
@@ -17,7 +21,7 @@
     group.define_field("members", person.slice)
 
     d = r.resolve_package_path(position, package)
-    r.emitter.emit_package(package, d=d)
+    r.emitter.emit_package(package, d=d, onemit=onemit)
 
 
 def main():

onemitのhookでは生成されるパス名が手に入るのでここでおもむろにgofmtを実行するコードをprintしてあげるようにする。 そして以下の様な感じでリダイレクトした結果をbashなどで実行して適用する。

$ python main.py --package="github.com/podhmo/goaway/onemit" --position=. > fmt.sh
INFO:goaway.emitter:write: ./onemit/person.go
INFO:goaway.emitter:write: ./onemit/group.go
$ bash -x fmt.sh
+ gofmt -w ./onemit/person.go
+ gofmt -w ./onemit/group.go

hookのところでsubprocessを作っても良いけれど。この方が楽な気がした。