strangejson最近の進捗はJSONのunmarshal部分の生成ができるようになったこと

github.com

まだ開発中で完成はしていないですが。strangejsonは最近の進捗によりJSONのunmarshal部分の生成ができるようになりました。 これはどういうことかと言うと以下のコードから。

github.com/podhmo/strangejson/examples/pointer02/person.go

package pointer

// Person :
type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`

    Father *Person `json:"father" required:"false"`
    Mother *Person `json:"mother" required:"false"`
}

UnmarshalJSONのメソッドが生成されるようになったということです。この生成されるUnmarshalJSONはrequired,unquiredのチェックを行ってくれます(詳しい内容昔の記事に書いた)。今のところ調べた範囲では任意のJSONにmarshal可能なstructは全部大丈夫だと思います。

ちなみに上のコード例ではわからないですが。外部のパッケージのimportもgoimportsの範囲では見てくれます。そしてちょっといじると元のstructと同じファイルに生成することもできそうです。

package pointer

import (
    "encoding/json"
    "log"
    "testing"
)

func TestLoad(t *testing.T) {
    {
        var p Person
        err := json.Unmarshal([]byte(`{"name": "foo"}`), &p)
        if err == nil {
            log.Fatal("must error")
        }
        t.Logf("expected: %q", err)
    }
    {
        var p Person
        err := json.Unmarshal([]byte(`{"age": 10}`), &p)
        if err == nil {
            log.Fatal("must error")
        }
        t.Logf("expected: %q", err)
    }
    {
        var p Person
        err := json.Unmarshal([]byte(`{"name": "foo", "age": 10}`), &p)
        if err != nil {
            t.Fatalf("unexpected: %q", err)
        }
    }
    {
        var p Person
        err := json.Unmarshal([]byte(`{"name": "foo", "age": 10, "father": {}}`), &p)
        if err == nil {
            log.Fatal("must error")
        }
        t.Logf("expected: %q", err)
    }
    {
        var p Person
        err := json.Unmarshal([]byte(`{"name": "foo", "age": 10, "father": {"name": "boo", "age": 30}}`), &p)
        if err != nil {
            t.Fatalf("unexpected: %q", err)
        }
    }
}

例えばこういう雑なテストコードが通ります。もうちょっとエラーメッセージは親切にしたいです。

$ go test -v
=== RUN   TestLoad
--- PASS: TestLoad (0.00s)
    pereson_test.go:16: expected: "age is required"
    pereson_test.go:24: expected: "name is required"
    pereson_test.go:39: expected: "name is required"
PASS
ok      github.com/podhmo/strangejson/examples/pointer02    0.004s

実行はこんな感じでやります。

$ strangejson --pkg github.com/podhmo/strangejson/examples/pointer02
2018/02/20 04:58:19 for github.com/podhmo/strangejson/examples/pointer02.Person.UnmarshalJSON
2018/02/20 04:58:19 write /home/nao/go/src/github.com/podhmo/strangejson/examples/pointer02/person_gen.go

生成されたコードは以下の様な感じ。

github.com/podhmo/strangejson/examples/pointer02/person_gen.go

package pointer

import (
    "encoding/json"
    "errors"
)

// UnmarshalJSON : (generated from github.com/podhmo/strangejson/examples/pointer02.Person)
func (x Person) UnmarshalJSON(b []byte) error {
    type internal struct {
        Name   *string  `json:"name"`
        Age    *int     `json:"age"`
        Father **Person `json:"father" required:"false"`
        Mother **Person `json:"mother" required:"false"`
    }

    var p internal
    if err := json.Unmarshal(b, &p); err != nil {
        return err
    }

    if p.Name == nil {
        return errors.New("name is required")
    }
    x.Name = *p.Name
    if p.Age == nil {
        return errors.New("age is required")
    }
    x.Age = *p.Age
    if p.Father != nil {
        x.Father = *p.Father
    }
    if p.Mother != nil {
        x.Mother = *p.Mother
    }

    return nil
}

golangの現在のファイルのimport関係に即したTypeStringの表記を作る

TypeString?

go/typesにQuqlifierという型がある。これと一緒にTypeStringという関数がある(いずれも、GOROOT/src/go/types/typestring.goに定義されている)。これらは型情報付きの表記をするために使われる。

type Qualifier func(*Package) string

// TypeString returns the string representation of typ.
// The Qualifier controls the printing of
// package-level objects, and may be nil.
func TypeString(typ Type, qf Qualifier) string {
    var buf bytes.Buffer
    WriteType(&buf, typ, qf)
    return buf.String()
}

Qualifierの例としては、go/typesのRelativeTo()という関数がわかりやすい。

// RelativeTo(pkg) returns a Qualifier that fully qualifies members of
// all packages other than pkg.
func RelativeTo(pkg *Package) Qualifier {
    if pkg == nil {
        return nil
    }
    return func(other *Package) string {
        if pkg == other {
            return "" // same package; unqualified
        }
        return other.Path()
    }
}

実装を見ればわかるとおり元のpackageと共通なら空文字でそうでなければpackageのpath名を付記した表現になる。

例えば golang.org/x/tools/go/loaderConfig を同一package上で使ったらどうなるか。あるいは異なるpackage(e.g. main)で使ったらどうなるかということを表せる

package main

import (
    "fmt"
    "go/types"

    xloader "golang.org/x/tools/go/loader"
)

func main() {
    conf := xloader.Config{}
    conf.Import(".")
    conf.Import("io")
    info, _ := conf.Load()

    loaderPkg := info.Package("golang.org/x/tools/go/loader").Pkg
    loaderConfig := loaderPkg.Scope().Lookup("Config")

    mainPkg := info.Package(".").Pkg

    {
        fmt.Println("in main package")
        qf := types.RelativeTo(mainPkg)

        fmt.Println("    ", types.TypeString(loaderConfig.Type(), qf))
      // main packageの中では golang.org/x/tools/go/loader.Config
    }
    {
        fmt.Println("in loader package")
        qf := types.RelativeTo(loaderPkg)
        fmt.Println("    ", types.TypeString(loaderConfig.Type(), qf))
      // golang.org/x/tools/go/loaderの中では Config
    }
}

面白いのはPointer型などでも良い感じに表記が得られる点。

// pointer *golang.org/x/tools/go/loader.Config
fmt.Println("    ", types.TypeString(types.NewPointer(loaderConfig.Type()), qf))

// slices []golang.org/x/tools/go/loader.Config
fmt.Println("    ", types.TypeString(types.NewSlice(loaderConfig.Type()), qf))

// map map[string]*golang.org/x/tools/go/loader.Config
fmt.Println("    ", types.TypeString(
    types.NewMap(
        types.Universe.Lookup("string").Type(),
        types.NewPointer(loaderConfig.Type()),
    ),
    qf,
))

似たような仕組みを使って良い感じの表記を作りたい

例えば通常importを使ってpackageから取り出したsymbolを使う場合にはimport nameに.を付けて使う。

import "golang.org/x/tools/go/loader"

// 使うときには
loader.Config

また名前付きでimportしたときにはそれが使われる。

import xloader "golang.org/x/tools/go/loader"

// 使うときには
xloader.Config

このimport nameの部分を意識せず使えるようになったら便利なような気がしていた。それでやってみようというのが今回のテーマ。

import nameなどの取得

importされたpackageに関してはファイル単位で異なるので、ast.Fileから見る必要がある。 各ast.FileのImportsのなかにimportされた用のImportSpecが入っているのでそれを取り出す。named importされた場合にはImportSpec.Nameがnil以外になる。

/*
これが元

import (
   "fmt"
   "go/types"

   xloader "golang.org/x/tools/go/loader"
)
*/

f := info.Package(".").Files[0]
for _, is := range f.Imports {
    fmt.Printf("name=%s path=%s\n", is.Name, is.Path.Value)
}
fmt.Println(f.Name)

// name=<nil> path="fmt"
// name=<nil> path="go/types"
// name=xloader path="golang.org/x/tools/go/loader"

2つを組み合わせてimport関係に即したTypeStringの表記を作る

上の2つを組み合わせて、あるファイルにおけるあるpackageに属するsymbolの表記を計算させることができる。 RelativeToと同様に同じパッケージの場合には空文字列。自身のast.Fileのimportsを見てprefixを決める(ここはキャッシュしても良いかもしれない)

すると例えばgolang.org/x/tools/go/loader をxloaderというimport nameでimportした場合のxloader.Configなどが取得できるようになる。

// .上ではxloaderという名前でimportされている
qf := NameTo(info.Package(".").Pkg, info.Package(".").Files[0])
// xloader.Config
fmt.Println(types.TypeString(loaderConfig.Type(), qf))

これが何かコード生成をする時に使えるかもしれない。これで任意のファイル上でのコード生成みたいな処理が書きやすくなる気がする。

NameToというのは以下の様な実装。

// NameTo :
func NameTo(pkg *types.Package, f *ast.File) types.Qualifier {
    return func(other *types.Package) string {
        if pkg == other {
            return "" // same package; unqualified
        }
        // todo: cache
        for _, is := range f.Imports {
            if is.Path.Value[1:len(is.Path.Value)-1] == other.Path() {
                if is.Name != nil {
                    return is.Name.String()
                }
                return other.Name()
            }
        }
        return other.Name() // todo: add import
    }
}

(ただもう少し利用方法など考えたほうが良い)

code

このコードを実行すると、このコードのソースコードをparseして、このコードがimportした文脈で golang.org/x/tools/go/loader の Configの表記を出力する。

package main

import (
    "fmt"
    "go/ast"
    "go/types"

    xloader "golang.org/x/tools/go/loader"
)

func main() {
    conf := xloader.Config{}
    conf.Import(".")
    info, _ := conf.Load()

    loaderPkg := info.Package("golang.org/x/tools/go/loader").Pkg
    loaderConfig := loaderPkg.Scope().Lookup("Config")

    qf := NameTo(info.Package(".").Pkg, info.Package(".").Files[0])
    fmt.Println(types.TypeString(loaderConfig.Type(), qf))
}

// NameTo :
func NameTo(pkg *types.Package, f *ast.File) types.Qualifier {
    return func(other *types.Package) string {
        if pkg == other {
            return "" // same package; unqualified
        }
        // todo: cache
        for _, is := range f.Imports {
            if is.Path.Value[1:len(is.Path.Value)-1] == other.Path() {
                if is.Name != nil {
                    return is.Name.String()
                }
                return other.Name()
            }
        }
        return other.Name() // todo: add import
    }
}

実行結果

xloader.Config

遊びでstrangejsonというgoのstructの定義とswagger定義(のようなもの)を行ったり来たりするようなものを作り始めた

github.com

strangejsonというgoのstructの定義とswagger定義(のようなもの)との間を行ったり来たりするようなものを作り始めた。まだ完成はしていない。

何をするもの?

何をするものかと言うと

  • goのstructの定義を見てswagger形式のschema定義(まだ確定はしていない)を生成してみたり
  • swagger形式のschema定義(まだ確定はしていない)からgoのstructの定義を生成してみたり

できるような何か。

swagger docとgo codeの間を行ったり来たりするような、reversibleな形になるようにしたいという気持ちがある(この辺の気持ちはnbreversibleの時と同様)。

go code -> swagger docについて

例えば以下のようなstructの定義があったときに、requiredのタグを見てjsonからUnmarshallする時に必須かどうかをチェックするUnmarshalJSONを定義するようにしたい。この時requiredタグがないものはrequiredとして扱う。

// User : user
type User struct {
    // Name : name of user
    Name     string `json:"name" required:"true"`
    Age      int    `json:"age"` // no required option, treated as required
    NickName string `json:"nickname" required:"false"`
}

そのようなschema定義を俯瞰する何かも欲しかったのでswagger(OAS)のschema定義も生成できるようにしようかなーと思ったりしていた。 (出力の形式としてのswagger docについては特にこだわりはない)

swagger doc -> go codeについて

逆にenumなどの定義が存在するswagger docからgoのコードを生成する機能を追加しても良いかもしれないと思ったりしている。 このあたりは生成して終わりではなく関数定義をsyncするという形にできるような気もしている(面倒だったら生成して終わりにするかもしれない)。

どういう経緯?

そもそもの発端はこの辺のJSONの取扱いについて考えていた時に思いついたこと。

これはJSONのunmarshalについての記事なのだけれど。元々のモチベーションとしてはstruct中のpointer定義を省きたいというのが根底にあった。そしてその実現にはカスタムのUnmarshalJSONメソッドを自作するということなのだけれど。逐一このようなメソッドを書くのはとても面倒。なので生成するための何かがほしいなというところから作り始めた。

このあたりで以下の記事がリンクする。

go/ast経由でASTを頑張って解釈するよりgo/typesの各Object(types.Objectはinterfaceだけれど)を触った方が楽なのではという趣旨の記事。このgo/types経由でstructの情報を集めてみるという作業を試してみることもついでにやりたいということも念頭にあったりした。

現状

現状は以下の様な感じ。○は実装に取り掛かっているもののことで完成しているものではない。

○: go -> swagger doc
×: swagger doc -> go

structをparseしてそれっぽいような構造体のリストを作ることができるようになったところまで。swagger docを生成するところまではいっていない。

description(コメント)の取得が結構力技で結局ASTからたどるというようなことをしてしまっているのでもう少し簡潔に書けないか試行錯誤したいというのと。あと各struct間の依存関係を解釈できていないのでできるようにしたい。その他enum的なものの対応やoneOf的なものの対応。validation的なものをタグでどうやって付加するかなど結構抜けている。

今の所できること

以下のようなファイルがあるgithub.com/podhmo/strangejson/examples/simple00というパッケージを対象に実行してみると。

github.com/podhmo/strangejson/examples/simple00/user.go

package simple00

// User : user
type User struct {
    // Name : name of user
    Name     string `json:"name" required:"true"`
    Age      int    `json:"age"` // no required option, treated as required
    NickName string `json:"nickname" required:"false"`
}

github.com/podhmo/strangejson/examples/simple00/skill.go

package simple00

// Skill :
type Skill struct {
    Name string `json:"name"`
}

以下の様な出力が手に入るというところまで。

$ strangejson --pkg github.com/podhmo/strangejson/examples/simple00
[]strangejson.Schema{
  strangejson.Schema{
    Name:        "Skill",
    Description: "",
    Type:        "object",
    Properties:  []strangejson.Property{
      strangejson.Property{
        Name:        "name",
        Description: "",
        Type:        "string",
        Required:    true,
        XGoName:     "Name",
      },
    },
    Required: []string{
      "name",
    },
    XGoName: "Skill",
    Depends: []strangejson.Schema{},
  },
  strangejson.Schema{
    Name:        "User",
    Description: "user",
    Type:        "object",
    Properties:  []strangejson.Property{
      strangejson.Property{
        Name:        "name",
        Description: "",
        Type:        "string",
        Required:    true,
        XGoName:     "Name",
      },
      strangejson.Property{
        Name:        "age",
        Description: "",
        Type:        "integer",
        Required:    true,
        XGoName:     "Age",
      },
      strangejson.Property{
        Name:        "nickname",
        Description: "",
        Type:        "string",
        Required:    false,
        XGoName:     "NickName",
      },
    },
    Required: []string{
      "name",
      "age",
    },
    XGoName: "User",
    Depends: []strangejson.Schema{},
  },
}

goでstructの定義を見てあれこれするのに、ast.Fileを直接見るよりtypes.Packageを参照したほうが楽(かもしれない)

goでstructの定義を見てあれこれしたいことがある。特にコード生成などの文脈が多い。このような時に巷の記事や情報などではASTをがんばって解析するという方法が紹介されている事が多い。それよりもtypes.Package経由で*types.Objectを触ったほうが便利だよという話。

types.Objectとは?

go/typesの中に記述されているinterface。以下のような定義になっている(長いけれど実質使うのはName,Type,Stringくらい(ASTへ反映したいときなどにはPosも使う))。

go/types/object.go(定義は長いし読み飛ばしても構わない)

// TODO(gri) Document factory, accessor methods, and fields. General clean-up.

// An Object describes a named language entity such as a package,
// constant, type, variable, function (incl. methods), or label.
// All objects implement the Object interface.
//
type Object interface {
    Parent() *Scope // scope in which this object is declared; nil for methods and struct fields
    Pos() token.Pos // position of object identifier in declaration
    Pkg() *Package  // nil for objects in the Universe scope and labels
    Name() string   // package local object name
    Type() Type     // object type
    Exported() bool // reports whether the name starts with a capital letter
    Id() string     // object name if exported, qualified name if not exported (see func Id)

    // String returns a human-readable string of the object.
    String() string

    // order reflects a package-level object's source order: if object
    // a is before object b in the source, then a.order() < b.order().
    // order returns a value > 0 for package-level objects; it returns
    // 0 for all other objects (including objects in file scopes).
    order() uint32

    // setOrder sets the order number of the object. It must be > 0.
    setOrder(uint32)

    // setParent sets the parent scope of the object.
    setParent(*Scope)

    // sameId reports whether obj.Id() and Id(pkg, name) are the same.
    sameId(pkg *Package, name string) bool

    // scopePos returns the start position of the scope of this Object
    scopePos() token.Pos

    // setScopePos sets the start position of the scope for this Object.
    setScopePos(pos token.Pos)
}

何が嬉しいのかというと以下のような機能が無料で使えるようになる点。go/typesパッケージの便利なapi群がそのまま使えるようになる。

  • ある型がある型に変換可能か?(ConvertableTo)
  • ある型がある型の変数に代入可能化?(AssinableTo)
  • ある型があるinterfaceを満たしているか?(Implements)

その他いろいろ。

go/typesのObjectの使用感

go/typesのObjectの使用感は、感覚的には、reflectパッケージで値を触るのとASTを直接触るものとの中間くらいのイメージ。少なくともASTを直接触るのよりはずっと楽。

例えば、あるstructのfield定義とそこに含まれているタグを見て何かをするということをしたい場合には以下の様な形になる。

あるstructの定義

package p
type S struct {
    Name string `json:"name"`
    Age int `json:"age"`
    i  int
}

タグを見て何かするコード。

    // pkgからSという名前のobjectを取り出す (pkg (*types.Package) の取得方法は後述)
    S := pkg.Scope().Lookup("S")

    // `type S struct {...}` のstruct定義部分を取り出す
    internal := S.Type().Underlying().(*types.Struct)

    // fieldをiterateする
    for i := 0; i < internal.NumFields(); i++ {
        // tagを取り出してjsonタグに関する情報を取り出す (need import "reflect")
        jsonname, found := reflect.StructTag(internal.Tag(i)).Lookup("json")
        field := internal.Field(i)
        fmt.Printf("%v (exported=%t, jsonname=%s, found=%t)\n", field, field.Exported(), jsonname, found)
    }

例えば上のコードを実行した結果は以下のようになる。

field Name string (exported=true, jsonname=name, found=true)
field Age int (exported=true, jsonname=age, found=true)
field i int (exported=false, jsonname=, found=false)

このようなstructのfieldをただparseするだけの作業なら。ASTをがんばって解析するよりもずっと楽。 加えて適切な型情報も一緒についてくる(後述)ので何らかの型(例えばtime.Timeだとか自分で定義した型だとか)を見た変換みたいなものも書くのが非常に楽。

time.Timeを加えてみる

field定義にtime.timeを加えてみると。以下の様になる。

package p

import "time"

type S struct {
    Name string `json:"name"`
    Age int `json:"age"`
    Birth time.Time `json:"birth`
    i  int
}

