Skip to content

Commit

Permalink
Marshal hierarchy using tags
Browse files Browse the repository at this point in the history
  • Loading branch information
blgm committed May 8, 2020
1 parent ad65395 commit 85f9370
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 43 deletions.
24 changes: 12 additions & 12 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,33 @@ import (
)

type UnsupportedType struct {
Context context.Context
TypeName string
context context.Context
typ reflect.Type
}

func NewUnsupportedTypeError(ctx context.Context, t reflect.Type) *UnsupportedType {
func newUnsupportedTypeError(ctx context.Context, t reflect.Type) *UnsupportedType {
return &UnsupportedType{
Context: ctx,
TypeName: t.String(),
context: ctx,
typ: t,
}
}

func (u UnsupportedType) Error() string {
return fmt.Sprintf(`unsupported type "%s" %s`, u.TypeName, u.Context)
return fmt.Sprintf(`unsupported type "%s" %s`, u.typ, u.context)
}

type UnsupportedKeyType struct {
Context context.Context
TypeName string
context context.Context
typ reflect.Type
}

func NewUnsupportedKeyTypeError(ctx context.Context, t reflect.Type) *UnsupportedKeyType {
func newUnsupportedKeyTypeError(ctx context.Context, t reflect.Type) *UnsupportedKeyType {
return &UnsupportedKeyType{
Context: ctx,
TypeName: t.String(),
context: ctx,
typ: t,
}
}

func (u UnsupportedKeyType) Error() string {
return fmt.Sprintf(`maps must only have strings keys for "%s" %s`, u.TypeName, u.Context)
return fmt.Sprintf(`maps must only have strings keys for "%s" %s`, u.typ, u.context)
}
93 changes: 93 additions & 0 deletions internal/path/path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package path

import (
"reflect"
"strings"
)

const omitEmptyToken string = "omitempty"

type Segment struct {
Name string
List bool
}

type Path struct {
segments []Segment
OmitEmpty bool
}

func (p Path) Len() int {
return len(p.segments)
}

func (p Path) Pull() (Segment, Path) {
return p.segments[0], Path{
segments: p.segments[1:],
OmitEmpty: p.OmitEmpty,
}
}

func (p Path) String() string {
var parts []string
for _, s := range p.segments {
name := s.Name
if s.List {
name = name + "[]"
}
parts = append(parts, name)
}
return strings.Join(parts, ".")
}

func ComputePath(field reflect.StructField) Path {
var segments []Segment
name := field.Name
omitempty := false

if tag := field.Tag.Get("json"); tag != "" {
name, omitempty = parseTag(tag, field.Name)
} else if tag := field.Tag.Get("jsonry"); tag != "" {
name, omitempty = parseTag(tag, field.Name)
segments = parseSegments(name)
}

if len(segments) == 0 {
segments = append(segments, Segment{
Name: name,
List: false,
})
}

return Path{
OmitEmpty: omitempty,
segments: segments,
}
}

func parseTag(tag, defaultName string) (name string, omitempty bool) {
parts := strings.Split(tag, ",")

if len(parts) >= 1 && len(parts[0]) > 0 {
name = parts[0]
} else {
name = defaultName
}

if len(parts) >= 2 && parts[1] == omitEmptyToken {
omitempty = true
}

return
}

func parseSegments(name string) (s []Segment) {
for _, elem := range strings.Split(name, ".") {
s = append(s, Segment{
Name: strings.TrimRight(elem, "[]"),
List: strings.HasSuffix(elem, "[]"),
})
}

return
}
13 changes: 13 additions & 0 deletions internal/path/path_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package path_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestPath(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Path Suite")
}
54 changes: 54 additions & 0 deletions internal/path/path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package path_test

import (
"reflect"

"code.cloudfoundry.org/jsonry/internal/path"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Path", func() {
It("computes a path from the field name", func() {
p := path.ComputePath(reflect.StructField{Name: "foo"})
Expect(p.String()).To(Equal("foo"))
Expect(p.Len()).To(Equal(1))
})

It("computes a path from a JSON tag", func() {
p := path.ComputePath(reflect.StructField{Tag: `json:"foo"`})
Expect(p.String()).To(Equal("foo"))
Expect(p.Len()).To(Equal(1))
})

It("computes a path from a JSONry tag", func() {
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"foo.bar[].baz.quz"`})
Expect(p.String()).To(Equal("foo.bar[].baz.quz"))
Expect(p.Len()).To(Equal(4))
})

It("implements Pull()", func() {
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"foo.bar[].baz.quz"`})
Expect(p.Len()).To(Equal(4))

s, p := p.Pull()
Expect(s).To(Equal(path.Segment{Name: "foo", List: false}))
Expect(p.Len()).To(Equal(3))

s, p = p.Pull()
Expect(s).To(Equal(path.Segment{Name: "bar", List: true}))
Expect(p.Len()).To(Equal(2))
})

Context("omitempty", func() {
It("picks it up from a JSON tag", func() {
p := path.ComputePath(reflect.StructField{Tag: `json:",omitempty"`})
Expect(p.OmitEmpty).To(BeTrue())
})

It("picks it up from a JSONry tag", func() {
p := path.ComputePath(reflect.StructField{Tag: `jsonry:",omitempty"`})
Expect(p.OmitEmpty).To(BeTrue())
})
})
})
45 changes: 45 additions & 0 deletions internal/tree/tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package tree

