goのコードを解析してgoのコードを生成するpythonのコードを書いてみる

  • 2
    Like
  • 0
    Comment

この記事は WACUL Advent Calendar 2016 14日目の記事です。
書いてる本人は今年の9月からWACULで働いていて、主にバックエンドでgoのコードを書いていたり時々pythonを書いていたりしています。

この記事は前回の記事の続きです(前回の記事は無駄に長いので無理に読まなくても良いです)。

前回はJSON-to-Goの真似をしてAPIのresponse例からgoのコードを生成するpythonのコードを書きました。今回はgoのコードを解析した結果を利用してgoのコードを生成したいと思います。

はじめに

変換処理の自動生成

例えば、go-swaggerなど使うとresponse用のstructの定義が生成されます。これは内部のdatabaseに格納されるstructとは微妙に異なりますがだいたいのところ同じような構造のstructです。

実際のweb APIの作成時には、databaseに永続化されているstructから、このresponse用のstructへの変換が必要になる事があります。これがわりとつまらなくてだるいですね。この部分のコードを自動生成したいという話です。

概要

もう少し話を整理すると、欲しいものはmodel(src)からresponseオブジェクト(dst)への変換コードです。これを得るために変換元や変換先のstruct定義を解析して型の情報を取り出します。このあたりはgoはastパッケージなどを使って気合で作ります。作ったツールで型情報を抽出した後JSONとして出力します。このJSONを利用してsrcからdstへの変換コードを生成しようという企みです。

sample: github api

テキトウな例を使ってコード生成の実行例を紹介したいと思います。

本来は、永続化層の表現をresponseの表現に変換する処理を生成しようという話になるところですが、面倒なので、少し恣意的ですが、以下の様な形で変換処理のコードの自動生成を試したいと思います。

  1. テキトウなAPIのresponseの例からstructを生成(前回の記事で作成したコードを利用)
  2. テキトウなswaggerのyamlからstructを生成(go-swaggerを利用)

これらで生成された2つのstructのgoコードをそれぞれ永続化のためのmodelとapiのresponse表現のstructと見立てて、srcからdstに変換する処理を自動生成をしてみたいと思います。

自動生成自体は以下の様な手順で作成します。

  1. 変換元のgoのstruct定義から情報を抽出 extract :: go[src] => JSON[src]
  2. 変換先のgoのstruct定義から情報を抽出 extract :: go[dst] => JSON[dst]
  3. 抽出した情報からgoのコードを生成 convert :: JSON[src], json[dst] => go[convert]

この2つを利用して変換元(src)のstructから変換先(dst)のstructへの変換処理のコードを生成します。

goのコード(src)の生成

github のdocumentの以下の部分から認証済みユーザーを取得するapiのresponse例を取ってきます。特にこのAPIを選んだ理由はありません。なんとなく目についたからただそれだけです。

github-get-authenticated-user.jsonという名前で保存しておきます。

{
  "login": "octocat",
  "id": 1,
  "avatar_url": "https://github.com/images/error/octocat_happy.gif",
  "gravatar_id": "",
  "url": "https://api.github.com/users/octocat",
  "html_url": "https://github.com/octocat",
  "followers_url": "https://api.github.com/users/octocat/followers",
  "following_url": "https://api.github.com/users/octocat/following{/other_user}",
  "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
  "organizations_url": "https://api.github.com/users/octocat/orgs",
  "repos_url": "https://api.github.com/users/octocat/repos",
  "events_url": "https://api.github.com/users/octocat/events{/privacy}",
  "received_events_url": "https://api.github.com/users/octocat/received_events",
  "type": "User",
  "site_admin": false,
  "name": "monalisa octocat",
  "company": "GitHub",
  "blog": "https://github.com/blog",
  "location": "San Francisco",
  "email": "octocat@github.com",
  "hireable": false,
  "bio": "There once was...",
  "public_repos": 2,
  "public_gists": 1,
  "followers": 20,
  "following": 0,
  "created_at": "2008-01-14T04:33:35Z",
  "updated_at": "2008-01-14T04:33:35Z",
  "total_private_repos": 100,
  "owned_private_repos": 100,
  "private_gists": 81,
  "disk_usage": 10000,
  "collaborators": 8,
  "plan": {
    "name": "Medium",
    "space": 400,
    "private_repos": 20,
    "collaborators": 0
  }
}

ここからstructの定義をを生成します。前回の記事で作ったスクリプトを使います。

$ swagger_src
mkdir -p src/github
python src/jsontogo/jsontogo.py json/github-get-authenticated-user.json --package github --name AuthenticatedUser | gofmt > src/github/authenticated_user.go

以下のようなstructの定義が手に入ります。(src/github/authenticated_user.goとして保存しておきます)

package github

import (
    "time"
)

