Skip to content

Commit

Permalink
Merge pull request #535 from hairyhenderson/jsonpath
Browse files Browse the repository at this point in the history
Adding coll.JSONPath function
  • Loading branch information
hairyhenderson authored Apr 10, 2019
2 parents 0863119 + 51ea378 commit 56fbcc8
Show file tree
Hide file tree
Showing 14 changed files with 2,518 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions coll/jsonpath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package coll

import (
"reflect"

"github.com/pkg/errors"
"k8s.io/client-go/util/jsonpath"
)

// JSONPath -
func JSONPath(p string, in interface{}) (interface{}, error) {
jp, err := parsePath(p)
if err != nil {
return nil, errors.Wrapf(err, "couldn't parse JSONPath %s", p)
}
results, err := jp.FindResults(in)
if err != nil {
return nil, errors.Wrap(err, "executing JSONPath failed")
}

var out interface{}
if len(results) == 1 && len(results[0]) == 1 {
v := results[0][0]
out, err = extractResult(v)
if err != nil {
return nil, err
}
} else {
a := []interface{}{}
for _, r := range results {
for _, v := range r {
o, err := extractResult(v)
if err != nil {
return nil, err
}
if o != nil {
a = append(a, o)
}
}
}
out = a
}

return out, nil
}

func parsePath(p string) (*jsonpath.JSONPath, error) {
jp := jsonpath.New("<jsonpath>")
err := jp.Parse("{" + p + "}")
if err != nil {
return nil, err
}
jp.AllowMissingKeys(false)
return jp, nil
}

func extractResult(v reflect.Value) (interface{}, error) {
if v.CanInterface() {
return v.Interface(), nil
}

return nil, errors.Errorf("JSONPath couldn't access field")
}
146 changes: 146 additions & 0 deletions coll/jsonpath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package coll

import (
"testing"

"github.com/stretchr/testify/assert"
)

type m = map[string]interface{}
type ar = []interface{}