依存したpackageの情報もふつうに解釈できて便利。

field Name string (exported=true, jsonname=name, found=true)
field Age int (exported=true, jsonname=age, found=true)
field Birth time.Time (exported=true, jsonname=birth, found=true)
field i int (exported=false, jsonname=, found=false)

primiteveな型の値が取り出したい場合は?

primiteveな型の値が取り出したい場合は、go/typesの中にUniverseという変数が存在していて。これがglobal scopeの値。なのでprimitiveな値がほしければuniverseを覗けば良い。宇宙。

types.Universe.Lookup("int64")

types.Packageを取り出す

types.PackageがあればScopeを取り出して色々できるということが分かった。それでは今度はこのtypes.Packageというものを取り出すにはどうすれば良いか?という話。詳しく話すと別の記事になりそうなのでここでは1つだけあげる。

go/typesのapiにはConfigというstructが定義されており。これを使うのが楽かもしれない。

   // need import "go/types"
    // need import "go/importer"

    conf := types.Config{
        Importer: importer.Default(),
        Error: func(err error) {
            fmt.Printf("!!! %#v\n", err)
        },
    }
    pkg, err := conf.Check("p", fset, []*ast.File{file}, nil)

Importerにimportを解釈するobject(defaultで使われるimporterはgo/importerのDefaultから取り出せる)。Errorに関しては型チェックや何らかの不整合が生じたときのエラー処理を渡してあげれば良い。

