From bbead993b33278381f3d5343a32e5abd8316d207 Mon Sep 17 00:00:00 2001 From: Tom <53711814+tbal999@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:26:01 +0100 Subject: [PATCH] Json objects to document func (#19) * Added new function ObjectsToDocument * lint & fixes * Updated to remove type conversion as already converted in the JSONata * add some errors --------- Co-authored-by: James Weeks Co-authored-by: tbal999 --- callable_test.go | 35 ++++++++++++----------- env.go | 6 ++++ errrors_test.go | 3 +- eval_test.go | 3 +- jlib/new.go | 53 +++++++++++++++++++++++++++++++++++ jlib/number.go | 2 +- jlib/string.go | 6 ++-- jparse/doc.go | 2 +- jparse/jparse_test.go | 3 +- jparse/lexer_test.go | 3 +- jsonata-test/main.go | 13 +++++---- jsonata.go | 1 - jsonata_test.go | 3 +- processor.go | 65 +++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 164 insertions(+), 34 deletions(-) create mode 100644 processor.go diff --git a/callable_test.go b/callable_test.go index 890e138..f07493b 100644 --- a/callable_test.go +++ b/callable_test.go @@ -6,7 +6,6 @@ package jsonata import ( "errors" - "github.com/stretchr/testify/assert" "math" "reflect" "regexp" @@ -15,6 +14,8 @@ import ( "sync" "testing" + "github.com/stretchr/testify/assert" + "github.com/xiatechs/jsonata-go/jparse" "github.com/xiatechs/jsonata-go/jtypes" ) @@ -2381,19 +2382,19 @@ func TestRegexCallable(t *testing.T) { end: 5, groups: []string{}, next: &matchCallable{ - callableName: callableName{ - sync.Mutex{}, - "next", - }, + callableName: callableName{ + sync.Mutex{}, + "next", + }, match: "ad", start: 5, end: 7, groups: []string{}, next: &matchCallable{ - callableName: callableName{ - sync.Mutex{}, - "next", - }, + callableName: callableName{ + sync.Mutex{}, + "next", + }, match: "ab", start: 7, end: 9, @@ -2452,10 +2453,10 @@ func TestRegexCallable(t *testing.T) { "d", }, next: &matchCallable{ - callableName: callableName{ - sync.Mutex{}, - "next", - }, + callableName: callableName{ + sync.Mutex{}, + "next", + }, match: "ab", start: 7, end: 9, @@ -2524,10 +2525,10 @@ func TestRegexCallable(t *testing.T) { "", // undefined in jsonata-js }, next: &matchCallable{ - callableName: callableName{ - sync.Mutex{}, - "next", - }, + callableName: callableName{ + sync.Mutex{}, + "next", + }, match: "ab", start: 7, end: 9, diff --git a/env.go b/env.go index 0d1e08e..91b0ac2 100644 --- a/env.go +++ b/env.go @@ -94,6 +94,12 @@ var baseEnv = initBaseEnv(map[string]Extension{ EvalContextHandler: defaultContextHandler, }, + "objectsToDocument": { + Func: jlib.ObjectsToDocument, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + /* EXTENDED END */ diff --git a/errrors_test.go b/errrors_test.go index 55232d9..90e2c74 100644 --- a/errrors_test.go +++ b/errrors_test.go @@ -2,8 +2,9 @@ package jsonata import ( "encoding/json" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestErrors(t *testing.T) { diff --git a/eval_test.go b/eval_test.go index 515a10c..4c1b4df 100644 --- a/eval_test.go +++ b/eval_test.go @@ -5,13 +5,14 @@ package jsonata import ( - "github.com/stretchr/testify/assert" "math" "reflect" "regexp" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/xiatechs/jsonata-go/jlib" "github.com/xiatechs/jsonata-go/jparse" "github.com/xiatechs/jsonata-go/jtypes" diff --git a/jlib/new.go b/jlib/new.go index b29cb14..92b6762 100644 --- a/jlib/new.go +++ b/jlib/new.go @@ -2,6 +2,7 @@ package jlib import ( "encoding/json" + "errors" "fmt" "reflect" "strings" @@ -229,3 +230,55 @@ func ObjMerge(i1, i2 interface{}) interface{} { return output } + +// setValue sets the value in the obj map at the specified dot notation path. +func setValue(obj map[string]interface{}, path string, value interface{}) { + paths := strings.Split(path, ".") // Split the path into parts + + // Iterate through path parts to navigate/create nested maps + for i := 0; i < len(paths)-1; i++ { + // If the key does not exist, create a new map at the key + _, ok := obj[paths[i]] + 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 + } + } + + obj[paths[len(paths)-1]] = value +} + +// objectsToDocument converts an array of Items to a nested map according to the Code paths. +func ObjectsToDocument(input interface{}) (interface{}, error) { + trueInput, ok := input.([]interface{}) + if !ok { + return nil, errors.New("$objectsToDocument input must be an array of objects") + } + + output := make(map[string]interface{}) // Initialize the output map + // Iterate through each item in the input + for _, itemToInterface := range trueInput { + item, ok := itemToInterface.(map[string]interface{}) + if !ok { + return nil, errors.New("$objectsToDocument input must be an array of objects with Code and Value fields") + } + // 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") + } + + value, ok := item["Value"] + if !ok { + return nil, errors.New("$objectsToDocument input must be an array of objects with Code and Value fields") + } + + setValue(output, code, value) + } + + return output, nil // Return the output map +} diff --git a/jlib/number.go b/jlib/number.go index 9cc62b0..ebc9282 100644 --- a/jlib/number.go +++ b/jlib/number.go @@ -116,7 +116,7 @@ func Random() float64 { // It does this by converting back and forth to strings to // avoid floating point rounding errors, e.g. // -// 4.525 * math.Pow10(2) returns 452.50000000000006 +// 4.525 * math.Pow10(2) returns 452.50000000000006 func multByPow10(x float64, n int) float64 { if n == 0 || math.IsNaN(x) || math.IsInf(x, 0) { return x diff --git a/jlib/string.go b/jlib/string.go index 8764825..225552f 100644 --- a/jlib/string.go +++ b/jlib/string.go @@ -231,9 +231,9 @@ func Join(values reflect.Value, separator jtypes.OptionalString) (string, error) // regular expression in the source string. Each object in the // array has the following fields: // -// match - the substring matched by the regex -// index - the starting offset of this match -// groups - any captured groups for this match +// match - the substring matched by the regex +// index - the starting offset of this match +// groups - any captured groups for this match // // The optional third argument specifies the maximum number // of matches to return. By default, Match returns all matches. diff --git a/jparse/doc.go b/jparse/doc.go index 22826d6..c352f34 100644 --- a/jparse/doc.go +++ b/jparse/doc.go @@ -6,7 +6,7 @@ // syntax trees. Most clients will not need to work with // this package directly. // -// Usage +// # Usage // // Call the Parse function, passing a JSONata expression as // a string. If an error occurs, it will be of type Error. diff --git a/jparse/jparse_test.go b/jparse/jparse_test.go index 221c7a1..0a19d00 100644 --- a/jparse/jparse_test.go +++ b/jparse/jparse_test.go @@ -6,13 +6,14 @@ package jparse_test import ( "fmt" - "github.com/stretchr/testify/assert" "regexp" "regexp/syntax" "strings" "testing" "unicode/utf8" + "github.com/stretchr/testify/assert" + "github.com/xiatechs/jsonata-go/jparse" ) diff --git a/jparse/lexer_test.go b/jparse/lexer_test.go index 06f09ab..18f0753 100644 --- a/jparse/lexer_test.go +++ b/jparse/lexer_test.go @@ -6,8 +6,9 @@ package jparse import ( "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) type lexerTestCase struct { diff --git a/jsonata-test/main.go b/jsonata-test/main.go index 12078ab..c77f462 100644 --- a/jsonata-test/main.go +++ b/jsonata-test/main.go @@ -179,12 +179,13 @@ func runTest(tc testCase, dataDir string, path string) (bool, error) { // loadTestExprFile loads a jsonata expression from a file and returns the // expression // For example, one test looks like this -// { -// "expr-file": "case000.jsonata", -// "dataset": null, -// "bindings": {}, -// "result": 2 -// } +// +// { +// "expr-file": "case000.jsonata", +// "dataset": null, +// "bindings": {}, +// "result": 2 +// } // // We want to load the expression from case000.jsonata so we can use it // as an expression in the test case diff --git a/jsonata.go b/jsonata.go index 54e3842..164061a 100644 --- a/jsonata.go +++ b/jsonata.go @@ -224,7 +224,6 @@ type evaluator interface { } type simple struct { - } func (s simple) InitialEval(item interface{}, expression string) (interface{}, error) { diff --git a/jsonata_test.go b/jsonata_test.go index 5be6eb5..d11c08c 100644 --- a/jsonata_test.go +++ b/jsonata_test.go @@ -8,7 +8,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/stretchr/testify/assert" "io/ioutil" "math" "os" @@ -20,6 +19,8 @@ import ( "time" "unicode/utf8" + "github.com/stretchr/testify/assert" + "github.com/xiatechs/jsonata-go/jparse" "github.com/xiatechs/jsonata-go/jtypes" ) diff --git a/processor.go b/processor.go new file mode 100644 index 0000000..374df58 --- /dev/null +++ b/processor.go @@ -0,0 +1,65 @@ +package jsonata + +import ( + "fmt" +) + +type JsonataProcessor struct { + tree *Expr +} + +func NewProcessor(jsonataString string) (j *JsonataProcessor, err error) { + defer func() { // go-jsonata uses panic fallthrough design so this is necessary + if r := recover(); r != nil { + err = fmt.Errorf("jsonata error: %v", r) + } + }() + + jsnt := replaceQuotesAndCommentsInPaths(jsonataString) + + e := MustCompile(jsnt) + + j = &JsonataProcessor{} + + j.tree = e + + return j, err +} + +// Execute - helper function that lets you parse and run jsonata scripts against an object +func (j *JsonataProcessor) Execute(input interface{}) (output []map[string]interface{}, err error) { + defer func() { // go-jsonata uses panic fallthrough design so this is necessary + if r := recover(); r != nil { + err = fmt.Errorf("jsonata error: %v", r) + } + }() + + output = make([]map[string]interface{}, 0) + + item, err := j.tree.Eval(input) + if err != nil { + return nil, err + } + + if aMap, ok := item.(map[string]interface{}); ok { + output = append(output, aMap) + + return output, nil + } + + if aList, ok := item.([]interface{}); ok { + for index := range aList { + if aMap, ok := aList[index].(map[string]interface{}); ok { + output = append(output, aMap) + } + } + + return output, nil + } + + if aList, ok := item.([]map[string]interface{}); ok { + return aList, nil + } + + return output, nil +}