func TestJSONPath(t *testing.T) {
in := m{
"store": m{
"book": ar{
m{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95,
},
m{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99,
},
m{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99,
},
m{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99,
},
},
"bicycle": m{
"color": "red",
"price": 19.95,
},
},
}
out, err := JSONPath(".store.bicycle.color", in)
assert.NoError(t, err)
assert.Equal(t, "red", out)

out, err = JSONPath(".store.bicycle.price", in)
assert.NoError(t, err)
assert.Equal(t, 19.95, out)

_, err = JSONPath(".store.bogus", in)
assert.Error(t, err)

_, err = JSONPath("{.store.unclosed", in)
assert.Error(t, err)

out, err = JSONPath(".store", in)
assert.NoError(t, err)
assert.EqualValues(t, in["store"], out)

out, err = JSONPath("$.store.book[*].author", in)
assert.NoError(t, err)
assert.Len(t, out, 4)
assert.Contains(t, out, "Nigel Rees")
assert.Contains(t, out, "Evelyn Waugh")
assert.Contains(t, out, "Herman Melville")
assert.Contains(t, out, "J. R. R. Tolkien")

out, err = JSONPath("$..book[?( @.price < 10.0 )]", in)
assert.NoError(t, err)
expected := ar{
m{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95,
},
m{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99,
},
}
assert.EqualValues(t, expected, out)

in = m{
"a": m{
"aa": m{
"foo": m{
"aaa": m{
"aaaa": m{
"bar": 1234,
},
},
},
},
"ab": m{
"aba": m{
"foo": m{
"abaa": true,
"abab": "baz",
},
},
},
},
}
out, err = JSONPath("..foo.*", in)
assert.NoError(t, err)
assert.Len(t, out, 3)
assert.Contains(t, out, m{"aaaa": m{"bar": 1234}})
assert.Contains(t, out, true)
assert.Contains(t, out, "baz")

type bicycleType struct {
Color string
}
type storeType struct {
Bicycle *bicycleType
safe interface{}
}

structIn := &storeType{
Bicycle: &bicycleType{
Color: "red",
},
safe: "hidden",
}

out, err = JSONPath(".Bicycle.Color", structIn)
assert.NoError(t, err)
assert.Equal(t, "red", out)

_, err = JSONPath(".safe", structIn)
assert.Error(t, err)

_, err = JSONPath(".*", structIn)
assert.Error(t, err)
}
22 changes: 22 additions & 0 deletions docs-src/content/functions/coll.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ funcs:
$ gomplate -i '{{ $o := data.JSON (getenv "DATA") -}}
{{ if (has $o "foo") }}{{ $o.foo }}{{ else }}THERE IS NO FOO{{ end }}'
THERE IS NO FOO
- name: conv.JSONPath
alias: jsonpath
description: |
Extracts portions of an input object or list using a [JSONPath][] expression.
Any object or list may be used as input. The output depends somewhat on the expression; if multiple items are matched, an array is returned.
JSONPath expressions can be validated at https://jsonpath.com
[JSONPath]: https://goessner.net/articles/JsonPath
pipeline: true
arguments:
- name: expression
required: true
description: The JSONPath expression
- name: in
required: true
description: The object or list to query
examples:
- |
$ gomplate -i '{{ .books | jsonpath `$..works[?( @.edition_count > 400 )].title` }}' -c books=https://openlibrary.org/subjects/fantasy.json
[Alice's Adventures in Wonderland Gulliver's Travels]
- name: coll.Keys
alias: keys
description: |
Expand Down
35 changes: 35 additions & 0 deletions docs/content/functions/coll.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,41 @@ $ gomplate -i '{{ $o := data.JSON (getenv "DATA") -}}
THERE IS NO FOO
```

## `conv.JSONPath`

**Alias:** `jsonpath`

Extracts portions of an input object or list using a [JSONPath][] expression.

Any object or list may be used as input. The output depends somewhat on the expression; if multiple items are matched, an array is returned.

JSONPath expressions can be validated at https://jsonpath.com

[JSONPath]: https://goessner.net/articles/JsonPath

### Usage

```go
conv.JSONPath expression in
```
```go
in | conv.JSONPath expression
```

### Arguments

| name | description |
|------|-------------|
| `expression` | _(required)_ The JSONPath expression |
| `in` | _(required)_ The object or list to query |

### Examples

```console
$ gomplate -i '{{ .books | jsonpath `$..works[?( @.edition_count > 400 )].title` }}' -c books=https://openlibrary.org/subjects/fantasy.json
[Alice's Adventures in Wonderland Gulliver's Travels]
```

## `coll.Keys`

**Alias:** `keys`
Expand Down
6 changes: 6 additions & 0 deletions funcs/coll.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func AddCollFuncs(f map[string]interface{}) {
f["reverse"] = CollNS().Reverse
f["merge"] = CollNS().Merge
f["sort"] = CollNS().Sort
f["jsonpath"] = CollNS().JSONPath
}

// CollFuncs -
Expand Down Expand Up @@ -108,3 +109,8 @@ func (f *CollFuncs) Sort(args ...interface{}) ([]interface{}, error) {
}
return coll.Sort(key, list)
}

// JSONPath -
func (f *CollFuncs) JSONPath(p string, in interface{}) (interface{}, error) {
return coll.JSONPath(p, in)
}
12 changes: 12 additions & 0 deletions tests/integration/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,15 @@ func (s *CollSuite) TestSort(c *C) {
`,
})
}

func (s *CollSuite) TestJSONPath(c *C) {
result := icmd.RunCmd(icmd.Command(GomplateBin,
"-c", "config="+s.tmpDir.Join("config.json"),
"-i", `{{ .config | jsonpath ".*.three" }}`))
result.Assert(c, icmd.Expected{ExitCode: 0, Out: `[5 6 7]`})

result = icmd.RunCmd(icmd.Command(GomplateBin,
"-c", "config="+s.tmpDir.Join("config.json"),
"-i", `{{ .config | coll.JSONPath ".values..a" }}`))
result.Assert(c, icmd.Expected{ExitCode: 0, Out: `eh?`})
}
Loading

0 comments on commit 56fbcc8

Please sign in to comment.