conf.Check()はCheckという名前の通りチェックもしてくれる。そしてtypeError的なものが起きたらErrorに渡した関数に渡してくれる。

conf.Check()で発生するエラー

どういうエラーが発生するかというと以下の様な感じ。例えばintをinとタイポした場合だとか。

!!! types.Error{Fset:(*token.FileSet)(0xc420050280), Pos:76, Msg:"undeclared name: in", Soft:false}

time.Timeではなくtime.timeを指定してそんなsymbol存在しないよという場合とか。

!!! types.Error{Fset:(*token.FileSet)(0xc420050280), Pos:103, Msg:"time not declared by package time", Soft:false}

soft:true というのは無視しても良い些細なエラーということ(soft:false は逆なのでヤバイエラーということ)。

とりあえずこれでtypes.Packageは作れる様になったはず。あとはast.Fileが存在すれば。*ast.Fileの作り方に関してはいろんな記事で触れてると思うので省略。

おわりに

(types.Packageの作成にgolang.org/x/tools/go/loaderを使うと便利という話しは別の記事として書く)

ちなみに完全に動くコードの例のgistは以下です。

goでJSONのunmarshal時のrequiredなfieldの扱いについて

TL;DR

  • goであんまりfieldにpointerを使いたくない
  • required,unrequiredを表現するときにはpointerを使わざる負えない?
  • unmarshal時に自分でチェックすればやれないこともない

