Allow missing keys in jsonpath

It is common in constrained circumstances to prefer an empty string
result from JSONPath templates for missing keys over an error. Several
other implementations provide this (the canonical JS and PHP, as well as
the Java implementation).  This also mirrors gotemplate, which allows
Options("missingkey=zero").

Added simple check and simple test case.
pull/6/head
Clayton Coleman 2016-08-30 12:50:51 -04:00
parent 1dfd6ab0c1
commit bcea2c8a4e
No known key found for this signature in database
GPG Key ID: 3D16906B4F1C5CB3
2 changed files with 24 additions and 6 deletions

View File

@ -34,6 +34,8 @@ type JSONPath struct {
beginRange int beginRange int
inRange int inRange int
endRange int endRange int
allowMissingKeys bool
} }
func New(name string) *JSONPath { func New(name string) *JSONPath {
@ -45,6 +47,13 @@ func New(name string) *JSONPath {
} }
} }
// AllowMissingKeys allows a caller to specify whether they want an error if a field or map key
// cannot be located, or simply an empty result. The receiver is returned for chaining.
func (j *JSONPath) AllowMissingKeys(allow bool) *JSONPath {
j.allowMissingKeys = allow
return j
}
// Parse parse the given template, return error // Parse parse the given template, return error
func (j *JSONPath) Parse(text string) (err error) { func (j *JSONPath) Parse(text string) (err error) {
j.parser, err = Parse(j.name, text) j.parser, err = Parse(j.name, text)
@ -305,7 +314,7 @@ func (j *JSONPath) findFieldInValue(value *reflect.Value, node *FieldNode) (refl
return value.FieldByName(node.Value), nil return value.FieldByName(node.Value), nil
} }
// evalField evaluates filed of struct or key of map. // evalField evaluates field of struct or key of map.
func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.Value, error) { func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.Value, error) {
results := []reflect.Value{} results := []reflect.Value{}
// If there's no input, there's no output // If there's no input, there's no output
@ -338,6 +347,9 @@ func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.
} }
} }
if len(results) == 0 { if len(results) == 0 {
if j.allowMissingKeys {
return results, nil
}
return results, fmt.Errorf("%s is not found", node.Value) return results, fmt.Errorf("%s is not found", node.Value)
} }
return results, nil return results, nil

View File

@ -33,9 +33,10 @@ type jsonpathTest struct {
expect string expect string
} }
func testJSONPath(tests []jsonpathTest, t *testing.T) { func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) {
for _, test := range tests { for _, test := range tests {
j := New(test.name) j := New(test.name)
j.AllowMissingKeys(allowMissingKeys)
err := j.Parse(test.template) err := j.Parse(test.template)
if err != nil { if err != nil {
t.Errorf("in %s, parse %s error %v", test.name, test.template, err) t.Errorf("in %s, parse %s error %v", test.name, test.template, err)
@ -166,10 +167,15 @@ func TestStructInput(t *testing.T) {
{"recurarray", "{..Book[2]}", storeData, {"recurarray", "{..Book[2]}", storeData,
"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}"}, "{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}"},
} }
testJSONPath(storeTests, t) testJSONPath(storeTests, false, t)
missingKeyTests := []jsonpathTest{
{"nonexistent field", "{.hello}", storeData, ""},
}
testJSONPath(missingKeyTests, true, t)
failStoreTests := []jsonpathTest{ failStoreTests := []jsonpathTest{
{"invalid identfier", "{hello}", storeData, "unrecognized identifier hello"}, {"invalid identifier", "{hello}", storeData, "unrecognized identifier hello"},
{"nonexistent field", "{.hello}", storeData, "hello is not found"}, {"nonexistent field", "{.hello}", storeData, "hello is not found"},
{"invalid array", "{.Labels[0]}", storeData, "map[string]int is not array or slice"}, {"invalid array", "{.Labels[0]}", storeData, "map[string]int is not array or slice"},
{"invalid filter operator", "{.Book[?(@.Price<>10)]}", storeData, "unrecognized filter operator <>"}, {"invalid filter operator", "{.Book[?(@.Price<>10)]}", storeData, "unrecognized filter operator <>"},
@ -196,7 +202,7 @@ func TestJSONInput(t *testing.T) {
{"exists filter", "{[?(@.z)].id}", pointsData, "i2 i5"}, {"exists filter", "{[?(@.z)].id}", pointsData, "i2 i5"},
{"bracket key", "{[0]['id']}", pointsData, "i1"}, {"bracket key", "{[0]['id']}", pointsData, "i1"},
} }
testJSONPath(pointsTests, t) testJSONPath(pointsTests, false, t)
} }
// TestKubernetes tests some use cases from kubernetes // TestKubernetes tests some use cases from kubernetes
@ -255,7 +261,7 @@ func TestKubernetes(t *testing.T) {
"[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]] "}, "[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]] "},
{"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret"}, {"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret"},
} }
testJSONPath(nodesTests, t) testJSONPath(nodesTests, false, t)
randomPrintOrderTests := []jsonpathTest{ randomPrintOrderTests := []jsonpathTest{
{"recursive name", "{..name}", nodesData, `127.0.0.1 127.0.0.2 myself e2e`}, {"recursive name", "{..name}", nodesData, `127.0.0.1 127.0.0.2 myself e2e`},