Skip to content

Commit

Permalink
wip: only parse if validjsonpath
Browse files Browse the repository at this point in the history
  • Loading branch information
dibyom committed Dec 2, 2019
1 parent c3f580e commit a85fd79
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 71 deletions.
98 changes: 57 additions & 41 deletions pkg/template/jsonpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package template
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
Expand All @@ -17,33 +18,6 @@ var (
jsonRegexp = regexp.MustCompile(`^\{\.?([^{}]+)\}$|^\.?([^{}]+)$`)
)

// RelaxedJSONPathExpression attempts to be flexible with JSONPath expressions, it accepts:
// * metadata.name (no leading '.' or curly braces '{...}'
// * {metadata.name} (no leading '.')
// * .metadata.name (no curly braces '{...}')
// * {.metadata.name} (complete expression)
// And transforms them all into a valid jsonpath expression:
// {.metadata.name}
func relaxedJSONPathExpression(pathExpression string) (string, error) {
if len(pathExpression) == 0 {
return pathExpression, nil
}
submatches := jsonRegexp.FindStringSubmatch(pathExpression)
if submatches == nil {
return "", fmt.Errorf("unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'")
}
if len(submatches) != 3 {
return "", fmt.Errorf("unexpected submatch list: %v", submatches)
}
var fieldSpec string
if len(submatches[1]) != 0 {
fieldSpec = submatches[1]
} else {
fieldSpec = submatches[2]
}
return fmt.Sprintf("{.%s}", fieldSpec), nil
}