はじめに

goのJSONへの対応について悩まされる事が多い。色々試行錯誤を繰り返したり何かのツールやライブラリでの捉え方を見てみたりはするのだけれど。これが良いという形を見出だせない。

何が問題なのかというと、goではpointerを多用するとnil dereference panicが起きやすいことで可能ならこれを避けたい(あとstruct自体をfieldにするとzero値で初期化できるのでテストなどで便利というような副作用もある)。一方でpointerを利用しなくてはrequiredのチェックができないということ。

似たような考えや思いは古くからあるらしく例えばqiitaのこの記事などは近い視点で書かれているように思う。

fieldのrequired/unrequired

例えばあるstructのUserがあったとしてnameをstringでageをintで定義するとする。

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

ここでname,ageをrequiredにしたい。このstruct定義ではunmarshal時marshal時それぞれに問題を抱えている。

unmarshal時の問題

unmarshalというのは string([]byte) -> object の場合の話。上の定義をそのまま利用してunmarshalした場合にはzero valueで初期化されたのかそれとも値が存在しなかったのかgo側の値になった後では判断が付かない(structの値生成時に対応するフィールドの初期値を与えなかった場合にはzero valueで初期化されるので)。

ageが存在していない場合

var user User
s := `{"name": "foo"}`
json.Unmarshal([]byte(s), &user)

