Skip to content

Commit

Permalink
Refactor by using a tree to hold parsed tags
Browse files Browse the repository at this point in the history
- Makes code simpler
- Less nested loops
- Cleaner recursion
- Also makes out of order arrays easy
  • Loading branch information
Aditya Anchuri committed Jan 20, 2017
1 parent a148598 commit 58348a8
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 144 deletions.
135 changes: 8 additions & 127 deletions hummus.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
"strconv"
"strings"

"github.com/Jeffail/gabs"
"github.com/aditya87/hummus/tree"
"github.com/jeffail/gabs"
)

type arrayTag struct {
Expand All @@ -35,136 +36,16 @@ func Marshal(input interface{}) ([]byte, error) {
}

func marshalReflect(t reflect.Type, v reflect.Value) (*gabs.Container, error) {
jsonObj := gabs.New()

for i := 0; i < t.NumField(); {
gt, err := parseHummusTag(t.Field(i).Tag)
if err != nil && strings.Contains(err.Error(), "invalid struct tag") {
i++
continue
} else if err != nil {
return nil, err
}

if gt.omitEmpty && isEmptyValue(v.Field(i)) {
i++
continue
}

path := gt.tagName
array := false
object := v.Field(i).Interface()
j := i + 1

if at, ok := parseArrayTag(path); ok {
if !jsonObj.ExistsP(at.arrayPath) {
jsonObj.ArrayP(at.arrayPath)
}

if at.childPath != "" {
var childFields []reflect.StructField
var childValues []interface{}
childTag := fmt.Sprintf("hummus:%q", at.childPath)
childFields = append(childFields, reflect.StructField{
Name: t.Field(i).Name,
Type: t.Field(i).Type,
Tag: reflect.StructTag(childTag),
})

childValues = append(childValues, v.Field(i).Interface())
for j < t.NumField() {
gtn, err := parseHummusTag(t.Field(j).Tag)
if err != nil {
return nil, err
}

if atn, ok := parseArrayTag(gtn.tagName); ok &&
atn.arrayPath == at.arrayPath && atn.arrayIndex == at.arrayIndex {
if gtn.omitEmpty && isEmptyValue(v.Field(j)) {
j++
continue
}
nextChildTag := fmt.Sprintf("hummus:%q", atn.childPath)
childFields = append(childFields, reflect.StructField{
Name: t.Field(j).Name,
Type: t.Field(j).Type,
Tag: reflect.StructTag(nextChildTag),
})
childValues = append(childValues, v.Field(j).Interface())
j++
} else {
break
}
}

var err error
var childObject *gabs.Container
childType := reflect.StructOf(childFields)
childValue := reflect.New(childType).Elem()
for k, v := range childValues {
childValue.Field(k).Set(reflect.ValueOf(v))
}
childObject, err = marshalReflect(childType, childValue)
if err != nil {
return nil, err
}

object = childObject.Data()
}

array = true
path = at.arrayPath
}

if array {
jsonObj.ArrayAppendP(object, path)
} else {
jsonObj.SetP(object, path)
}
replaceHashTags(jsonObj, path)
i = j
}
parseTree := tree.NewTree()

return jsonObj, nil
}

func replaceHashTags(obj *gabs.Container, path string) {
keys := strings.Split(path, ".")
curKey := keys[0]
curSubTree := obj.Path(curKey)
if strings.Contains(curKey, "#") {
obj.DeleteP(curKey)
curKey = strings.Replace(curKey, "#", ".", -1)
existingSubTree := obj.S(curKey)
subTreeData := existingSubTree.Data()
subTreeData = mergeObjects(subTreeData, curSubTree.Data())
obj.Set(subTreeData, curKey)
}
if len(keys) != 1 {
replaceHashTags(curSubTree, strings.Join(keys[1:], "."))
}
}

func mergeObjects(dst interface{}, src interface{}) interface{} {
dstMap, dmok := dst.(map[string]interface{})
srcMap, smok := src.(map[string]interface{})
dstArr, daok := dst.([]interface{})
srcArr, saok := src.([]interface{})
if dmok && smok {
for k, v := range srcMap {
dstMap[k] = mergeObjects(dstMap[k], v)
}
dst = dstMap
} else if daok && saok {
for _, v := range srcArr {
dstArr = append(dstArr, v)
for i := 0; i < t.NumField(); i++ {
err := parseTree.Insert(string(t.Field(i).Tag), v.Field(i).Interface(), isEmptyValue(v.Field(i)))
if err != nil {
return nil, err
}
dst = dstArr
} else {
dst = src
}

return dst
return parseTree.BuildJSON(), nil
}

func parseHummusTag(tag reflect.StructTag) (hummusTag, error) {
Expand Down
24 changes: 21 additions & 3 deletions hummus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,25 @@ var _ = Describe("Hummus", func() {
}`))
})

It("handles arrays out of order", func() {
input := struct {
Brand0 string `hummus:"brands[2]"`
Brand1 string `hummus:"brands[0]"`
Brand2 string `hummus:"brands[1]"`
}{
Brand0: "sabra",
Brand1: "athenos",
Brand2: "whole-foods",
}

outJSON, err := hummus.Marshal(input)
Expect(err).NotTo(HaveOccurred())
Expect(outJSON).To(MatchJSON(`
{
"brands": ["athenos", "whole-foods", "sabra"]
}`))
})

It("deals with simple nested arrays", func() {
input := struct {
Brand0 string `hummus:"safeway.brands[0]"`
Expand Down Expand Up @@ -208,7 +227,7 @@ var _ = Describe("Hummus", func() {
Brand0Store1Price: 10,
Brand1Name: "cedars",
Brand1Store0Name: "safeway",
Brand1Store0Price: 10,
Brand1Store0Price: 0,
Reputation: "good",
}

Expand Down Expand Up @@ -236,8 +255,7 @@ var _ = Describe("Hummus", func() {
"name": "cedars",
"stores": [
{
"name": "safeway",
"price": 10
"name": "safeway"
}
]
}
Expand Down
78 changes: 75 additions & 3 deletions tree/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"regexp"
"strconv"
"strings"

"github.com/jeffail/gabs"
)

type Node struct {
Expand Down Expand Up @@ -37,12 +39,18 @@ func NewTree() Tree {
}
}

func (t Tree) Insert(tag string, child interface{}) error {
func (t Tree) Insert(tag string, child interface{}, empty bool) error {
gt, err := parseHummusTag(reflect.StructTag(tag))
if err != nil {
if err != nil && strings.Contains(err.Error(), "invalid struct tag") {
return nil
} else if err != nil {
return err
}

if gt.omitEmpty && empty {
return nil
}

childToInsert := child

if at, yes := parseArrayTag(gt.tagName); yes {
Expand Down Expand Up @@ -74,7 +82,7 @@ func (t Tree) Insert(tag string, child interface{}) error {
return errors.New("fatal error: existing subchild is not a tree")
}

err = childTree.Insert(fmt.Sprintf("hummus:%q", at.childPath), child)
err = childTree.Insert(fmt.Sprintf("hummus:%q", at.childPath), child, empty)
if err != nil {
return err
}
Expand All @@ -93,6 +101,70 @@ func (t Tree) Insert(tag string, child interface{}) error {
return nil
}

func (t Tree) BuildJSON() *gabs.Container {
jsonObj := gabs.New()

for path, node := range t.NodeMap {
if !node.IsArray {
jsonObj.SetP(node.SingleChild, path)
} else {
jsonObj.ArrayP(path)

for _, child := range node.ArrayChildren {
if childTree, ok := child.(Tree); ok {
childJsonObj := childTree.BuildJSON()
jsonObj.ArrayAppendP(childJsonObj.Data(), path)
} else {
jsonObj.ArrayAppendP(child, path)
}
}
}

replaceHashTags(jsonObj, path)
}

return jsonObj
}

func replaceHashTags(obj *gabs.Container, path string) {
keys := strings.Split(path, ".")
curKey := keys[0]
curSubTree := obj.Path(curKey)
if strings.Contains(curKey, "#") {
obj.DeleteP(curKey)
curKey = strings.Replace(curKey, "#", ".", -1)
existingSubTree := obj.S(curKey)
subTreeData := existingSubTree.Data()
subTreeData = mergeObjects(subTreeData, curSubTree.Data())
obj.Set(subTreeData, curKey)
}
if len(keys) != 1 {
replaceHashTags(curSubTree, strings.Join(keys[1:], "."))
}
}

func mergeObjects(dst interface{}, src interface{}) interface{} {
dstMap, dmok := dst.(map[string]interface{})
srcMap, smok := src.(map[string]interface{})
dstArr, daok := dst.([]interface{})
srcArr, saok := src.([]interface{})
if dmok && smok {
for k, v := range srcMap {
dstMap[k] = mergeObjects(dstMap[k], v)
}
dst = dstMap
} else if daok && saok {
for _, v := range srcArr {
dstArr = append(dstArr, v)
}
dst = dstArr
} else {
dst = src
}

return dst
}

func parseHummusTag(tag reflect.StructTag) (hummusTag, error) {
hummusTagString := tag.Get("hummus")
if hummusTagString == "" {
Expand Down
Loading

0 comments on commit 58348a8

Please sign in to comment.