From bcea2c8a4efb70dbd4e1cf6dade11a2a74ca5915 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 30 Aug 2016 12:50:51 -0400 Subject: [PATCH] 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. --- pkg/util/jsonpath/jsonpath.go | 14 +++++++++++++- pkg/util/jsonpath/jsonpath_test.go | 16 +++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/pkg/util/jsonpath/jsonpath.go b/pkg/util/jsonpath/jsonpath.go index 0b48b2ed553..d84a1855976 100644 --- a/pkg/util/jsonpath/jsonpath.go +++ b/pkg/util/jsonpath/jsonpath.go @@ -34,6 +34,8 @@ type JSONPath struct { beginRange int inRange int endRange int + + allowMissingKeys bool } 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 func (j *JSONPath) Parse(text string) (err error) { 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 } -// 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) { results := []reflect.Value{} // 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 j.allowMissingKeys { + return results, nil + } return results, fmt.Errorf("%s is not found", node.Value) } return results, nil diff --git a/pkg/util/jsonpath/jsonpath_test.go b/pkg/util/jsonpath/jsonpath_test.go index 8e1532834f3..a85517e6183 100644 --- a/pkg/util/jsonpath/jsonpath_test.go +++ b/pkg/util/jsonpath/jsonpath_test.go @@ -33,9 +33,10 @@ type jsonpathTest struct { expect string } -func testJSONPath(tests []jsonpathTest, t *testing.T) { +func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) { for _, test := range tests { j := New(test.name) + j.AllowMissingKeys(allowMissingKeys) err := j.Parse(test.template) if err != nil { 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, "{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{ - {"invalid identfier", "{hello}", storeData, "unrecognized identifier hello"}, + {"invalid identifier", "{hello}", storeData, "unrecognized identifier hello"}, {"nonexistent field", "{.hello}", storeData, "hello is not found"}, {"invalid array", "{.Labels[0]}", storeData, "map[string]int is not array or slice"}, {"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"}, {"bracket key", "{[0]['id']}", pointsData, "i1"}, } - testJSONPath(pointsTests, t) + testJSONPath(pointsTests, false, t) } // 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]] "}, {"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret"}, } - testJSONPath(nodesTests, t) + testJSONPath(nodesTests, false, t) randomPrintOrderTests := []jsonpathTest{ {"recursive name", "{..name}", nodesData, `127.0.0.1 127.0.0.2 myself e2e`},