// main.User{Name: "foo", Age: 0}

ageに0の値が渡された場合

var user User
s := `{"name": "foo", "age": 0}`
json.Unmarshal([]byte(s), &user)

// main.User{Name: "foo", Age: 0}

(同様の問題はもちろんstringでも起こり。この場合は空文字列を許容するかという話になる)

一方で型をpointerにしてしまえばnilかそうでないかでrequired,unrequiredを判別する事ができる。しかし取り回しがし辛い。

type User struct {
    Name *string `json:"name"` // required
    Age  *int    `json:"age"`  // required
}

(実際真面目にjsonschemaなりの仕様を忠実に再現しようとするとpointerになってしまう。完全にmodel用の表現と値の受け渡し用の表現を分けて変換関数を別途作るなどやっても良いがコストが大きい)

marshal時の問題

marshalというのは object -> string([]byte) の場合の話。marshal時にも同様の問題は起きる。とは言え通常marshalといえばどこかに保存されたデータを取ってくる。そしてデータは一度validationを通り抜けたinputを元に作られる(はず)なので。壊れたデータがやってくる事は少ない。なのでunmarshal時に初期値の有無が判別できなくても別に困らないのでは?という思いがある。

(goにおいて値を渡し忘れたかどうかを静的に判別する術が無いと言うのは問題ではあるのだけれど。それは永続化されたデータを取ってくるときの問題。のはず。複数の値の集計結果を保持したい場合の話で言えばちょっと状況が異なるかもしれない)