/* structure
AuthenticatedUser
    Plan
*/
// AuthenticatedUser : auto generated JSON container
type AuthenticatedUser struct {
    AvatarURL         string    `json:"avatar_url" example:"https://github.com/images/error/octocat_happy.gif"`
    Bio               string    `json:"bio" example:"There once was..."`
    Blog              string    `json:"blog" example:"https://github.com/blog"`
    Collaborators     int       `json:"collaborators" example:"8"`
    Company           string    `json:"company" example:"GitHub"`
    CreatedAt         time.Time `json:"created_at" example:"2008-01-14T04:33:35Z"`
    DiskUsage         int       `json:"disk_usage" example:"10000"`
    Email             string    `json:"email" example:"octocat@github.com"`
    EventsURL         string    `json:"events_url" example:"https://api.github.com/users/octocat/events{/privacy}"`
    Followers         int       `json:"followers" example:"20"`
    FollowersURL      string    `json:"followers_url" example:"https://api.github.com/users/octocat/followers"`
    Following         int       `json:"following" example:"0"`
    FollowingURL      string    `json:"following_url" example:"https://api.github.com/users/octocat/following{/other_user}"`
    GistsURL          string    `json:"gists_url" example:"https://api.github.com/users/octocat/gists{/gist_id}"`
    GravatarID        string    `json:"gravatar_id" example:""`
    HTMLURL           string    `json:"html_url" example:"https://github.com/octocat"`
    Hireable          bool      `json:"hireable" example:"False"`
    ID                int       `json:"id" example:"1"`
    Location          string    `json:"location" example:"San Francisco"`
    Login             string    `json:"login" example:"octocat"`
    Name              string    `json:"name" example:"monalisa octocat"`
    OrganizationsURL  string    `json:"organizations_url" example:"https://api.github.com/users/octocat/orgs"`
    OwnedPrivateRepos int       `json:"owned_private_repos" example:"100"`
    Plan              Plan      `json:"plan"`
    PrivateGists      int       `json:"private_gists" example:"81"`
    PublicGists       int       `json:"public_gists" example:"1"`
    PublicRepos       int       `json:"public_repos" example:"2"`
    ReceivedEventsURL string    `json:"received_events_url" example:"https://api.github.com/users/octocat/received_events"`
    ReposURL          string    `json:"repos_url" example:"https://api.github.com/users/octocat/repos"`
    SiteAdmin         bool      `json:"site_admin" example:"False"`
    StarredURL        string    `json:"starred_url" example:"https://api.github.com/users/octocat/starred{/owner}{/repo}"`
    SubscriptionsURL  string    `json:"subscriptions_url" example:"https://api.github.com/users/octocat/subscriptions"`
    TotalPrivateRepos int       `json:"total_private_repos" example:"100"`
    Type              string    `json:"type" example:"User"`
    URL               string    `json:"url" example:"https://api.github.com/users/octocat"`
    UpdatedAt         time.Time `json:"updated_at" example:"2008-01-14T04:33:35Z"`
}

// Plan : auto generated JSON container
type Plan struct {
    Collaborators int    `json:"collaborators" example:"0"`
    Name          string `json:"name" example:"Medium"`
    PrivateRepos  int    `json:"private_repos" example:"20"`
    Space         int    `json:"space" example:"400"`
}

これで変換元(src)の方のstruct定義が手に入りました。

goのコード(dst)の生成

次は変換先(dst)の方のstruct定義を手に入れましょう。swaggerの定義からgo-swaggerを使ってのgoのstruct定義を手に入れます。

api.gru

api.gruというサービスがあります。これは、世の中にあるwebサービスのAPI specを共有してくれているサービスです。多くはOpenAPI/Swagger2.0の形式のものが提供されています。ちなみにここで取得できるswagger.yamlをReDocなどを使って見てみるとswaggerがどんなものかなんとなく分かって良いと思います。

api.gru

試しにgithubのAPIを例に自動生成を試してみましょう。以下のURLからswagger定義のyamlをダウンロードしてきます。

$ make swagger_fetch
mkdir -p yaml
wget https://api.apis.guru/v2/specs/github.com/v3/swagger.yaml -O yaml/github-swagger.yaml
gsed -i "s/type: *'null'/type: object/g; s/'+1':/'plus1':/g; s/'-1':/'minus1':/" yaml/github-swagger.yaml

リンク先の様なyamlが手に入ります。wgetの後のsedは現状のgo-swaggerの問題や登録されているswagger.yamlの問題に対応するためのちょっとした修正です。

struct定義から型情報抽出

こちらは普通にgo-swaggerを実行するだけです。以下の様にして実行します。今回はstructの変換だけなのでmodelのみを生成します。本筋とは関係ないのですがなぜか"github.com/go-openapi/swag"のimportが出力されないという問題があったためgoimportsで追加しています。

