strangejsonで生成されるUnmarshalJSONのコードを短くした

github.com

生成されるUnmarshalJSONのコードを短くした。具体的にやったことはnewtype的に定義したstructのunmarsalをcastで済ませるようにした。

生成されるUnmarshalJSON

例えば以下のようなstruct定義で生成されるUnmarshalJSONは以下のようなもの。

// Item :
type Item struct {
    Name string
}

// UnmarshalJSON : (generated from github.com/podhmo/strangejson/examples/manytypes03.Item)
func (x *Item) UnmarshalJSON(b []byte) error {
    type internal struct {
        Name *string
    }

    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
    return nil
}

何が異なるかと言うと。requiredのチェックが入っている点。nameが空の場合にはエラーになる。 (defaultがrequired。unrequiredにするにはフィールドにタグを追加する)

newtype的に定義した場合

以下のようにnewtype的に定義した場合に重複したコードを生成するのを止めた。

// Item2 : newtype
type Item2 Item

func (x *Item2) UnmarshalJSON(b []byte) error {
    return (*Item)(x).UnmarshalJSON(b)
}

短い(良い)。

(aliasの場合)

これは何も生成されない。aliasなので。

// Item3 : alias
type Item3 = Item

構造が同じもの

構造が同じものの場合も新しめのgoならキャスト可能なのだけれど。それには対応していない。

// Item4 : duplicated
type Item4 struct {
    Name string
}

go/typesの関数群を使えばたぶんサポートすることは可能なのだけれど。対応していない理由の1つにタグ部分だけ異なるものへの対応も考えなければ行けない点がある。

// Item5 : difference only required/unrequired
type Item5 struct {
    Name string `required:"false"`
}

こちらの定義ではnameはunrequiredになって欲しい。実際生成されたコードではnameのrequiredチェックが外れている。

func (x *Item5) UnmarshalJSON(b []byte) error {
    type internal struct {
        Name *string `required:"false"`
    }

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

    if p.Name != nil {
        x.Name = *p.Name
    }
    return nil
}

パッケージをまたがった場合

複数のパッケージに跨ったものでもキャストで済ませられる。例えば以下の様な構造になっていて。

$ tree
.
├── item
│   ├── model_gen.go
│   └── model.go
├── item2
│   ├── model_gen.go
│   └── model.go

item/model.goの定義をitem2/model.goで使っていた場合にも省略してくれるようにした。

package item2

import "github.com/podhmo/strangejson/examples/manypackages04/item"

// UnmarshalJSON : (generated from github.com/podhmo/strangejson/examples/manypackages04/item2.Item2)
func (x *Item2) UnmarshalJSON(b []byte) error {
    return (*item.Item)(x).UnmarshalJSON(b)
}

この時のstructの定義はそれぞれ以下のようなもの。

item/model.go

package item

// Item :
type Item struct {
    Name string
}

item2/model.go

package item2

import (
    "github.com/podhmo/strangejson/examples/manypackages04/item"
)

// Item2 : newtype
type Item2 item.Item

コマンド

ちなみに生成する時のコマンドは以下の様な感じ(変わりうる)

$ strangejson --pkg github.com/podhmo/strangejson/examples/manypackages04/*
2018/02/23 04:07:24 for github.com/podhmo/strangejson/examples/manypackages04/item.Item.Item.UnmarshalJSON
2018/02/23 04:07:24 write $GOPATH/src/github.com/podhmo/strangejson/examples/manypackages04/item/model_gen.go
2018/02/23 04:07:24 for github.com/podhmo/strangejson/examples/manypackages04/item2.Item2.Item2.UnmarshalJSON
2018/02/23 04:07:24 write $GOPATH/src/github.com/podhmo/strangejson/examples/manypackages04/item2/model_gen.go

packageからfilepathを推測する方法あるいはその逆

packageからfilepathを推測する方法などをまとめておきたかった。 (ついでに個人的な信仰からホームディレクトリは~になっている)

packageからfilepath

package main

import (
    "go/build"
    "os"
    "os/user"
    "path/filepath"
    "strings"

    "github.com/pkg/errors"
)

func guessPath(pkgname string) (string, error) {
    ctxt := build.Default
    for _, srcdir := range ctxt.SrcDirs() {
        path := filepath.Join(srcdir, pkgname)
        if info, err := os.Stat(path); err == nil && info.IsDir() {
            u, err := user.Current()
            if err != nil {
                return "", err
            }
            return strings.Replace(path, u.HomeDir, "~", 1), nil
        }
    }
    return "", errors.Errorf("%q's physical address is not found", pkgname)
}

filepathからpackage

package main

import (
    "go/build"
    "os/user"
    "path/filepath"
    "strings"

    "github.com/pkg/errors"
)

func guessPkg(path string) (string, error) {
    if strings.HasPrefix(path, "~") {
        u, err := user.Current()
        if err != nil {
            return "", err
        }
        path = filepath.Join(u.HomeDir, path[1:])
    }

    ctxt := build.Default
    path, err := filepath.Abs(path)
    if err != nil {
        return "", err
    }
    for _, srcdir := range ctxt.SrcDirs() {
        if strings.HasPrefix(path, srcdir) {
            pkgname := strings.TrimLeft(strings.Replace(path, srcdir, "", 1), "/")
            return pkgname, nil
        }
    }
    return "", errors.Errorf("%q is not subdir of srcdirs(%q)", path, build.Default.SrcDirs())
}

使った結果

GOPATHの方もGOROOTの方もOK

func main() {
    {
        pkg := "golang.org/x/tools/go/loader"
        fmt.Println(guessPath(pkg))
        path, _ := guessPath(pkg)
        fmt.Println(guessPkg(path))
    }
    fmt.Println("----------------------------------------")
    {
        pkg := "encoding/json"
        fmt.Println(guessPath(pkg))
        path, _ := guessPath(pkg)
        fmt.Println(guessPkg(path))
    }
}
~/go/src/golang.org/x/tools/go/loader <nil>
golang.org/x/tools/go/loader <nil>
----------------------------------------
/usr/lib/go/src/encoding/json <nil>
encoding/json <nil>