unmarshalのrequired/unrequiredの解釈

元の記事では何らかのライブラリ(当時では(?)jason, go-simplejson, go-scan)を使って書くという方法だったけれど。特にライブラリを使う必要は無く自前でUnmarshalJSONを定義すれば、文字列をparseするタイミングでrequired/unrequiredのチェックはできそうだった(依存は少ない方が良い)。

例えば以下の様な形で全部pointerのstructをlocalで定義してそちらで一度unmarshalしてから元の型に値を受け渡す形にする。

// UnmarshalJSON :
func (u *User) UnmarshalJSON(b []byte) error {
    type userP struct {
        Name *string `json:"name"` // required
        Age  *int    `json:"age"`  // required
    }

    var p userP
    if err := json.Unmarshal(b, &p); err != nil {
        return err
    }
    if p.Name == nil {
        return errors.Errorf("in User, name is not found data=%s", b)
    }
    u.Name = *p.Name
    if p.Age == nil {
        return errors.Errorf("in User, age is not found data=%s", b)
    }
    u.Age = *p.Age
    return nil
}

このようにしてあげれば外の世界からJSON経由でやってきた場合にはrequired/unrequiredが正しくチェックされるようになる。

var user User
s := `{'name': 'foo'}`
json.Unmarshal([]byte(s), &user)

