Skip to content

Commit

Permalink
XC-1364 (#20)
Browse files Browse the repository at this point in the history
* Added new function ObjectsToDocument

* lint & fixes

* Updated to remove type conversion as already converted in the JSONata

* Added new JSONata function OneToManyJoin

* add tests to assert behaviour

* test

* remove usage of reflect

---------

Co-authored-by: James Weeks <[email protected]>
Co-authored-by: tbal999 <[email protected]>
  • Loading branch information
3 people authored Oct 10, 2023
1 parent bbead99 commit 0a4fc93
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 7 deletions.
7 changes: 7 additions & 0 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/xiatechs/jsonata-go/jlib"
"github.com/xiatechs/jsonata-go/jparse"
"github.com/xiatechs/jsonata-go/jtypes"

)

type environment struct {
Expand Down Expand Up @@ -100,6 +101,12 @@ var baseEnv = initBaseEnv(map[string]Extension{
EvalContextHandler: nil,
},

"oneToManyJoin": {
Func: jlib.OneToManyJoin,
UndefinedHandler: defaultUndefinedHandler,
EvalContextHandler: nil,
},

/*
EXTENDED END
*/
Expand Down
91 changes: 84 additions & 7 deletions jlib/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func setValue(obj map[string]interface{}, path string, value interface{}) {
if !ok {
obj[paths[i]] = make(map[string]interface{})
}
// Move to the next nested map

obj, ok = obj[paths[i]].(map[string]interface{})
if !ok {
continue
Expand All @@ -269,16 +269,93 @@ func ObjectsToDocument(input interface{}) (interface{}, error) {
// Call setValue for each item to set the value in the output map
code, ok := item["Code"].(string)
if !ok {
return nil, errors.New("$objectsToDocument input must be an array of objects with Code and Value fields")
continue
}
value := item["Value"]
setValue(output, code, value)
}

value, ok := item["Value"]
if !ok {
return nil, errors.New("$objectsToDocument input must be an array of objects with Code and Value fields")
return output, nil // Return the output map
}

func mergeItems(leftItem interface{}, rightItems []interface{}, rightArrayName string) map[string]interface{} {
mergedItem := make(map[string]interface{})

// Check if leftItem is a map or a struct and merge accordingly
leftVal := reflect.ValueOf(leftItem)
if leftVal.Kind() == reflect.Map {
// Merge fields from the map
for _, key := range leftVal.MapKeys() {
mergedItem[key.String()] = leftVal.MapIndex(key).Interface()
}
} else {
// Merge fields from the struct
leftType := leftVal.Type()
for i := 0; i < leftVal.NumField(); i++ {
fieldName := leftType.Field(i).Name
fieldValue := leftVal.Field(i).Interface()
mergedItem[fieldName] = fieldValue
}
}

setValue(output, code, value)
// If there are matching items in the right array, add them under the specified name
if len(rightItems) > 0 {
mergedItem[rightArrayName] = rightItems
}

return output, nil // Return the output map
return mergedItem
}

func OneToManyJoin(leftArr, rightArr interface{}, leftKey, rightKey, rightArrayName string) (interface{}, error) {
trueLeftArr, ok := leftArr.([]interface{})
if !ok {
return nil, errors.New("left input must be an array of Objects")
}

trueRightArr, ok := rightArr.([]interface{})
if !ok {
return nil, errors.New("right input must be an array of Objects")
}

// Create a map for faster lookup of rightArr elements based on the key
rightMap := make(map[string][]interface{})
for _, item := range trueRightArr {
var val interface{}
// Check if leftItem is a map or a struct and get the key value accordingly
itemMap, ok := item.(map[string]interface{})
if ok {
itemKey, ok := itemMap[rightKey]
if ok {
val = itemKey
}
}
// Convert the key value to a string and associate it with the item in the map
strVal := fmt.Sprintf("%v", val)
rightMap[strVal] = append(rightMap[strVal], item)
}

// Create a slice to store the merged results
var result []map[string]interface{}

// Iterate through the left array and perform the join
for _, leftItem := range trueLeftArr {
var leftVal interface{}
// Check if leftItem is a map or a struct and get the key value accordingly
itemMap, ok := leftItem.(map[string]interface{})
if ok {
itemKey, ok := itemMap[leftKey]
if ok {
leftVal = itemKey
}
}
// Convert the key value to a string
strVal := fmt.Sprintf("%v", leftVal)
rightItems := rightMap[strVal]

// Merge the left and right items
mergedItem := mergeItems(leftItem, rightItems, rightArrayName)
result = append(result, mergedItem)
}

return result, nil
}
86 changes: 86 additions & 0 deletions jlib/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jlib

import (
"encoding/json"
"log"
"reflect"
"testing"

Expand Down Expand Up @@ -75,3 +76,88 @@ func TestSJoin(t *testing.T) {
})
}
}

func TestOneToManyJoin(t *testing.T) {
tests := []struct {
description string
object1 string
object2 string
joinStr1 string
joinStr2 string
expectedOutput string
hasError bool
}{
{
description: "one to many join on key 'id'",
object1: `[{"id":1,"age":5}]`,
object2: `[{"id":1,"name":"Tim"},{"id":1,"name":"Tam"}]`,
joinStr1: "id",
joinStr2: "id",
expectedOutput: "[{\"age\":5,\"example\":[{\"id\":1,\"name\":\"Tim\"},{\"id\":1,\"name\":\"Tam\"}],\"id\":1}]",
},
{
description: "one to many join on key 'id' - left side not an array",
object1: `{"id":1,"age":5}`,
object2: `[{"id":1,"name":"Tim"},{"id":1,"name":"Tam"}]`,
joinStr1: "id",
joinStr2: "id",
expectedOutput: "null",
hasError: true,
},
{
description: "one to many join on key 'id' - right side not an array",
object1: `[{"id":1,"age":5}]`,
object2: `{"id":1,"name":"Tim"}`,
joinStr1: "id",
joinStr2: "id",
expectedOutput: "null",
hasError: true,
},
{
description: "one to many join on key 'id' - has a nested different type - should ignore",
object1: `[{"id":1,"age":5}]`,
object2: `[{"id":1,"name":"Tim"},{"id":1,"name":"Tam"}, ["1", "2"]]`,
joinStr1: "id",
joinStr2: "id",
expectedOutput: "[{\"age\":5,\"example\":[{\"id\":1,\"name\":\"Tim\"},{\"id\":1,\"name\":\"Tam\"}],\"id\":1}]",
},
{
description: "one to many join on key 'id' - has a nested different type - should ignore",
object1: `[{"id":1,"age":5}]`,
object2: `[{"id":1,"name":"Tim"},{"id":1,"name":"Tam"}, [{"id":1,"name":"Tim"},{"id":1,"name":"Tam"}]]`,
joinStr1: "id",
joinStr2: "id",
expectedOutput: "[{\"age\":5,\"example\":[{\"id\":1,\"name\":\"Tim\"},{\"id\":1,\"name\":\"Tam\"}],\"id\":1}]",
},
{
description: "one to many join - complex",
object1: `[{"ID":1,"Name":"Item1"},{"ID":2,"Name":"Item2"}]`,
object2: `[{"ProductID":"1","Price":19.99},{"ProductID":"1","Price":29.99},{"ProductID":"2","Price":39.99}]`,
joinStr1: "ID",
joinStr2: "ProductID",
expectedOutput: "[{\"ID\":1,\"Name\":\"Item1\",\"example\":[{\"Price\":19.99,\"ProductID\":\"1\"},{\"Price\":29.99,\"ProductID\":\"1\"}]},{\"ID\":2,\"Name\":\"Item2\",\"example\":[{\"Price\":39.99,\"ProductID\":\"2\"}]}]",
},
}
for _, tt := range tests {
tt := tt

t.Run(tt.description, func(t *testing.T) {
var o1, o2 interface{}

err := json.Unmarshal([]byte(tt.object1), &o1)
assert.NoError(t, err)
err = json.Unmarshal([]byte(tt.object2), &o2)
assert.NoError(t, err)

output, err := OneToManyJoin(o1, o2, tt.joinStr1, tt.joinStr2, "example")
assert.Equal(t, err != nil, tt.hasError)
if err != nil {
log.Println(tt.description, "|", err)
}

bytes, err := json.Marshal(output)
assert.NoError(t, err)
assert.Equal(t, tt.expectedOutput, string(bytes))
})
}
}

0 comments on commit 0a4fc93

Please sign in to comment.