この記事は 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の表現に変換する処理を生成しようという話になるところですが、面倒なので、少し恣意的ですが、以下の様な形で変換処理のコードの自動生成を試したいと思います。
- テキトウなAPIのresponseの例からstructを生成(前回の記事で作成したコードを利用)
- テキトウなswaggerのyamlからstructを生成(go-swaggerを利用)
これらで生成された2つのstructのgoコードをそれぞれ永続化のためのmodelとapiのresponse表現のstructと見立てて、srcからdstに変換する処理を自動生成をしてみたいと思います。
自動生成自体は以下の様な手順で作成します。
- 変換元のgoのstruct定義から情報を抽出
extract :: go[src] => JSON[src]
- 変換先のgoのstruct定義から情報を抽出
extract :: go[dst] => JSON[dst]
- 抽出した情報から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がどんなものかなんとなく分かって良いと思います。
試しに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.Uri
が strfmt.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
}