// in User, age is not found data={"name": "foo"}

ネストした場合

ネストのときも同様で単にstruct毎にUnmarshalJSONを定義して上げれば良い(この時内部のネストされたstruct部分は単にコピーするだけで良い)。

// User :
type User struct {
    Name   string  `json:"name"`
    Age    int     `json:"age"`
    Skills []Skill `json:"skills"`
}

// Skill :
type Skill struct {
    Name string `json:"name"`
}


// UnmarshalJSON :
func (u *User) UnmarshalJSON(b []byte) error {
    type userP struct {
        Name   *string  `json:"name"`
        Age    *int     `json:"age"`
        Skills *[]Skill `json:"skills"`
    }

    var p userP
    if err := json.Unmarshal(b, &p); err != nil {
        return err
    }
    if p.Name == nil {
        return errors.Errorf("in User, name is not found data=%s", b)
    }
    u.Name = *p.Name
    if p.Age == nil {
        return errors.Errorf("in User, age is not found data=%s", b)
    }
    u.Age = *p.Age

    // skillsは自身のUnmarshalJSONによりチェック済みの値が埋まっているので単に受け渡しをするだけで良い
    if p.Skills == nil {
        return errors.Errorf("in User, skills is not found data=%s", b)
    }
    u.Skills = *p.Skills
    return nil
}

// UnmarshalJSON :
func (u *Skill) UnmarshalJSON(b []byte) error {
    type skillP struct {
        Name *string `json:"name"`
    }

    var p skillP
    if err := json.Unmarshal(b, &p); err != nil {
        return err
    }
    if p.Name == nil {
        return errors.Errorf("in Skill, name is not found data=%s", b)
    }
    u.Name = *p.Name
}

:feet: ところで現代においては関数を手書きするというのはやってられないのでコード生成する仕組みをあっても(作っても)良いように思う。

型定義からpointerを無くすことは可能?

そんな悪者呼ばわりされるpointerによるフィールドの定義だけれど。これを無くすというのは不可能。

例えば再帰的な型の定義には絶対にpointerが必要になる(無限再帰が発生するので)。

type User struct {
    Name   string  `json:"name"`
    Age    int     `json:"age"`
    Skills []Skill `json:"skills"`

    Father User `json:"father"`
    Mother User `json:"mother"`
}

あるいはjsonschemaでいうoneOf的なデータの取扱いもnilの方が自然(こちらについてはJSON側での表現をoneOfのそれにするか決める必要もありそう)。

type Item struct {
    Type   string  `json:"type"` // a or b
    A      *A      `json:"a,omitempty"`
    B      *B      `json:"b,omitempty"`
}

