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.
parent 1dfd6ab0
...@@ -34,6 +34,8 @@ type JSONPath struct { ...@@ -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 { ...@@ -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 ...@@ -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. ...@@ -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
......
...@@ -33,9 +33,10 @@ type jsonpathTest struct { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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`},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment