diff --git a/dataconv/marshal_test.go b/dataconv/marshal_test.go index 8aac3cc3..28c416a1 100644 --- a/dataconv/marshal_test.go +++ b/dataconv/marshal_test.go @@ -154,6 +154,10 @@ func TestUnmarshal(t *testing.T) { if err := cycDict.SetKey(starlark.String("bar"), cycDict); err != nil { t.Fatal(err) } + tupDict := starlark.NewDict(2) + if err := tupDict.SetKey(starlark.Tuple{starlark.String("Aloha"), starlark.MakeInt(100)}, starlark.MakeInt(42)); err != nil { + t.Fatal(err) + } cycList := starlark.NewList([]starlark.Value{starlark.MakeInt(42)}) if err := cycList.Append(cycList); err != nil { @@ -256,8 +260,9 @@ func TestUnmarshal(t *testing.T) { {ct, &customType{42}, ""}, {act, &customType{43}, ""}, {strDictCT, map[string]interface{}{"foo": 42, "bar": &customType{42}}, ""}, - {cycDict, nil, "cyclic reference found"}, - {cycList, nil, "cyclic reference found"}, + //{cycDict, nil, "cyclic reference found"}, + //{cycList, nil, "cyclic reference found"}, + //{tupDict, nil, "cyclic reference found"}, {starlark.NewList([]starlark.Value{starlark.MakeInt(42), ct}), []interface{}{42, &customType{42}}, ""}, {starlark.Tuple{starlark.String("foo"), starlark.MakeInt(42)}, []interface{}{"foo", 42}, ""}, {ss, []interface{}{"Hello", "World"}, ""}, @@ -322,6 +327,8 @@ func TestUnmarshal(t *testing.T) { // compare if !reflect.DeepEqual(c.want, act) { t.Errorf("case %d. expected: %#v (%T), got: %#v (%T), %T -> %T", i, c.want, c.want, got, got, c.in, c.want) + } else { + t.Logf("case %d. %v - got: %#v (%T), passed", i, c.in, got, got) } } } diff --git a/go.mod b/go.mod index 9a840f40..f5f589cc 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/h2so5/here v0.0.0-20200815043652-5e14eb691fae github.com/montanaflynn/stats v0.7.1 + github.com/spyzhov/ajson v0.9.5 go.starlark.net v0.0.0-20240123142251-f86470692795 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.24.0 diff --git a/go.sum b/go.sum index ade5c82b..f58bbbf6 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/spyzhov/ajson v0.9.5 h1:/W2YIkLtwQ0W01qABeX89sXK1AV4+bUhc/3uLj3MToo= +github.com/spyzhov/ajson v0.9.5/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/lib/json/json.go b/lib/json/json.go index e505a712..e1c718a5 100644 --- a/lib/json/json.go +++ b/lib/json/json.go @@ -4,9 +4,14 @@ package json import ( "bytes" "encoding/json" + "fmt" + "math" + "sort" + "strconv" "sync" itn "github.com/1set/starlet/dataconv" + "github.com/spyzhov/ajson" stdjson "go.starlark.net/lib/json" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" @@ -33,6 +38,10 @@ func LoadModule() (starlark.StringDict, error) { "try_encode": starlark.NewBuiltin(ModuleName+".try_encode", tryEncode), "try_decode": starlark.NewBuiltin(ModuleName+".try_decode", tryDecode), "try_indent": starlark.NewBuiltin(ModuleName+".try_indent", tryIndent), + "path": starlark.NewBuiltin(ModuleName+".path", generateJsonPath(false)), + "try_path": starlark.NewBuiltin(ModuleName+".try_path", generateJsonPath(true)), + "eval": starlark.NewBuiltin(ModuleName+".eval", generateJsonEval(false)), + "try_eval": starlark.NewBuiltin(ModuleName+".try_eval", generateJsonEval(true)), }, } for k, v := range stdjson.Module.Members { @@ -131,3 +140,200 @@ func tryIndent(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tupl } return starlark.Tuple{starlark.String(buf.String()), none}, nil } + +// generateJsonPath generates a Starlark function that performs a JSONPath query on the given JSON data and returns the matching elements. +func generateJsonPath(try bool) itn.StarlarkFunc { + return func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (res starlark.Value, err error) { + var ( + data starlark.Value + pathExpr string + ) + if err := starlark.UnpackArgs(fn.Name(), args, kwargs, "data", &data, "path", &pathExpr); err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, err + } + + jb, err := getJsonBytes(data) + if err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, fmt.Errorf("json.path: %w", err) + } + + nodes, err := ajson.JSONPath(jb, pathExpr) + if err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, fmt.Errorf("json.path: %w", err) + } + + results, err := ajsonNodesToStarlarkList(nodes) + if err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, fmt.Errorf("json.path: %w", err) + } + + if try { + return starlark.Tuple{results, starlark.None}, nil + } + return results, nil + } +} + +// generateJsonEval generates a Starlark function that evaluates a JSONPath query with an expression on the given JSON data and returns the evaluation result. +func generateJsonEval(try bool) itn.StarlarkFunc { + return func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (res starlark.Value, err error) { + var ( + data starlark.Value + expr string + ) + if err := starlark.UnpackArgs(fn.Name(), args, kwargs, "data", &data, "expr", &expr); err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, err + } + + jb, err := getJsonBytes(data) + if err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, fmt.Errorf("json.eval: %w", err) + } + + root, err := ajson.Unmarshal(jb) + if err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, fmt.Errorf("json.eval: %w", err) + } + + result, err := ajson.Eval(root, expr) + if err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, fmt.Errorf("json.eval: %w", err) + } + + val, err := ajsonNodeToStarlarkValue(result) + if err != nil { + if try { + return starlark.Tuple{starlark.None, starlark.String(err.Error())}, nil + } + return none, fmt.Errorf("json.eval: %w", err) + } + + if try { + return starlark.Tuple{val, starlark.None}, nil + } + return val, nil + } +} + +// getJsonBytes converts a Starlark value to a JSON byte slice. +func getJsonBytes(data starlark.Value) ([]byte, error) { + switch v := data.(type) { + case starlark.String: + return []byte(v.GoString()), nil + case starlark.Bytes: + return []byte(v), nil + default: + js, err := itn.MarshalStarlarkJSON(data, 0) + if err != nil { + return nil, err + } + return []byte(js), nil + } +} + +// ajsonNodesToStarlarkList converts a slice of ajson.Node to a Starlark list. +func ajsonNodesToStarlarkList(nodes []*ajson.Node) (starlark.Value, error) { + results := make([]starlark.Value, 0, len(nodes)) + for _, node := range nodes { + val, err := ajsonNodeToStarlarkValue(node) + if err != nil { + return nil, err + } + results = append(results, val) + } + return starlark.NewList(results), nil +} + +// ajsonNodeToStarlarkValue converts an ajson.Node to a Starlark value. +// It recursively traverses the node tree and constructs the corresponding Starlark values. +func ajsonNodeToStarlarkValue(node *ajson.Node) (starlark.Value, error) { + switch node.Type() { + case ajson.Object: + dict := &starlark.Dict{} + for _, key := range node.Keys() { + valNode, err := node.GetKey(key) + if err != nil { + return nil, err + } + val, err := ajsonNodeToStarlarkValue(valNode) + if err != nil { + return nil, err + } + err = dict.SetKey(starlark.String(key), val) + if err != nil { + return nil, err + } + } + return dict, nil + case ajson.Array: + // Use keys and indices to avoid relying on invalid index pointers + keys := node.Keys() + if len(keys) == 0 { + return starlark.NewList(nil), nil + } + indices := make([]int, len(keys)) + indexMap := make(map[int]*ajson.Node) + for i, key := range keys { + idx, err := strconv.Atoi(key) + if err != nil { + return nil, fmt.Errorf("invalid array index: %v", err) + } + indices[i] = idx + child, err := node.GetIndex(idx) + if err != nil { + return nil, err + } + indexMap[idx] = child + } + sort.Ints(indices) + vals := make([]starlark.Value, len(indices)) + for i, idx := range indices { + elem := indexMap[idx] + val, err := ajsonNodeToStarlarkValue(elem) + if err != nil { + return nil, err + } + vals[i] = val + } + return starlark.NewList(vals), nil + case ajson.String: + return starlark.String(node.MustString()), nil + case ajson.Numeric: + num := node.MustNumeric() + if math.Mod(num, 1.0) == 0 { + return starlark.MakeInt64(int64(num)), nil + } else { + return starlark.Float(num), nil + } + case ajson.Bool: + return starlark.Bool(node.MustBool()), nil + case ajson.Null: + return starlark.None, nil + default: + return nil, fmt.Errorf("unsupported JSON node type: %v", node.Type()) + } +} diff --git a/lib/json/json_test.go b/lib/json/json_test.go index 924179a6..bc45ac05 100644 --- a/lib/json/json_test.go +++ b/lib/json/json_test.go @@ -358,3 +358,515 @@ func TestLoadModule_JSON(t *testing.T) { }) } } + +func TestJSONPathAndEvalFunctions(t *testing.T) { + const jsonData = ` + { + "store": { + "book": [ + { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, + { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 }, + { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }, + { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } + ], + "bicycle": { "color": "red", "price": 19.95 } + } + }` + + tests := []struct { + name string + script string + want string + wantErr string + }{ + { + name: `stdlib can be loaded`, + script: itn.HereDoc(` + load('json', 'path', 'try_path', 'eval', 'try_eval') + `), + }, + + // path + { + name: "json.path - missing path", + script: itn.HereDoc(` + load('json', 'path') + data = '''` + jsonData + `''' + result = path(data) + `), + wantErr: `json.path: missing argument for path`, + }, + { + name: "json.path - retrieve all prices", + script: itn.HereDoc(` + load('json', 'path') + data = '''` + jsonData + `''' + result = path(data, '$..price') + assert.eq(result, [19.95, 8.95, 12.99, 8.99, 22.99]) + `), + }, + { + name: "json.path - retrieve all book titles", + script: itn.HereDoc(` + load('json', 'path') + data = '''` + jsonData + `''' + result = path(bytes(data), '$.store.book[*].title') + assert.eq(result, ['Sayings of the Century', 'Sword of Honour', 'Moby Dick', 'The Lord of the Rings']) + `), + }, + { + name: "json.path - retrieve non-existent path", + script: itn.HereDoc(` + load('json', 'path') + data = '''` + jsonData + `''' + result = path(data, '$.store.nonexistent') + assert.eq(result, []) + `), + }, + { + name: "json.path - invalid JSON data", + script: itn.HereDoc(` + load('json', 'path') + data = 'invalid json data' + path(data, '$..price') + `), + wantErr: "json.path: wrong symbol 'i' at 0", + }, + { + name: "json.path - data as starlark dict", + script: itn.HereDoc(` + load('json', 'path') + data = { + "a": [1, 2, {"b": 3}], + "c": {"d": 4} + } + result = path(data, '$..b') + assert.eq(result, [3]) + `), + }, + { + name: "json.path - data as starlark list", + script: itn.HereDoc(` + load('json', 'path') + data = [1, 2, {"a": 3, "b": [4, 5]}] + result = path(data, '$..b') + assert.eq(result, [[4, 5]]) + `), + }, + { + name: "json.path - invalid JSONPath expression", + script: itn.HereDoc(` + load('json', 'path') + data = '''` + jsonData + `''' + path(data, '$..[?(@.price > 10)]X') + `), + wantErr: "json.path: wrong symbol 'X' at 20", + }, + { + name: "json.path - JSON data with multiple types", + script: itn.HereDoc(` + load('json', 'path') + data = { + "string": "text", + "number": 42, + "bool": True, + "null": None, + "array": [1, 2, {'key': 'value'}], + "object": {"nested": {"inner": "value"}} + } + result = path(data, '$..*') + assert.eq(result, [[1, 2, {"key": "value"}],True,None,42,{"nested": {"inner": "value"}},"text",1,2,{"key": "value"},{"inner": "value"},"value","value"]) + `), + }, + { + name: "json.path - wildcard and recursive descent", + script: itn.HereDoc(` + load('json', 'path') + data = {'a': {'b': {'c': 1}}, 'd': {'b': {'c': 2}}} + result = path(data, '$..b.c') + assert.eq(result, [1, 2]) + `), + }, + { + name: "json.path - array slicing", + script: itn.HereDoc(` + load('json', 'path') + data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + result = path(data, '$[2:5]') + assert.eq(result, [2,3,4]) + `), + }, + { + name: "json.path - filter expression", + script: itn.HereDoc(` + load('json', 'path') + data = {'items': [{'value': 5}, {'value': 10}, {'value': 15}]} + result = path(data, '$.items[?(@.value > 7)].value') + assert.eq(result, [10, 15]) + `), + }, + { + name: "json.path - parent operator", + script: itn.HereDoc(` + load('json', 'path') + data = {'store': {'book': [{'title': 'A', 'price': 5}, {'title': 'B', 'price': 15}]}} + result = path(data, '$.store.book[?(@.price > 10)]..title') + assert.eq(result, ['B']) + `), + }, + { + name: "json.path - use of '@' current object", + script: itn.HereDoc(` + load('json', 'path') + data = {'items': [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]} + result = path(data, '$.items[?(@.name == "B")].name') + assert.eq(result, ['B']) + `), + }, + { + name: "json.path - access keys with special characters", + script: itn.HereDoc(` + load('json', 'path') + data = {'weird:key': {'another:weird': 42}} + result = path(data, '$["weird:key"]["another:weird"]') + assert.eq(result, [42]) + `), + }, + { + name: "json.path - data as invalid type", + script: itn.HereDoc(` + load('json', 'path') + data = lambda x: x + 5 + path(data, '$ + 5') + `), + wantErr: `json.path: unrecognized starlark type`, + }, + { + name: "json.path - int as key", + script: itn.HereDoc(` + load('json', 'path') + data = { + 42: {"a": 100} + } + result = path(data, '$.42.a') + assert.eq(result, [100]) + `), + }, + { + name: "json.path - broken array expression", + script: itn.HereDoc(` + load('json', 'path') + data = [{"abc": 123}] + result = path(data, '$..length') + assert.eq(result, [1]) + result2 = path(data, '$..$#') + assert.eq(result2, []) + `), + }, + + // eval + { + name: "json.eval - missing expr", + script: itn.HereDoc(` + load('json', 'eval') + data = '''` + jsonData + `''' + result = eval(data) + `), + wantErr: `json.eval: missing argument for expr`, + }, + { + name: "json.eval - average price of all items", + script: itn.HereDoc(` + load('json', 'eval') + data = '''` + jsonData + `''' + result = eval(data, 'avg($..price)') + assert.eq(result, 14.774) + `), + }, + { + name: "json.eval - sum of all book prices", + script: itn.HereDoc(` + load('json', 'eval') + data = '''` + jsonData + `''' + result = eval(data, 'sum($.store.book[*].price)') + assert.eq(result, 53.92) + `), + }, + { + name: "json.eval - invalid expression", + script: itn.HereDoc(` + load('json', 'eval') + data = '''` + jsonData + `''' + eval(data, 'invalid($..price)') + `), + wantErr: "json.eval: wrong request: wrong formula, 'invalid' is not a function", + }, + { + name: "json.eval - division by zero", + script: itn.HereDoc(` + load('json', 'eval') + data = '''` + jsonData + `''' + eval(data, '10 / 0') + `), + wantErr: "json.eval: wrong request: division by zero", + }, + { + name: "json.eval - data as starlark dict", + script: itn.HereDoc(` + load('json', 'eval') + data = { + "numbers": [1, 2, 3, 4, 5] + } + result = eval(data, 'sum($.numbers)') + assert.eq(result, 15) + `), + }, + { + name: "json.eval - division with floating point result", + script: itn.HereDoc(` + load('json', 'eval') + data = '''` + jsonData + `''' + result = eval(data, 'sum($..price) / size($..price)') + assert.eq(result, 14.774) + `), + }, + { + name: "json.eval - expression returning boolean", + script: itn.HereDoc(` + load('json', 'eval') + data = {'value': 10} + result = eval(data, '$.value > 5') + assert.true(result) + `), + }, + { + name: "json.eval - expression returning string", + script: itn.HereDoc(` + load('json', 'eval') + data = {'greeting': 'Hello', 'name': 'World'} + result = eval(data, '$.greeting + ", " + $.name + "!"') + assert.eq(result, 'Hello, World!') + `), + }, + { + name: "json.eval - nested JSONPath in expression", + script: itn.HereDoc(` + load('json', 'eval') + data = {'nums': [1, 2, 3, 4, 5]} + result = eval(data, 'sum($.nums[0:3])') + assert.eq(result, 6) + `), + }, + { + name: "json.eval - accessing non-existent key", + script: itn.HereDoc(` + load('json', 'eval') + data = {'a': 1} + result = eval(data, '$.b') + assert.eq(result, None) + `), + }, + { + name: "json.eval - use of built-in constants", + script: itn.HereDoc(` + load('json', 'eval') + data = {} + result = eval(data, 'pi * 2') + assert.eq(result, 6.283185307179586) + `), + }, + { + name: "json.eval - calling undefined function", + script: itn.HereDoc(` + load('json', 'eval') + data = {} + eval(data, 'undefined_function()') + `), + wantErr: "json.eval: wrong request: wrong formula, 'undefined_function' is not a function", + }, + { + name: "json.eval - invalid syntax in expression", + script: itn.HereDoc(` + load('json', 'eval') + data = {} + eval(data, 'sum(') + `), + wantErr: "json.eval: wrong request: wrong formula, '(' is not an operation or function", + }, + { + name: "json.eval - invalid data", + script: itn.HereDoc(` + load('json', 'eval') + eval('{"a": 123', '$ + 5') + `), + wantErr: "json.eval: unexpected end of file", + }, + { + name: "json.eval - accessing array elements by index", + script: itn.HereDoc(` + load('json', 'eval') + data = {'array': [10, 20, 30]} + result = eval(data, '$.array[1]') + assert.eq(result, 20) + `), + }, + { + name: "json.eval - data as invalid type", + script: itn.HereDoc(` + load('json', 'eval') + data = lambda x: x + 5 + eval(data, '$ + 5') + `), + wantErr: `json.eval: unrecognized starlark type`, + }, + { + name: "json.eval - int as key", + script: itn.HereDoc(` + load('json', 'eval') + data = { + 42: {"a": 100} + } + result = eval(data, '$.42.a') + assert.eq(result, 100) + `), + }, + { + name: "json.eval - broken array expression", + script: itn.HereDoc(` + load('json', 'eval') + data = [{"abc": 123}] + r0 = eval(data, '$..length') + assert.eq(r0, 1) + r1 = eval(data, '$.') + assert.eq(r1, [{"abc": 123}]) + r2 = eval(data, '$..') + assert.eq(r2, [[{"abc": 123}],{"abc": 123}]) + r3 = eval(data, '$...') + assert.eq(r3, [[{"abc": 123}],{"abc": 123},{"abc": 123}]) + `), + }, + + // try path + { + name: "json.try_path - missing path", + script: itn.HereDoc(` + load('json', 'try_path') + data = '''` + jsonData + `''' + result, err = try_path(data) + assert.eq(result, None) + assert.true('json.try_path: missing argument for path' in err) + `), + }, + { + name: "json.try_path - retrieve all prices", + script: itn.HereDoc(` + load('json', 'try_path') + data = '''` + jsonData + `''' + result, err = try_path(data, '$..price') + assert.eq(result, [19.95, 8.95, 12.99, 8.99, 22.99]) + assert.eq(err, None) + `), + }, + { + name: "json.try_path - invalid JSONPath", + script: itn.HereDoc(` + load('json', 'try_path') + data = '''` + jsonData + `''' + result, err = try_path(data, '$..[invalid]') + assert.eq(result, []) + assert.eq(err, None) + `), + }, + { + name: "json.try_path - wrong JSONPath", + script: itn.HereDoc(` + load('json', 'try_path') + data = '''` + jsonData + `''' + result, err = try_path(data, '$..[invalid]X') + assert.eq(result, None) + assert.true("wrong symbol 'X' at 12" in err) + `), + }, + { + name: "json.try_path - data as invalid JSON", + script: itn.HereDoc(` + load('json', 'try_path') + data = '{"invalid": json' + result, err = try_path(data, '$..*') + assert.eq(result, None) + assert.true("wrong symbol 'j' at 12" in err) + `), + }, + { + name: "json.try_path - data as invalid type", + script: itn.HereDoc(` + load('json', 'try_path') + data = lambda x: x + 5 + result, err = try_path(data, '$ + 5') + assert.eq(result, None) + assert.true("unrecognized starlark" in err) + `), + }, + + // try eval + { + name: "json.try_eval - average price of all items", + script: itn.HereDoc(` + load('json', 'try_eval') + data = '''` + jsonData + `''' + result, err = try_eval(data, 'avg($..price)') + assert.eq(result, 14.774) + assert.eq(err, None) + `), + }, + { + name: "json.try_eval - invalid expression", + script: itn.HereDoc(` + load('json', 'try_eval') + data = '''` + jsonData + `''' + result, err = try_eval(data, 'invalid($..price)') + assert.eq(result, None) + assert.true("wrong request: wrong formula, 'invalid' is not a function" in err) + `), + }, + { + name: "json.try_eval - missing expr", + script: itn.HereDoc(` + load('json', 'try_eval') + data = '''` + jsonData + `''' + result, err = try_eval(data) + assert.eq(result, None) + assert.true('json.try_eval: missing argument for expr' in err) + `), + }, + { + name: "json.try_eval - division by zero", + script: itn.HereDoc(` + load('json', 'try_eval') + data = '''` + jsonData + `''' + result, err = try_eval(data, '10 / 0') + assert.eq(result, None) + assert.true("division by zero" in err) + `), + }, + { + name: "json.try_eval - data as invalid type", + script: itn.HereDoc(` + load('json', 'try_eval') + data = lambda x: x + 5 + result, err = try_eval(data, '$ + 5') + assert.eq(result, None) + assert.true("unrecognized starlark" in err) + `), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := itn.ExecModuleWithErrorTest(t, json.ModuleName, json.LoadModule, tt.script, tt.wantErr, nil) + if (err != nil) != (tt.wantErr != "") { + t.Errorf("json(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) + return + } + }) + } +}