$ make swagger_gen
rm -rf dst/swagger/gen
mkdir -p dst/swagger/gen
swagger generate model -f yaml/github-swagger.yaml --target dst/swagger/gen --model-package def
goimports -w dst/swagger/gen/def/*.go

たくさんのコードが生成されますが今回気にするのはUser.goだけです。

package def

// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command

import (
    strfmt "github.com/go-openapi/strfmt"
    "github.com/go-openapi/swag"

    "github.com/go-openapi/errors"
)

// User user
// swagger:model user
type User struct {

    // avatar url
    AvatarURL string `json:"avatar_url,omitempty"`

    // bio
    Bio string `json:"bio,omitempty"`

    // blog
    Blog string `json:"blog,omitempty"`

    // collaborators
    Collaborators int64 `json:"collaborators,omitempty"`

    // company
    Company string `json:"company,omitempty"`

    // ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
    CreatedAt string `json:"created_at,omitempty"`

    // disk usage
    DiskUsage int64 `json:"disk_usage,omitempty"`

    // email
    Email string `json:"email,omitempty"`

    // followers
    Followers int64 `json:"followers,omitempty"`

    // following
    Following int64 `json:"following,omitempty"`

    // gravatar id
    GravatarID string `json:"gravatar_id,omitempty"`

    // hireable
    Hireable bool `json:"hireable,omitempty"`

    // html url
    HTMLURL string `json:"html_url,omitempty"`

    // id
    ID int64 `json:"id,omitempty"`

    // location
    Location string `json:"location,omitempty"`

    // login
    Login string `json:"login,omitempty"`

    // name
    Name string `json:"name,omitempty"`

    // owned private repos
    OwnedPrivateRepos int64 `json:"owned_private_repos,omitempty"`

    // plan
    Plan *UserPlan `json:"plan,omitempty"`

    // private gists
    PrivateGists int64 `json:"private_gists,omitempty"`

    // public gists
    PublicGists int64 `json:"public_gists,omitempty"`

    // public repos
    PublicRepos int64 `json:"public_repos,omitempty"`

    // total private repos
    TotalPrivateRepos int64 `json:"total_private_repos,omitempty"`

    // type
    Type string `json:"type,omitempty"`

    // url
    URL string `json:"url,omitempty"`
}

// Validate validates this user
func (m *User) Validate(formats strfmt.Registry) error {
    var res []error

    if err := m.validatePlan(formats); err != nil {
        // prop
        res = append(res, err)
    }

    if len(res) > 0 {
        return errors.CompositeValidationError(res...)
    }
    return nil
}

func (m *User) validatePlan(formats strfmt.Registry) error {

    if swag.IsZero(m.Plan) { // not required
        return nil
    }

    if m.Plan != nil {

        if err := m.Plan.Validate(formats); err != nil {
            return err
        }
    }

    return nil
}

// UserPlan user plan
// swagger:model UserPlan
type UserPlan struct {

    // collaborators
    Collaborators int64 `json:"collaborators,omitempty"`

    // name
    Name string `json:"name,omitempty"`

    // private repos
    PrivateRepos int64 `json:"private_repos,omitempty"`

    // space
    Space int64 `json:"space,omitempty"`
}

// Validate validates this user plan
func (m *UserPlan) Validate(formats strfmt.Registry) error {
    var res []error

    if len(res) > 0 {
        return errors.CompositeValidationError(res...)
    }
    return nil
}

structからの型情報の抽出

なにはなくともstruct定義のgoコードから型情報を取り出さなくてはいけません。この辺は気合で頑張りましょう。astと気合があればどうにかなります。go-structjsonというコマンドを作りました。雑な作りなのであまり中のコードの品質には期待しないでください。

以下のようなコードから

package alias

// Person :
type Person struct {
    Name string
}

type P *Person
type PS []Person
type PS2 []P
type PSP *[]P

以下のようなJSONを出力します。

{
  "module": {
    "alias": {
      "file": {
        "GOPATH/src/github.com/podhmo/go-structjson/examples/alias/person.go": {
          "alias": {
            "P": {
              "candidates": null,
              "name": "P",
              "original": {
                "kind": "pointer",
                "value": {
                  "kind": "primitive",
                  "value": "Person"
                }
              }
            },
            "PS": {
              "candidates": null,
              "name": "PS",
              "original": {
                "kind": "array",
                "value": {
                  "kind": "primitive",
                  "value": "Person"
                }
              }
            },
            "PS2": {
              "candidates": null,
              "name": "PS2",
              "original": {
                "kind": "array",
                "value": {
                  "kind": "primitive",
                  "value": "P"
                }
              }
            },
            "PSP": {
              "candidates": null,
              "name": "PSP",
              "original": {
                "kind": "pointer",
                "value": {
                  "kind": "array",
                  "value": {
                    "kind": "primitive",
                    "value": "P"
                  }
                }
              }
            }
          },
          "name": "GOPATH/src/github.com/podhmo/go-structjson/examples/alias/person.go",
          "struct": {
            "Person": {
              "fields": {
                "Name": {
                  "embed": false,
                  "name": "Name",
                  "tags": {},
                  "type": {
                    "kind": "primitive",
                    "value": "string"
                  }
                }
              },
              "name": "Person"
            }
          }
        }
      },
      "fullname": "github.com/podhmo/go-structjson/examples/alias",
      "name": "alias"
    }
  }
}

aliasという名前で扱っているものはnewtypeという名前の方が適切だという話があったりもしましたが。ここではtype MyInt intなどはtype aliasと呼んでいます。ごめんなさい。

それぞれのsrc,dstに対してJSONファイルを出力してやります。

$ make swagger_extract
mkdir -p dst/swagger/convert
mkdir -p json/extracted
go-structjson -target src/github > json/extracted/src.json
go-structjson -target dst/swagger/gen/def > json/extracted/dst.json

抽出された型情報のJSONから変換用のコードの生成

さて変換元と変換先のstructのコードは手に入れる事ができました。次は変換処理の自動生成です。この時のためにgoconvertというライブラリを作っています。作り途中のことがreadmeにtodoしか書かれていないところにも現れています。とりあえずこれを使います。実際にはこのライブラリを使って作ったconvert.pyを使ってsrcからdstへの変換用のコードを生成します。

$make swagger_convert
mkdir -p dst/swagger/convert
python src/convert.py --logger=DEBUG --src json/extracted/src.json --dst json/extracted/dst.json --override json/extracted/convert.json > dst/swagger/convert/autogen.go || rm dst/swagger/convert/autogen.go
INFO:goconvert.builders:start register function AuthenticatedUserToUser: ['github.com/podhmo/advent2016/src/github.AuthenticatedUser'] -> ['github.com/podhmo/advent2016/dst/swagger/gen/def.User']
DEBUG:goconvert.builders:resolve: avatarurl ('string',) -> avatarurl ('string',)
DEBUG:goconvert.minicode:gencode: ('string',) -> ('string',)
...
NotImplementedError: mapping not found ('time.Time',) -> ('string',)

おや。上手くいかないですね。

time.Time から string のコードを生成できないようです。中を見てみるとCreatedAtの扱いで困っているようでした。

srcの方(api responseから生成)

type AuthenticatedUser struct {
    AvatarURL         string    `json:"avatar_url" example:"https://github.com/images/error/octocat_happy.gif"`
...
    CreatedAt         time.Time `json:"created_at" example:"2008-01-14T04:33:35Z"`

dstの方(swagger.yamlから生成)

// User user
// swagger:model user
type User struct {

    // avatar url
    AvatarURL string `json:"avatar_url,omitempty"`

...
    // ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
    CreatedAt string `json:"created_at,omitempty"`

まぁたしかにこれはある意味当然で仕方がないことです。goの型の情報をどんなに辿ってみてもtime.Timeからstringの変換はどのように行えば良いかわからないですね。分かるのは開発者だけです。変換処理の出力先と同じのパッケージの中にprimitive.goとして以下のようなtime.Timeからstringへの変換処理を書いてあげます。

package convert

import "time"

// TimeToString :
func TimeToString(t time.Time) string {
    return t.Format(time.RFC3339)
}

そしてJSONの出力をやり直します。今回はgo-funcjsonも追加しています(go-funcjsonで生成したJSONを渡すことで手書きした変換処理の関数はそのまま自動生成の時に使われる様になります)。

$ make swagger_extract
mkdir -p dst/swagger/convert
mkdir -p json/extracted
go-structjson -target src/github > json/extracted/src.json
go-structjson -target dst/swagger/gen/def > json/extracted/dst.json
go-funcjson -target dst/swagger/convert > json/extracted/convert.json || echo '{}' > json/extracted/convert.json
...
-- write: convert --
gofmt -w dst/swagger/convert/autogen.go

今回は成功しました以下の様なコードが生成されています(dst/swagger/convert/autogen.go)。

package convert

import (
    def "github.com/podhmo/advent2016/dst/swagger/gen/def"
    github "github.com/podhmo/advent2016/src/github"
)

// AuthenticatedUserToUser :
func AuthenticatedUserToUser(from *github.AuthenticatedUser) *def.User {
    to := &def.User{}
    to.AvatarURL = from.AvatarURL
    to.Bio = from.Bio
    to.Blog = from.Blog
    tmp1 := (int64)(from.Collaborators)
    to.Collaborators = tmp1
    to.Company = from.Company
    to.CreatedAt = TimeToString(from.CreatedAt)
    tmp2 := (int64)(from.DiskUsage)
    to.DiskUsage = tmp2
    to.Email = from.Email
    tmp3 := (int64)(from.Followers)
    to.Followers = tmp3
    tmp4 := (int64)(from.Following)
    to.Following = tmp4
    to.GravatarID = from.GravatarID
    to.Hireable = from.Hireable
    to.HTMLURL = from.HTMLURL
    tmp5 := (int64)(from.ID)
    to.ID = tmp5
    to.Location = from.Location
    to.Login = from.Login
    to.Name = from.Name
    tmp6 := (int64)(from.OwnedPrivateRepos)
    to.OwnedPrivateRepos = tmp6
    to.Plan = PlanToUserPlan(&(from.Plan))
    tmp7 := (int64)(from.PrivateGists)
    to.PrivateGists = tmp7
    tmp8 := (int64)(from.PublicGists)
    to.PublicGists = tmp8
    tmp9 := (int64)(from.PublicRepos)
    to.PublicRepos = tmp9
    tmp10 := (int64)(from.TotalPrivateRepos)
    to.TotalPrivateRepos = tmp10
    to.Type = from.Type
    to.URL = from.URL
    return to
}

// PlanToUserPlan :
func PlanToUserPlan(from *github.Plan) *def.UserPlan {
    to := &def.UserPlan{}
    tmp11 := (int64)(from.Collaborators)
    to.Collaborators = tmp11
    to.Name = from.Name
    tmp12 := (int64)(from.PrivateRepos)
    to.PrivateRepos = tmp12
    tmp13 := (int64)(from.Space)
    to.Space = tmp13
    return to
}

primitive.goで追加した TimeToString()CreatedAt の部分で使われてますね。コンパイルは通るでしょうか。

$ (cd dst/swagger/convert/; go install)

大丈夫そうです。

実際に変換処理を実行してみる

生成した変換処理のコードを実際に使ってみましょう。以下の様なコードを実行します。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"

    "github.com/davecgh/go-spew/spew"
    "github.com/podhmo/advent2016/dst/swagger/convert"
    "github.com/podhmo/advent2016/src/github"
)

func main() {
    decoder := json.NewDecoder(os.Stdin)
    var from github.AuthenticatedUser
    if err := decoder.Decode(&from); err != nil {
        log.Fatal(err)
    }

    {
        fmt.Println("------------------------------")
        fmt.Println("source:")
        fmt.Println("------------------------------")
        spew.Dump(from)
    }
    {
        to := convert.AuthenticatedUserToUser(&from)
        fmt.Println("------------------------------")
        fmt.Println("destination:")
        fmt.Println("------------------------------")
        spew.Dump(to)
    }
}

上手く変換処理のコードが生成できていればコンパイルは通りますし。実行できるはずです。面倒なので元々ダウンロードしたgithub apiのresponseを入力に渡してみましょう。

$ make swagger_run1
cat json/github-get-authenticated-user.json | go run dst/swagger/main/spew/*.go
------------------------------
source:
------------------------------
(github.AuthenticatedUser) {
 AvatarURL: (string) (len=49) "https://github.com/images/error/octocat_happy.gif",
 Bio: (string) (len=17) "There once was...",
 Blog: (string) (len=23) "https://github.com/blog",
 Collaborators: (int) 8,
 Company: (string) (len=6) "GitHub",
 CreatedAt: (time.Time) 2008-01-14 04:33:35 +0000 UTC,
 DiskUsage: (int) 10000,
 Email: (string) (len=18) "octocat@github.com",
 EventsURL: (string) (len=53) "https://api.github.com/users/octocat/events{/privacy}",
 Followers: (int) 20,
 FollowersURL: (string) (len=46) "https://api.github.com/users/octocat/followers",
 Following: (int) 0,
 FollowingURL: (string) (len=59) "https://api.github.com/users/octocat/following{/other_user}",
 GistsURL: (string) (len=52) "https://api.github.com/users/octocat/gists{/gist_id}",
 GravatarID: (string) "",
 HTMLURL: (string) (len=26) "https://github.com/octocat",
 Hireable: (bool) false,
 ID: (int) 1,
 Location: (string) (len=13) "San Francisco",
 Login: (string) (len=7) "octocat",
 Name: (string) (len=16) "monalisa octocat",
 OrganizationsURL: (string) (len=41) "https://api.github.com/users/octocat/orgs",
 OwnedPrivateRepos: (int) 100,
 Plan: (github.Plan) {
  Collaborators: (int) 0,
  Name: (string) (len=6) "Medium",
  PrivateRepos: (int) 20,
  Space: (int) 400
 },
 PrivateGists: (int) 81,
 PublicGists: (int) 1,
 PublicRepos: (int) 2,
 ReceivedEventsURL: (string) (len=52) "https://api.github.com/users/octocat/received_events",
 ReposURL: (string) (len=42) "https://api.github.com/users/octocat/repos",
 SiteAdmin: (bool) false,
 StarredURL: (string) (len=59) "https://api.github.com/users/octocat/starred{/owner}{/repo}",
 SubscriptionsURL: (string) (len=50) "https://api.github.com/users/octocat/subscriptions",
 TotalPrivateRepos: (int) 100,
 Type: (string) (len=4) "User",
 URL: (string) (len=36) "https://api.github.com/users/octocat",
 UpdatedAt: (time.Time) 2008-01-14 04:33:35 +0000 UTC
}
------------------------------
destination:
------------------------------
(*def.User)(0xc4200ae140)({
 AvatarURL: (string) (len=49) "https://github.com/images/error/octocat_happy.gif",
 Bio: (string) (len=17) "There once was...",
 Blog: (string) (len=23) "https://github.com/blog",
 Collaborators: (int64) 8,
 Company: (string) (len=6) "GitHub",
 CreatedAt: (string) (len=20) "2008-01-14T04:33:35Z",
 DiskUsage: (int64) 10000,
 Email: (string) (len=18) "octocat@github.com",
 Followers: (int64) 20,
 Following: (int64) 0,
 GravatarID: (string) "",
 Hireable: (bool) false,
 HTMLURL: (string) (len=26) "https://github.com/octocat",
 ID: (int64) 1,
 Location: (string) (len=13) "San Francisco",
 Login: (string) (len=7) "octocat",
 Name: (string) (len=16) "monalisa octocat",
 OwnedPrivateRepos: (int64) 100,
 Plan: (*def.UserPlan)(0xc4204da1b0)({
  Collaborators: (int64) 0,
  Name: (string) (len=6) "Medium",
  PrivateRepos: (int64) 20,
  Space: (int64) 400
 }),
 PrivateGists: (int64) 81,
 PublicGists: (int64) 1,
 PublicRepos: (int64) 2,
 TotalPrivateRepos: (int64) 100,
 Type: (string) (len=4) "User",
 URL: (string) (len=36) "https://api.github.com/users/octocat"
})

上手くいっていそうです。(ところで全く構造が同じものの場合は json.Marshal のあとの json.Unmarshal でJSONを経由して値を変換した方が簡単かもしれません)

変換に使ったコマンドなどのMakefileはこちらです。

さらにつづき 既存の型が更新された場合の話

実はこれまでのコード変換で使ってきたコードのsrcの方は前回の記事と同様といっておきながら完全に同様ではありません。前回のコードの以下の部分をコメントアウトしていたのでした。

        # elif "://" in val:
        #     return "github.com/go-openapi/strfmt.Uri"

このコメントアウトを外して再度生成してみます。

$ make swagger_src
...
$ make swagger_extract
...
$ make swagger_convert
...
KeyError: 'Uri'

おや失敗してしまいました。実はこれは以前の記事を書いた時から使っていてたライブラリの github.com/go-openapi/strfmtが更新されているせいでした。具体的には strfmt.Uristrfmt.URI に変わったようです。

updateし直してから再度生成してみましょう。

$ go get -u github.com/go-openapi/strfmt
$ gsed -i 's/strfmt.Uri/strfmt.URI/' src/jsontogo/jsontogo.py
$ make swagger_src
$ make swagger_extract
$ make swagger_convert

今回は成功したようです。もちろん変換も実行できます。

$ make swagger_run1
cat json/github-get-authenticated-user.json | go run dst/swagger/main/spew/*.go
------------------------------
source:
------------------------------
(github.AuthenticatedUser) {
 AvatarURL: (strfmt.URI) (len=49) https://github.com/images/error/octocat_happy.gif,
 Bio: (string) (len=17) "There once was...",
 Blog: (strfmt.URI) (len=23) https://github.com/blog,
 Collaborators: (int) 8,
 Company: (string) (len=6) "GitHub",
 CreatedAt: (time.Time) 2008-01-14 04:33:35 +0000 UTC,
 DiskUsage: (int) 10000,
 Email: (string) (len=18) "octocat@github.com",
 EventsURL: (strfmt.URI) (len=53) https://api.github.com/users/octocat/events{/privacy},
 Followers: (int) 20,
 FollowersURL: (strfmt.URI) (len=46) https://api.github.com/users/octocat/followers,
 Following: (int) 0,
 FollowingURL: (strfmt.URI) (len=59) https://api.github.com/users/octocat/following{/other_user},
 GistsURL: (strfmt.URI) (len=52) https://api.github.com/users/octocat/gists{/gist_id},
 GravatarID: (string) "",
 HTMLURL: (strfmt.URI) (len=26) https://github.com/octocat,
 Hireable: (bool) false,
 ID: (int) 1,
 Location: (string) (len=13) "San Francisco",
 Login: (string) (len=7) "octocat",
 Name: (string) (len=16) "monalisa octocat",
 OrganizationsURL: (strfmt.URI) (len=41) https://api.github.com/users/octocat/orgs,
 OwnedPrivateRepos: (int) 100,
 Plan: (github.Plan) {
  Collaborators: (int) 0,
  Name: (string) (len=6) "Medium",
  PrivateRepos: (int) 20,
  Space: (int) 400
 },
 PrivateGists: (int) 81,
 PublicGists: (int) 1,
 PublicRepos: (int) 2,
 ReceivedEventsURL: (strfmt.URI) (len=52) https://api.github.com/users/octocat/received_events,
 ReposURL: (strfmt.URI) (len=42) https://api.github.com/users/octocat/repos,
 SiteAdmin: (bool) false,
 StarredURL: (strfmt.URI) (len=59) https://api.github.com/users/octocat/starred{/owner}{/repo},
 SubscriptionsURL: (strfmt.URI) (len=50) https://api.github.com/users/octocat/subscriptions,
 TotalPrivateRepos: (int) 100,
 Type: (string) (len=4) "User",
 URL: (strfmt.URI) (len=36) https://api.github.com/users/octocat,
 UpdatedAt: (time.Time) 2008-01-14 04:33:35 +0000 UTC
}
------------------------------
destination:
------------------------------
(*def.User)(0xc42015e280)({
 AvatarURL: (string) (len=49) "https://github.com/images/error/octocat_happy.gif",
 Bio: (string) (len=17) "There once was...",
 Blog: (string) (len=23) "https://github.com/blog",
 Collaborators: (int64) 8,
 Company: (string) (len=6) "GitHub",
 CreatedAt: (string) (len=20) "2008-01-14T04:33:35Z",
 DiskUsage: (int64) 10000,
 Email: (string) (len=18) "octocat@github.com",
 Followers: (int64) 20,
 Following: (int64) 0,
 GravatarID: (string) "",
 Hireable: (bool) false,
 HTMLURL: (string) (len=26) "https://github.com/octocat",
 ID: (int64) 1,
 Location: (string) (len=13) "San Francisco",
 Login: (string) (len=7) "octocat",
 Name: (string) (len=16) "monalisa octocat",
 OwnedPrivateRepos: (int64) 100,
 Plan: (*def.UserPlan)(0xc420330420)({
  Collaborators: (int64) 0,
  Name: (string) (len=6) "Medium",
  PrivateRepos: (int64) 20,
  Space: (int64) 400
 }),
 PrivateGists: (int64) 81,
 PublicGists: (int64) 1,
 PublicRepos: (int64) 2,
 TotalPrivateRepos: (int64) 100,
 Type: (string) (len=4) "User",
 URL: (string) (len=36) "https://api.github.com/users/octocat"
})

おわりに

今回はgoのコードを解析して、特定のstructから別のstructへの変換のコードを生成してみました。ちなみに面倒だったので詳しい内部の話を書くのは止めました。コード生成にはpythonを使いましたがgoで完結した方が綺麗ですし便利かもしれません。ポインタの解析部分だとかalias(newtype)の扱いだとかslicesに対応したコードだとか変換の対応が見つからない場合にどうするかなど話す気力があれば話せるトピックが幾つかあるので気が向いたら書くかもしれません。

おまけ

生成されたコードに勢いがないのでおまけを追加します。いくつかsrcの方の型定義をいじってpointerにするなど意地悪をしてみます。

diff --git a/src/github/authenticated_user.go b/src/github/authenticated_user.go
index 8893144..7e04142 100644
--- a/src/github/authenticated_user.go
+++ b/src/github/authenticated_user.go
@@ -11,8 +11,8 @@ AuthenticatedUser
 */
 // AuthenticatedUser : auto generated JSON container
 type AuthenticatedUser struct {
-   AvatarURL         strfmt.URI `json:"avatar_url" example:"https://github.com/images/error/octocat_happy.gif"`
-   Bio               string     `json:"bio" example:"There once was..."`
+   AvatarURL         *******strfmt.URI `json:"avatar_url" example:"https://github.com/images/error/octocat_happy.gif"`
+   Bio               *********string     `json:"bio" example:"There once was..."`
    Blog              strfmt.URI `json:"blog" example:"https://github.com/blog"`
    Collaborators     int        `json:"collaborators" example:"8"`
    Company           string     `json:"company" example:"GitHub"`
@@ -35,10 +35,10 @@ type AuthenticatedUser struct {
    OrganizationsURL  strfmt.URI `json:"organizations_url" example:"https://api.github.com/users/octocat/orgs"`
    OwnedPrivateRepos int        `json:"owned_private_repos" example:"100"`
    Plan              Plan       `json:"plan"`
-   PrivateGists      int        `json:"private_gists" example:"81"`
-   PublicGists       int        `json:"public_gists" example:"1"`
-   PublicRepos       int        `json:"public_repos" example:"2"`
-   ReceivedEventsURL strfmt.URI `json:"received_events_url" example:"https://api.github.com/users/octocat/received_events"`
+   PrivateGists      *int        `json:"private_gists" example:"81"`
+   PublicGists       **int        `json:"public_gists" example:"1"`
+   PublicRepos       ***int        `json:"public_repos" example:"2"`
+   ReceivedEventsURL ****strfmt.URI `json:"received_events_url" example:"https://api.github.com/users/octocat/received_events"`
    ReposURL          strfmt.URI `json:"repos_url" example:"https://api.github.com/users/octocat/repos"`
    SiteAdmin         bool       `json:"site_admin" example:"False"`
    StarredURL        strfmt.URI `json:"starred_url" example:"https://api.github.com/users/octocat/starred{/owner}{/repo}"`

生成されたコードは以下の様になりますし。まともに動くようです。考えてみれば今回の例にはslicesもなかったですね。

package convert

import (
    def "github.com/podhmo/advent2016/dst/swagger/gen/def"
    github "github.com/podhmo/advent2016/src/github"
)

// AuthenticatedUserToUser :
func AuthenticatedUserToUser(from *github.AuthenticatedUser) *def.User {
    to := &def.User{}
    if from.AvatarURL != nil {
        tmp1 := *(from.AvatarURL)
        if tmp1 != nil {
            tmp2 := *(tmp1)
            if tmp2 != nil {
                tmp3 := *(tmp2)
                if tmp3 != nil {
                    tmp4 := *(tmp3)
                    if tmp4 != nil {
                        tmp5 := *(tmp4)
                        if tmp5 != nil {
                            tmp6 := *(tmp5)
                            if tmp6 != nil {
                                tmp7 := *(tmp6)
                                tmp8 := (string)(tmp7)
                                to.AvatarURL = tmp8
                            }
                        }
                    }
                }
            }
        }
    }
    if from.Bio != nil {
        tmp9 := *(from.Bio)
        if tmp9 != nil {
            tmp10 := *(tmp9)
            if tmp10 != nil {
                tmp11 := *(tmp10)
                if tmp11 != nil {
                    tmp12 := *(tmp11)
                    if tmp12 != nil {
                        tmp13 := *(tmp12)
                        if tmp13 != nil {
                            tmp14 := *(tmp13)
                            if tmp14 != nil {
                                tmp15 := *(tmp14)
                                if tmp15 != nil {
                                    tmp16 := *(tmp15)
                                    if tmp16 != nil {
                                        tmp17 := *(tmp16)
                                        to.Bio = tmp17
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    tmp18 := (string)(from.Blog)
    to.Blog = tmp18
    tmp19 := (int64)(from.Collaborators)
    to.Collaborators = tmp19
    to.Company = from.Company
    to.CreatedAt = TimeToString(from.CreatedAt)
    tmp20 := (int64)(from.DiskUsage)
    to.DiskUsage = tmp20
    to.Email = from.Email
    tmp21 := (int64)(from.Followers)
    to.Followers = tmp21
    tmp22 := (int64)(from.Following)
    to.Following = tmp22
    to.GravatarID = from.GravatarID
    to.Hireable = from.Hireable
    tmp23 := (string)(from.HTMLURL)
    to.HTMLURL = tmp23
    tmp24 := (int64)(from.ID)
    to.ID = tmp24
    to.Location = from.Location
    to.Login = from.Login
    to.Name = from.Name
    tmp25 := (int64)(from.OwnedPrivateRepos)
    to.OwnedPrivateRepos = tmp25
    to.Plan = PlanToUserPlan(&(from.Plan))
    if from.PrivateGists != nil {
        tmp26 := *(from.PrivateGists)
        tmp27 := (int64)(tmp26)
        to.PrivateGists = tmp27
    }
    if from.PublicGists != nil {
        tmp28 := *(from.PublicGists)
        if tmp28 != nil {
            tmp29 := *(tmp28)
            tmp30 := (int64)(tmp29)
            to.PublicGists = tmp30
        }
    }
    if from.PublicRepos != nil {
        tmp31 := *(from.PublicRepos)
        if tmp31 != nil {
            tmp32 := *(tmp31)
            if tmp32 != nil {
                tmp33 := *(tmp32)
                tmp34 := (int64)(tmp33)
                to.PublicRepos = tmp34
            }
        }
    }
    tmp35 := (int64)(from.TotalPrivateRepos)
    to.TotalPrivateRepos = tmp35
    to.Type = from.Type
    tmp36 := (string)(from.URL)
    to.URL = tmp36
    return to
}

// PlanToUserPlan :
func PlanToUserPlan(from *github.Plan) *def.UserPlan {
    to := &def.UserPlan{}
    tmp37 := (int64)(from.Collaborators)
    to.Collaborators = tmp37
    to.Name = from.Name
    tmp38 := (int64)(from.PrivateRepos)
    to.PrivateRepos = tmp38
    tmp39 := (int64)(from.Space)
    to.Space = tmp39
    return to
}