func ParseJSONPath(input interface{}, expr string) (string, error) {
j := jsonpath.New("").AllowMissingKeys(false)
buf := new(bytes.Buffer)
Expand All @@ -63,6 +37,7 @@ func ParseJSONPath(input interface{}, expr string) (string, error) {
return "", err
}

// Copy of j.PrintResults
for ix := range fullResults {
if err := printResults(buf, fullResults[ix]); err != nil {
return "", err
Expand All @@ -72,21 +47,9 @@ func ParseJSONPath(input interface{}, expr string) (string, error) {
return buf.String(), nil
}

// TektonJSONPathExpression returns a valid JSONPath expression. It accepts
// a "RelaxedJSONPath" expression that is wrapped in the Tekton variable
// interpolation syntax i.e. $(). RelaxedJSONPath expressions can optionally
// omit the leading curly braces '{}' and '.'
func TektonJSONPathExpression(expr string) (string, error) {
return relaxedJSONPathExpression(removeTektonVar(expr))
}

// removeTektonVar takes an experession of the form $(abc) and returns abc.
func removeTektonVar(expr string) string {
return tektonVar.ReplaceAllString(expr, "$1")
}

// PrintResults writes the results into writer
// This is a copy of the original j.PrintResults
// This is a slightly modified copy of the original
// j.PrintResults from k8s.io/client-go/util/jsonpath/jsonpath.go
// in that it uses the modified evalToText function below
func printResults(wr io.Writer, results []reflect.Value) error {
for i, r := range results {
Expand All @@ -105,6 +68,11 @@ func printResults(wr io.Writer, results []reflect.Value) error {
}

// evalToText translates reflect value to corresponding text
// This is a modified version of evalToText from k8s.io/client-go/util/jsonpath/jsonpath.go
// If the value if an array or map, it returns a JSON representation
// of the value (as opposed to the internal go representation of the value)
// Otherwise, it prints out the value from the original `evalToText`
// This is a workaround for kubernetes/kubernetes#16707
func evalToText(v reflect.Value) ([]byte, error) {
// Type is always an interface
t := reflect.TypeOf(v.Interface())
Expand All @@ -129,3 +97,51 @@ func evalToText(v reflect.Value) ([]byte, error) {
return buffer.Bytes(), nil
}
}

// TektonJSONPathExpression returns a valid JSONPath expression. It accepts
// a "RelaxedJSONPath" expression that is wrapped in the Tekton variable
// interpolation syntax i.e. $(). RelaxedJSONPath expressions can optionally
// omit the leading curly braces '{}' and '.'
func TektonJSONPathExpression(expr string) (string, error) {
if !isTektonExpr(expr) {
return "", errors.New("Expression not wrapped in $()")
}
return relaxedJSONPathExpression(unwrwapTektonExpr(expr))
}

// RelaxedJSONPathExpression attempts to be flexible with JSONPath expressions, it accepts:
// * metadata.name (no leading '.' or curly braces '{...}'
// * {metadata.name} (no leading '.')
// * .metadata.name (no curly braces '{...}')
// * {.metadata.name} (complete expression)
// And transforms them all into a valid jsonpath expression:
// {.metadata.name}
func relaxedJSONPathExpression(pathExpression string) (string, error) {
if len(pathExpression) == 0 {
return pathExpression, nil
}
submatches := jsonRegexp.FindStringSubmatch(pathExpression)
if submatches == nil {
return "", fmt.Errorf("unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'")
}
if len(submatches) != 3 {
return "", fmt.Errorf("unexpected submatch list: %v", submatches)
}
var fieldSpec string
if len(submatches[1]) != 0 {
fieldSpec = submatches[1]
} else {
fieldSpec = submatches[2]
}
return fmt.Sprintf("{.%s}", fieldSpec), nil
}

// unwrwapTektonExpr takes an experession of the form $(abc) and returns abc.
func unwrwapTektonExpr(expr string) string {
return tektonVar.ReplaceAllString(expr, "$1")
}

// IsTektonExpr returns true if the expr is wrapped in $()
func isTektonExpr(expr string) bool {
return tektonVar.MatchString(expr)
}
75 changes: 45 additions & 30 deletions pkg/template/jsonpath_test.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
package template
package template_test

import (
"encoding/json"
"fmt"
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/tektoncd/triggers/pkg/template"
)

var objects = `{"a":"v","c":{"d":"e"}}`
var arrays = `{"a":["simple","array",1],"b":[{"b":"c"},{"d":"e"}]}`

// Checks that we print JSON strings when the expressions selects
// JSON objects or arrays
func TestParseJSONPath_JSONStrings(t *testing.T) {
var objects = `{"a":"v","c":{"d":"e"}}`
var arrays = `{"a":["simple","array",1],"b":[{"b":"c"},{"d":"e"}]}`
tests := []struct {
name string
expr string
in string
want string
wantErr bool
}{{
name: "objects",
in: fmt.Sprintf(`{"body":%s}`, objects),
Expand All @@ -38,51 +38,66 @@ func TestParseJSONPath_JSONStrings(t *testing.T) {
if err != nil {
t.Errorf("Could not unmarshall body : %q", err)
}
got, err := ParseJSONPath(data, tt.expr)
if (err != nil) != tt.wantErr {
t.Errorf("ParseJSONPath() error = %v, wantErr %v", err, tt.wantErr)
got, err := template.ParseJSONPath(data, tt.expr)
if err != nil {
t.Errorf("ParseJSONPath() error = %v", err)
return
}

if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Reflect Deep Equal did not work")
}

if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("ParseJSONPath() -got,+want: %s", diff)
}

fmt.Println("GOT")
fmt.Println(got)
fmt.Println("WANT")
fmt.Println(tt.want)
fmt.Println("")
})
}
}

// Tests for RelaxedJSONPath + TektonJSONPath
//
func TestParseJSONPath_Error(t *testing.T) {
testJSON := `{"body": {"key": "val"}}`
invalidExprs := []string{
"$({.hello)",
"$(+12.3.0)",
"$([1)",
"$(body",
"body)",
"body",
}
var data interface{}
err := json.Unmarshal([]byte(testJSON), &data)
if err != nil {
t.Errorf("Could not unmarshall body : %q", err)
return
}

for _, expr := range invalidExprs {
t.Run(expr, func(t *testing.T) {
got, err := template.ParseJSONPath(data, expr)
if err == nil {
t.Errorf("ParseJSONPath() did not return expected error; got = %v", got)
}
})
}
}

func TestTektonJSONPathExpression(t *testing.T) {
type args struct {
expr string
}
tests := []struct {
name string
args args
expr string
want string
wantErr bool
}{
// TODO: Add test cases.
{"$(metadata.name)", "{.metadata.name}", false},
{"$(.metadata.name)", "{.metadata.name}", false},
{"$({.metadata.name})", "{.metadata.name}", false},
{"$()", "", false},
{"{.metadata.name}", "", true}, // not wrapped in $()
{"", "", true},
{"$({asd)", "", true},
{"$({)", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := TektonJSONPathExpression(tt.args.expr)
t.Run(tt.expr, func(t *testing.T) {
got, err := template.TektonJSONPathExpression(tt.expr)
if (err != nil) != tt.wantErr {
t.Errorf("TektonJSONPathExpression() error = %v, wantErr %v", err, tt.wantErr)
return
t.Errorf("TektonJSONPathExpression() error = %v, wantErr %v, got = %v", err, tt.wantErr, got)
}
if got != tt.want {
t.Errorf("TektonJSONPathExpression() got = %v, want %v", got, tt.want)
Expand Down

0 comments on commit a85fd79

Please sign in to comment.