Skip to content

Commit

Permalink
add merge operation
Browse files Browse the repository at this point in the history
  • Loading branch information
can3p committed Apr 14, 2024
1 parent 02f4fb9 commit 62dd9d8
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Available operations:
| ------------- | ------------- |
| set(path, value) | assign field to a particular value |
| del(path) | delete a key |
| merge(path, value) | merge json value into the path. Only JSON values are allowed |

## Examples:

Expand Down
70 changes: 70 additions & 0 deletions pkg/operations/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package operations

import (
"github.com/can3p/sackmesser/pkg/traverse/types"
"github.com/pkg/errors"
)

func Merge(root types.Node, path []string, args ...any) error {
if len(args) != 1 {
return errors.Errorf("set operation expects one argument")
}

value, ok := args[0].(map[string]any)
if !ok {
return errors.Errorf("Merge expects a json as an argument")
}

if len(path) == 1 {
fieldName := path[0]
fieldVal, err := root.GetField(fieldName)

if err == types.ErrFieldMissing {
return root.SetField(fieldName, value)
}

if err != nil {
return err
}

fieldVal = mergeObject(fieldVal, value)
return root.SetField(fieldName, fieldVal)
}

node, err := root.Visit(path[0])

if err != nil {
return err
}

return Merge(node, path[1:], value)
}

func mergeObject(existingValue any, value map[string]any) any {
typed, ok := existingValue.(map[string]any)

if !ok {
return value
}

for fieldName, value := range value {
subfieldValue, exists := typed[fieldName]

if !exists {
typed[fieldName] = value
continue
}

typedSubfieldValue, subfieldIsMap := subfieldValue.(map[string]any)
typedNewValue, newValueIsMap := value.(map[string]any)

if !subfieldIsMap || !newValueIsMap {
typed[fieldName] = value
continue
}

typed[fieldName] = mergeObject(typedSubfieldValue, typedNewValue)
}

return typed
}
68 changes: 68 additions & 0 deletions pkg/operations/merge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package operations

import (
"testing"

"github.com/alecthomas/assert/v2"
"github.com/can3p/sackmesser/pkg/traverse/simplejson"
)

func TestMergeOperation(t *testing.T) {
jstr := `{ "abc": { "def": { "cfa": [1, 2, 3] } } }`

examples := []struct {
description string
path []string
arg any
initial string
expected string
isErr bool
}{
{
description: "add new field to the object",
path: []string{"abc", "def"},
arg: map[string]any{"added": true},
initial: jstr,
expected: `{ "abc": { "def": { "cfa": [ 1, 2, 3 ], "added": true } } }`,
},
{
description: "scalar value should produce an error",
path: []string{"abc", "def"},
arg: true,
initial: jstr,
isErr: true,
},
{
description: "non existant field is just set",
path: []string{"abc", "new field"},
arg: map[string]any{"added": true},
initial: jstr,
expected: `{ "abc": { "def": { "cfa": [ 1, 2, 3 ] }, "new field": { "added": true } } }`,
},
{
description: "non object target field means set",
path: []string{"abc", "def"},
arg: map[string]any{"added": true},
initial: `{ "abc": { "def": true } }`,
expected: `{ "abc": { "def": { "added": true } } }`,
},
}

for idx, ex := range examples {
node := simplejson.MustParse([]byte(ex.initial))

err := Merge(node, ex.path, ex.arg)

if ex.isErr {
assert.Error(t, err, "[Ex %d - %s]", idx+1, ex.description)
continue
} else {
assert.NoError(t, err, "[Ex %d - %s]", idx+1, ex.description)
}

expected := simplejson.MustParse([]byte(ex.expected))

assert.Equal(t, expected.Value(), node.Value(), "[Ex %d - %s]", idx+1, ex.description)
}

}
5 changes: 3 additions & 2 deletions pkg/operations/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ func (op *OpInstance) Apply(root types.Node) error {
}

var operations = map[string]Operation{
"set": Set,
"del": Delete,
"set": Set,
"del": Delete,
"merge": Merge,
}

//nolint:govet
Expand Down
14 changes: 14 additions & 0 deletions pkg/traverse/simplejson/simplejson.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ func (n *jnode) Visit(field string) (types.Node, error) {
panic("unreachable")
}

func (n *jnode) GetField(field string) (any, error) {
typed, ok := n.v.(map[string]any)

if !ok {
return nil, types.ErrWrongVisit
}

if value, ok := typed[field]; ok {
return value, nil
}

return nil, types.ErrFieldMissing
}

func (n *jnode) SetField(field string, value any) error {
switch n.NodeType() {
case types.NodeTypeNull:
Expand Down
14 changes: 14 additions & 0 deletions pkg/traverse/simpleyaml/simpleyaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ func (n *jnode) Visit(field string) (types.Node, error) {
panic("unreachable")
}

func (n *jnode) GetField(field string) (any, error) {
typed, ok := n.v.(map[string]any)

if !ok {
return nil, types.ErrWrongVisit
}

if value, ok := typed[field]; ok {
return value, nil
}

return nil, types.ErrFieldMissing
}

func (n *jnode) SetField(field string, value any) error {
switch n.NodeType() {
case types.NodeTypeNull:
Expand Down
1 change: 1 addition & 0 deletions pkg/traverse/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Node interface {
Visit(field string) (Node, error)
NodeType() NodeType
Value() any
GetField(field string) (any, error)
SetField(field string, value any) error
DeleteField(field string) error
Serialize() ([]byte, error)
Expand Down

0 comments on commit 62dd9d8

Please sign in to comment.