strangejson最近の進捗はJSONのunmarshal部分の生成ができるようになったこと
まだ開発中で完成はしていないですが。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/loader
の Config
を同一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