blob: c5fa34c81cf8fa7a8b040e4ac1709fb7cc25abe0 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package structpath
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
"regexp"
"strings"
)
import (
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"k8s.io/client-go/util/jsonpath"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/util/protomarshal"
)
var (
fixupNumericJSONComparison = regexp.MustCompile(`([=<>]+)\s*([0-9]+)\s*\)`)
fixupAttributeReference = regexp.MustCompile(`\[\s*'[^']+\s*'\s*]`)
)
type Instance struct {
structure interface{}
isJSON bool
constraints []constraint
creationError error
}
type constraint func() error
// ForProto creates a structpath Instance by marshaling the proto to JSON and then evaluating over that
// structure. This is the most generally useful form as serialization to JSON also automatically
// converts proto.Any and proto.Struct to the serialized JSON forms which can then be evaluated
// over. The downside is the loss of type fidelity for numeric types as JSON can only represent
// floats.
func ForProto(proto proto.Message) *Instance {
if proto == nil {
return newErrorInstance(errors.New("expected non-nil proto"))
}
parsed, err := protoToParsedJSON(proto)
if err != nil {
return newErrorInstance(err)
}
i := &Instance{
isJSON: true,
structure: parsed,
}
i.structure = parsed
return i
}
func newErrorInstance(err error) *Instance {
return &Instance{
isJSON: true,
creationError: err,
}
}
func protoToParsedJSON(message proto.Message) (interface{}, error) {
// Convert proto to json and then parse into struct
jsonText, err := protomarshal.MarshalIndent(message, " ")
if err != nil {
return nil, fmt.Errorf("failed to convert proto to JSON: %v", err)
}
var parsed interface{}
err = json.Unmarshal(jsonText, &parsed)
if err != nil {
return nil, fmt.Errorf("failed to parse into JSON struct: %v", err)
}
return parsed, nil
}
func (i *Instance) Select(path string, args ...interface{}) *Instance {
if i.creationError != nil {
// There was an error during the creation of this Instance. Just return the
// same instance since it will error on Check anyway.
return i
}
path = fmt.Sprintf(path, args...)
value, err := i.findValue(path)
if err != nil {
return newErrorInstance(err)
}
if value == nil {
return newErrorInstance(fmt.Errorf("cannot select non-existent path: %v", path))
}
// Success.
return &Instance{
isJSON: i.isJSON,
structure: value,
}
}
func (i *Instance) appendConstraint(fn func() error) *Instance {
i.constraints = append(i.constraints, fn)
return i
}
func (i *Instance) Equals(expected interface{}, path string, args ...interface{}) *Instance {
path = fmt.Sprintf(path, args...)
return i.appendConstraint(func() error {
typeOf := reflect.TypeOf(expected)
protoMessageType := reflect.TypeOf((*proto.Message)(nil)).Elem()
if typeOf.Implements(protoMessageType) {
return i.equalsStruct(expected.(proto.Message), path)
}
switch kind := typeOf.Kind(); kind {
case reflect.String:
return i.equalsString(reflect.ValueOf(expected).String(), path)
case reflect.Bool:
return i.equalsBool(reflect.ValueOf(expected).Bool(), path)
case reflect.Float32, reflect.Float64:
return i.equalsNumber(reflect.ValueOf(expected).Float(), path)
case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64:
return i.equalsNumber(float64(reflect.ValueOf(expected).Int()), path)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return i.equalsNumber(float64(reflect.ValueOf(expected).Uint()), path)
case protoMessageType.Kind():
}
// TODO: Add struct support
return fmt.Errorf("attempt to call Equals for unsupported type: %v", expected)
})
}
func (i *Instance) ContainSubstring(substr, path string) *Instance {
return i.appendConstraint(func() error {
value, err := i.execute(path)
if err != nil {
return err
}
if found := strings.Contains(value, substr); !found {
return fmt.Errorf("substring %v did not match: %v", substr, value)
}
return nil
})
}
func (i *Instance) equalsString(expected string, path string) error {
value, err := i.execute(path)
if err != nil {
return err
}
if value != expected {
return fmt.Errorf("expected %v but got %v for path %v", expected, value, path)
}
return nil
}
func (i *Instance) equalsNumber(expected float64, path string) error {
v, err := i.findValue(path)
if err != nil {
return err
}
result := reflect.ValueOf(v).Float()
if result != expected {
return fmt.Errorf("expected %v but got %v for path %v", expected, result, path)
}
return nil
}
func (i *Instance) equalsBool(expected bool, path string) error {
v, err := i.findValue(path)
if err != nil {
return err
}
result := reflect.ValueOf(v).Bool()
if result != expected {
return fmt.Errorf("expected %v but got %v for path %v", expected, result, path)
}
return nil
}
func (i *Instance) equalsStruct(proto proto.Message, path string) error {
jsonStruct, err := protoToParsedJSON(proto)
if err != nil {
return err
}
v, err := i.findValue(path)
if err != nil {
return err
}
diff := cmp.Diff(reflect.ValueOf(v).Interface(), jsonStruct)
if diff != "" {
return fmt.Errorf("structs did not match: %v", diff)
}
return nil
}
func (i *Instance) Exists(path string, args ...interface{}) *Instance {
path = fmt.Sprintf(path, args...)
return i.appendConstraint(func() error {
v, err := i.findValue(path)
if err != nil {
return err
}
if v == nil {
return fmt.Errorf("no entry exists at path: %v", path)
}
return nil
})
}
func (i *Instance) NotExists(path string, args ...interface{}) *Instance {
path = fmt.Sprintf(path, args...)
return i.appendConstraint(func() error {
parser := jsonpath.New("path")
err := parser.Parse(i.fixPath(path))
if err != nil {
return fmt.Errorf("invalid path: %v - %v", path, err)
}
values, err := parser.AllowMissingKeys(true).FindResults(i.structure)
if err != nil {
return fmt.Errorf("err finding results for path: %v - %v", path, err)
}
if len(values) == 0 {
return nil
}
if len(values[0]) > 0 {
return fmt.Errorf("expected no result but got: %v for path: %v", values[0], path)
}
return nil
})
}
// Check executes the set of constraints for this selection
// and returns the first error encountered, or nil if all constraints
// have been successfully met. All constraints are removed after them
// check is performed.
func (i *Instance) Check() error {
// After the check completes, clear out the constraints.
defer func() {
i.constraints = i.constraints[:0]
}()
// If there was a creation error, just return that immediately.
if i.creationError != nil {
return i.creationError
}
for _, c := range i.constraints {
if err := c(); err != nil {
return err
}
}
return nil
}
// CheckOrFail calls Check on this selection and fails the given test if an
// error is encountered.
func (i *Instance) CheckOrFail(t test.Failer) *Instance {
t.Helper()
if err := i.Check(); err != nil {
t.Fatal(err)
}
return i
}
func (i *Instance) execute(path string) (string, error) {
parser := jsonpath.New("path")
err := parser.Parse(i.fixPath(path))
if err != nil {
return "", fmt.Errorf("invalid path: %v - %v", path, err)
}
buf := new(bytes.Buffer)
err = parser.Execute(buf, i.structure)
if err != nil {
return "", fmt.Errorf("err finding results for path: %v - %v", path, err)
}
return buf.String(), nil
}
func (i *Instance) findValue(path string) (interface{}, error) {
parser := jsonpath.New("path")
err := parser.Parse(i.fixPath(path))
if err != nil {
return nil, fmt.Errorf("invalid path: %v - %v", path, err)
}
values, err := parser.FindResults(i.structure)
if err != nil {
return nil, fmt.Errorf("err finding results for path: %v: %v. Structure: %v", path, err, i.structure)
}
if len(values) == 0 || len(values[0]) == 0 {
return nil, fmt.Errorf("no value for path: %v", path)
}
return values[0][0].Interface(), nil
}
// Fixes up some quirks in jsonpath handling.
// See https://github.com/kubernetes/client-go/issues/553
func (i *Instance) fixPath(path string) string {
// jsonpath doesn't handle numeric comparisons in a tolerant way. All json numbers are floats
// and filter expressions on the form {.x[?(@.some.value==123]} won't work but
// {.x[?(@.some.value==123.0]} will.
result := path
if i.isJSON {
template := "$1$2.0)"
result = fixupNumericJSONComparison.ReplaceAllString(path, template)
}
// jsonpath doesn't like map literal references that contain periods. I.e
// you can't do x['user.map'] but x.user\.map works so we just translate to that
result = string(fixupAttributeReference.ReplaceAllFunc([]byte(result), func(i []byte) []byte {
input := string(i)
input = strings.Replace(input, "[", "", 1)
input = strings.Replace(input, "]", "", 1)
input = strings.Replace(input, "'", "", 2)
parts := strings.Split(input, ".")
output := "."
for i := 0; i < len(parts)-1; i++ {
output += parts[i]
output += "\\."
}
output += parts[len(parts)-1]
return []byte(output)
}))
return result
}