もうちょっと複雑な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を作っても良いかもしれない。