go/typesのEval()でリテラルを扱った数値計算のその先

go/types関連の関数などの使い方をメモしておくと便利なのでメモ。おそらく一番わかり易い例として電卓の様な振る舞いをするようなものを作ってみる。

かんたんな数式の計算(電卓)

たまにgo/typesのEval()に触れる記事の中でpkgにnilを渡して数式をする例をあげることがある。以下の様な感じ。 (ここまでだけで終わっている記事があったりしてちょっとさみしい)

package main

import (
    "fmt"
    "go/token"
    "go/types"
    "log"
)

func main() {
    fset := token.NewFileSet()
    expr := "1 * 2 + 3 / 4.0"
    tv, err := types.Eval(fset, nil, token.NoPos, expr)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(expr, "=", tv.Value)
    // 1 * 2 + 3 / 4.0 = 2.75
}

これだけだと結局リテラルを計算するだけなので微妙。

pkgにnilを渡さないで変数を使う

pkgに *types.Package の値を渡してあげる。pkgというのはふつうにgoのコードを書いているときの package <package name> と同じ概念。packageの中にScopeというフィールドを持っており、そこに値を格納しておける。

package main

import (
    "fmt"
    "go/constant"
    "go/token"
    "go/types"
    "log"
)

func main() {
    fset := token.NewFileSet()
    pkg := types.NewPackage("<dummy>", "p")

    // const x = int64(10) とほとんど同じ
    inttype := types.Universe.Lookup("int64").Type()
    x := inttype, constant.MakeInt64(10)
    pkg.Scope().Insert(types.NewConst(token.NoPos, pkg, "x", x))

    expr := "x * x"

    tv, err := types.Eval(fset, pkg, token.NoPos, expr)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(expr, "=", tv.Value)
    // x * x = 100
}

xという変数を利用した電卓のようなことができた。

事前に定義しておいた定数を使った電卓

go/typesにはConfigという便利オブジェクトがいて、このConfigのCheck()を実行した結果がpackageとなって返ってくる。これを利用して事前に定義しておいた定数を使って計算することもできる(名前から推測できるように色々チェックしてくれる。エラーが発生した場合にはErrorフィールドに渡した関数を呼んでくれる)。

package main

import (
    "fmt"
    "go/ast"
    "go/importer"
    "go/parser"
    "go/token"
    "go/types"
    "log"
)

func main() {
    fset := token.NewFileSet()

    code := `
package p
const x = 10
`
    conf := types.Config{
        Importer: importer.Default(),
        Error: func(err error) {
            fmt.Printf("!!! %#v\n", err)
        },
    }

    // load
    file, err := parser.ParseFile(fset, "p", code, parser.AllErrors)
    pkg, err := conf.Check("p", fset, []*ast.File{file}, nil)
    if err != nil {
        log.Fatal(err)
    }

    // eval
    expr := "x * x"

    tv, err := types.Eval(fset, pkg, token.NoPos, expr)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(expr, "=", tv.Value)
    // x * x = 100
}

もちろん。ここで計算した x * x の値をscopeに格納して利用する事もできる。 y = x * x として利用する場合には以下の様にして書く。

   // y = x * x
    pkg.Scope().Insert(types.NewConst(token.NoPos, pkg, "y", tv.Type, tv.Value))
    expr2 := "x + y"
    tv2, err := types.Eval(fset, pkg, token.NoPos, expr2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(expr2, "=", tv2.Value)
    // x + y = 110

その先

その他 go/types 以下には LookupFieldOrMethod() という名前の関数だったり ConvertibleTo(), AssignableTo() と言った名前の関数がある。これらは名前からわかるとおり、渡された値が代入可能であるかだったり変換可能であったりを調べられる。あるいはフィールドやメソッドを持っているかどうか。

これらを使ってASTをparseして変換以外の便利な何かができないかということを考えたりしていた。 (例えば、pkg上でメソッドの有無を調べてAST上にdefault実装を付加だったり。ある型から別の型への変換関数を気軽に生成できないかなどを考えたりしていた)

go/xxx の依存関係

go/xxx 部分の依存関係のメモ。svgをそのままpreviewできる場所がほしい。

f:id:podhmo:20180131001058p:plain

go/typesとgo/importerあたりが外側っぽい。