diff --git a/env.go b/env.go index 91b0ac2..f39ba28 100644 --- a/env.go +++ b/env.go @@ -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 { @@ -100,6 +101,12 @@ var baseEnv = initBaseEnv(map[string]Extension{ EvalContextHandler: nil, }, + "oneToManyJoin": { + Func: jlib.OneToManyJoin, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + /* EXTENDED END */ diff --git a/jlib/new.go b/jlib/new.go index 92b6762..75bc5dd 100644 --- a/jlib/new.go +++ b/jlib/new.go @@ -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 @@ -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 } diff --git a/jlib/new_test.go b/jlib/new_test.go index a6c98c7..0140f3e 100644 --- a/jlib/new_test.go +++ b/jlib/new_test.go @@ -2,6 +2,7 @@ package jlib import ( "encoding/json" + "log" "reflect" "testing" @@ -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)) + }) + } +}