import (
"reflect"

"code.cloudfoundry.org/jsonry/internal/path"
)

type Tree map[string]interface{}

func (t Tree) Attach(p path.Path, v interface{}) Tree {
switch p.Len() {
case 0:
panic("empty path")
case 1:
branch, _ := p.Pull()
t[branch.Name] = v
default:
branch, stem := p.Pull()
if branch.List {
t[branch.Name] = spread(stem, v)
} else {
if _, ok := t[branch.Name].(Tree); !ok {
t[branch.Name] = make(Tree)
}
t[branch.Name].(Tree).Attach(stem, v)
}
}

return t
}

func spread(p path.Path, v interface{}) []interface{} {
vv := reflect.ValueOf(v)
if vv.Kind() != reflect.Array && vv.Kind() != reflect.Slice {
v = []interface{}{v}
vv = reflect.ValueOf(v)
}

var s []interface{}
for i := 0; i < vv.Len(); i++ {
s = append(s, make(Tree).Attach(p, vv.Index(i).Interface()))
}
return s
}
13 changes: 13 additions & 0 deletions internal/tree/tree_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package tree_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestTree(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Tree Suite")
}
42 changes: 42 additions & 0 deletions internal/tree/tree_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package tree_test

import (
"encoding/json"
"reflect"

"code.cloudfoundry.org/jsonry/internal/path"
"code.cloudfoundry.org/jsonry/internal/tree"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Tree", func() {
Describe("Attach", func() {
It("attaches a branch with the right value", func() {
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`})
t := make(tree.Tree).Attach(p, "hello")
Expect(json.Marshal(t)).To(MatchJSON(`{"a":{"b":{"c":{"d":{"e":"hello"}}}}}`))
})

It("can attach multiple branches", func() {
t := make(tree.Tree).
Attach(path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`}), "hello").
Attach(path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.f.g"`}), "world!")
Expect(json.Marshal(t)).To(MatchJSON(`{"a":{"b":{"c":{"d":{"e":"hello"}},"f":{"g":"world!"}}}}`))
})

It("creates lists according to the first list hint", func() {
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b[].c.d[].e"`})
t := make(tree.Tree).Attach(p, []string{"hello", "world", "!"})
Expect(json.Marshal(t)).To(MatchJSON(`{"a":{"b":[{"c":{"d":[{"e":"hello"}]}},{"c":{"d":[{"e":"world"}]}},{"c":{"d":[{"e":"!"}]}}]}}`))
})

When("there is no list hint", func() {
It("creates lists at the leaf", func() {
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`})
t := make(tree.Tree).Attach(p, []string{"hello", "world", "!"})
Expect(json.Marshal(t)).To(MatchJSON(`{"a":{"b":{"c":{"d":{"e":["hello","world","!"]}}}}}`))
})
})
})
})

0 comments on commit 85f9370

Please sign in to comment.