Skip to content

Commit 5af5b11

Browse files
authored
jsonpath (#118)
* feat: add keyValToMap and MapToKeyVal * feat: add jsonpath and jmespath functions * fix: nil handling of jq/jsonpath/jmespath * chore: test fixes * chore: fix typo
1 parent 8c73150 commit 5af5b11

File tree

13 files changed

+307
-11
lines changed

13 files changed

+307
-11
lines changed

coll/coll.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
package coll
77

88
import (
9+
"bytes"
10+
"errors"
911
"fmt"
1012
"reflect"
1113
"sort"
14+
"strings"
1215

1316
"github.com/flanksource/gomplate/v3/conv"
1417
iconv "github.com/flanksource/gomplate/v3/internal/conv"
@@ -340,3 +343,30 @@ func Flatten(list interface{}, depth int) ([]interface{}, error) {
340343
}
341344
return out, nil
342345
}
346+
347+
func MapToKeyVal[T string | any | interface{}](m map[string]T) string {
348+
var buf bytes.Buffer
349+
for k, v := range m {
350+
if buf.Len() > 0 {
351+
buf.WriteByte(',')
352+
353+
}
354+
buf.WriteString(k)
355+
buf.WriteByte('=')
356+
buf.WriteString(fmt.Sprintf("%v", v))
357+
}
358+
return buf.String()
359+
}
360+
361+
func KeyValToMap(s string) (map[string]string, error) {
362+
m := make(map[string]string)
363+
for _, kv := range strings.Split(s, ",") {
364+
kv = strings.TrimSpace(kv)
365+
parts := strings.Split(kv, "=")
366+
if len(parts) != 2 {
367+
return nil, errors.New("invalid input string")
368+
}
369+
m[parts[0]] = parts[1]
370+
}
371+
return m, nil
372+
}

coll/coll_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,17 @@ func TestPick(t *testing.T) {
616616

617617
assert.EqualValues(t, in, Pick(in, "foo", "bar", ""))
618618
}
619+
620+
func TestKeyValToMap(t *testing.T) {
621+
in := "foo=bar,bar=true"
622+
expected := map[string]string{
623+
"foo": "bar",
624+
"bar": "true",
625+
}
626+
result, err := KeyValToMap(in)
627+
assert.NoError(t, err)
628+
assert.EqualValues(t, expected, result)
629+
out := MapToKeyVal(expected)
630+
assert.EqualValues(t, in, out)
631+
632+
}

coll/jq.go renamed to coll/json.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import (
77
"reflect"
88

99
"github.com/itchyny/gojq"
10+
"github.com/jmespath/go-jmespath"
11+
"github.com/ohler55/ojg/jp"
1012
)
1113

14+
const NullValue = "NULL_VALUE"
15+
1216
// JQ -
1317
func JQ(ctx context.Context, jqExpr string, in interface{}) (interface{}, error) {
1418
query, err := gojq.Parse(jqExpr)
@@ -52,6 +56,63 @@ func JQ(ctx context.Context, jqExpr string, in interface{}) (interface{}, error)
5256
return out, nil
5357
}
5458

59+
func JMESPath(jmesPath string, in interface{}) (interface{}, error) {
60+
// convert input to a supported type, if necessary
61+
in, err := jqConvertType(in)
62+
if err != nil {
63+
return nil, fmt.Errorf("type conversion: %w", err)
64+
}
65+
66+
if inString, ok := in.(string); ok {
67+
var v map[string]any
68+
if err := json.Unmarshal([]byte(inString), &v); err == nil {
69+
in = v
70+
}
71+
}
72+
out, err := jmespath.Search(jmesPath, in)
73+
74+
if err != nil {
75+
return nil, fmt.Errorf("%+w", err)
76+
}
77+
if out == nil || out == NullValue || out == "" {
78+
out = ""
79+
}
80+
81+
return out, nil
82+
}
83+
84+
func JSONPath(jsonPath string, in interface{}) (interface{}, error) {
85+
// convert input to a supported type, if necessary
86+
in, err := jqConvertType(in)
87+
if err != nil {
88+
return nil, fmt.Errorf("type conversion: %w", err)
89+
}
90+
91+
if inString, ok := in.(string); ok {
92+
var v map[string]any
93+
if err := json.Unmarshal([]byte(inString), &v); err == nil {
94+
in = v
95+
}
96+
}
97+
98+
x, err := jp.ParseString(jsonPath)
99+
if err != nil {
100+
return nil, err
101+
}
102+
out := x.Get(in)
103+
104+
if len(out) == 1 {
105+
if out[0] == NullValue || out[0] == nil {
106+
return "", nil
107+
}
108+
return out[0], nil
109+
}
110+
if len(out) == 0 {
111+
return "", nil
112+
}
113+
return out, nil
114+
}
115+
55116
func isSupportableType(in interface{}) bool {
56117
switch in.(type) {
57118
case map[string]interface{},
File renamed without changes.

funcs/cel_exports.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

funcs/coll.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func CreateCollFuncs(ctx context.Context) map[string]interface{} {
4343
f["sort"] = ns.Sort
4444
f["jq"] = ns.JQ
4545
f["flatten"] = ns.Flatten
46+
f["mapToKeyVal"] = coll.MapToKeyVal[any]
47+
f["keyValToMap"] = coll.KeyValToMap
48+
f["jsonpath"] = coll.JSONPath
49+
f["jmespath"] = coll.JMESPath
4650
return f
4751
}
4852

funcs/coll_gen.go

Lines changed: 104 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ require (
6767
github.com/inconshreveable/mousetrap v1.1.0 // indirect
6868
github.com/itchyny/timefmt-go v0.1.6 // indirect
6969
github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603 // indirect
70+
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect
7071
github.com/json-iterator/go v1.1.12 // indirect
7172
github.com/kr/pretty v0.3.1 // indirect
7273
github.com/kr/text v0.2.0 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/my
8484
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
8585
github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603 h1:gSech9iGLFCosfl/DC7BWnpSSh/tQClWnKS2I2vdPww=
8686
github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
87+
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY=
88+
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
89+
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
8790
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
8891
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
8992
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=

template.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/robertkrimen/otto/registry"
2424
_ "github.com/robertkrimen/otto/underscore"
2525
"github.com/samber/oops"
26+
"google.golang.org/protobuf/types/known/structpb"
2627
)
2728

2829
var funcMap gotemplate.FuncMap
@@ -265,6 +266,9 @@ func RunTemplateContext(ctx commonsContext.Context, environment map[string]any,
265266
if err != nil {
266267
return "", err
267268
}
269+
if _, ok := out.(structpb.NullValue); ok || out == nil {
270+
return "", nil
271+
}
268272
return fmt.Sprintf("%v", out), nil
269273
}
270274

tests/cel_test.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func TestCelColl(t *testing.T) {
117117
{nil, `Dict(['a','b', 'c', 'd']).c`, "d"},
118118
{nil, `Has(['a','b', 'c'], 'a')`, "true"},
119119
{nil, `Has(['a','b', 'c'], 'e')`, "false"},
120+
{map[string]interface{}{"kv": "a=b,c=d"}, "keyValToMap(kv).a", "b"},
120121
})
121122
}
122123

@@ -170,10 +171,6 @@ func TestCelFilePath(t *testing.T) {
170171
}
171172

172173
func TestCelJSON(t *testing.T) {
173-
person := Person{
174-
Name: "Aditya",
175-
Address: &Address{City: "Kathmandu"},
176-
}
177174

178175
personJSONString, _ := json.Marshal(person)
179176

@@ -183,19 +180,28 @@ func TestCelJSON(t *testing.T) {
183180
{nil, `dyn({'name': 'John'}).toJSON()`, `{"name":"John"}`},
184181
{nil, `{'name': 'John'}.toJSON()`, `{"name":"John"}`},
185182
{nil, `1.toJSON()`, `1`},
186-
{map[string]interface{}{"i": person}, "i.toJSON().JSON().name", "Aditya"},
183+
{map[string]interface{}{"i": person}, "i.toJSON().JSON().name", "John Doe"},
187184
{map[string]interface{}{"i": person}, `'["1", "2"]'.JSONArray()[0]`, "1"},
188185
{map[string]interface{}{"i": map[string]string{"name": "aditya"}}, `i.toJSON()`, `{"name":"aditya"}`},
189186

190187
{nil, `'{"name": "John"}'.JSON().name`, `John`},
191188
{nil, `'{"name": "Alice", "age": 30}'.JSON().name`, `Alice`},
192189
{nil, `'[1, 2, 3, 4, 5]'.JSONArray()[0]`, `1`},
193-
{map[string]interface{}{"i": person}, "i.toJSONPretty('\t')", "{\n\t\"Address\": {\n\t\t\"city_name\": \"Kathmandu\"\n\t},\n\t\"name\": \"Aditya\"\n}"},
194-
{nil, "[\"Alice\", 30].toJSONPretty('\t')", "[\n\t\"Alice\",\n\t30\n]"},
190+
{map[string]interface{}{"i": person}, "i.toJSONPretty('\t').JSON().addresses[0].country", "Nepal"},
195191
{nil, "{'name': 'aditya'}.toJSONPretty('\t')", "{\n\t\"name\": \"aditya\"\n}"},
196192

197-
// JQ
193+
{map[string]interface{}{"i": person}, "jsonpath('$.addresses[-1:].city_name', i)", "New York"},
194+
{map[string]interface{}{"i": person}, "jmespath('addresses[*].city_name | [0]', i)", "Kathmandu"},
195+
//FIXME: jmespath function return a parse error
196+
{map[string]interface{}{"i": person}, "jmespath('length(addresses)', i)", "3"},
197+
{map[string]interface{}{"i": person}, "jmespath('ceil(`1.2`)', i)", "2"},
198+
//FIXME: jmespath always returns a list
199+
{map[string]interface{}{"i": person}, "jmespath('name', i)", "John Doe"},
200+
{map[string]interface{}{"i": person}, "jmespath('Address.country', i)", ""},
201+
{map[string]interface{}{"i": person}, "jsonpath('Address.country', i)", ""},
202+
198203
{map[string]interface{}{"i": person}, "jq('.Address.city_name', i)", "Kathmandu"},
204+
{map[string]interface{}{"i": person}, "jq('.Address.country', i)", ""},
199205
{map[string]interface{}{"i": personJSONString}, "jq('.Address.city_name', i)", "Kathmandu"},
200206
})
201207
}
@@ -313,9 +319,9 @@ func TestCelMaps(t *testing.T) {
313319
{m, "x.a", "b"},
314320
{m, "x.c", "1"},
315321
{m, "x.d", "true"},
316-
{m, "x.?e", "<nil>"},
322+
{m, "x.?e", ""},
317323
{m, "x.?f.?a", "5"},
318-
{m, "x.?f.?b", "<nil>"},
324+
{m, "x.?f.?b", ""},
319325
{nil, "{'a': 'c'}.merge({'b': 'd'}).keys().join(',')", "a,b"},
320326
{nil, "{'a': '1', 'b': '2', 'c': '3'}.pick(['a', 'c']).keys()", "[a c]"},
321327
{nil, "{'a': '1', 'b': '2', 'c': '3'}.omit(['b']).keys()", "[a c]"},

0 commit comments

Comments
 (0)