-
-
Notifications
You must be signed in to change notification settings - Fork 110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add support to MatchV2 structs generated by proto-gen-go #262
Draft
hborham
wants to merge
1
commit into
pact-foundation:2.x.x
Choose a base branch
from
hborham:feature/proto-gen-go
base: 2.x.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+242
−5
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
package extensions | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"reflect" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/pact-foundation/pact-go/v2/matchers" | ||
"github.com/pkg/errors" | ||
|
||
"google.golang.org/protobuf/reflect/protoreflect" | ||
"google.golang.org/protobuf/reflect/protoregistry" | ||
) | ||
|
||
var ( | ||
Like = matchers.Like | ||
EachLike = matchers.EachLike | ||
Term = matchers.Term | ||
) | ||
|
||
type ( | ||
StructMatcher = matchers.StructMatcher | ||
Matcher = matchers.Matcher | ||
) | ||
|
||
func TestMatch(t *testing.T) { | ||
// mixedDTO in order to reuse protoc-gen-go where structs are compatible with protobuf and json | ||
type mixedDTO struct { | ||
// has tag and should be in output | ||
OnlyJsonTag string `json:"onlyJsonTag"` | ||
// no tag, skip | ||
NoTagString string | ||
// no tag, skip - this covers case of proto compatible structs that contain func fields | ||
NoTagFunc func() | ||
BothUseJsonTag int32 `protobuf:"varint,1,opt,name=both_use_json_tag,json=bothNameFromProtobufTag,proto3" json:"bothNameFromJsonTag,omitempty"` | ||
ProtoWithoutJsonTag *struct { | ||
OnlyJsonTag string `json:"onlyJsonTagNested"` | ||
// no tag, skip | ||
NoTag func() | ||
} `protobuf:"bytes,7,opt,name=proto_without_json_tag,json=onlyProtobufTag,proto3,oneof"` | ||
} | ||
type args struct { | ||
src interface{} | ||
} | ||
type matchTest struct { | ||
name string | ||
args args | ||
want Matcher | ||
wantPanic bool | ||
} | ||
defaultTests := []matchTest{ | ||
{ | ||
name: "base case - string", | ||
args: args{ | ||
src: "string", | ||
}, | ||
want: Like("string"), | ||
}, | ||
{ | ||
name: "base case - bool", | ||
args: args{ | ||
src: true, | ||
}, | ||
want: Like(true), | ||
}, | ||
{ | ||
name: "base case - int", | ||
args: args{ | ||
src: 1, | ||
}, | ||
want: Like(1), | ||
}, | ||
{ | ||
name: "base case - uint", | ||
args: args{ | ||
src: uint(1), | ||
}, | ||
want: Like(1), | ||
}, | ||
{ | ||
name: "base case - float32", | ||
args: args{ | ||
src: float32(1), | ||
}, | ||
want: Like(1.1), | ||
}, | ||
{ | ||
name: "error - unhandled type", | ||
args: args{ | ||
src: make(map[string]string), | ||
}, | ||
wantPanic: true, | ||
}, | ||
} | ||
|
||
matchV2ProtoTests := append(defaultTests, matchTest{ | ||
name: "structs mixed for compatibility with proto3 and json types", | ||
args: args{ | ||
src: mixedDTO{}, | ||
}, | ||
want: StructMatcher{ | ||
"onlyJsonTag": Like("string"), | ||
"bothNameFromJsonTag": Like(1), | ||
"onlyProtobufTag": StructMatcher{"onlyJsonTagNested": Like("string")}, | ||
}, | ||
}) | ||
customMatcher := matchers.NewCustomMatchStructV2(matchers.CustomMatchStructV2Args{StrategyFunc: protoJsonFieldStrategyFunc}) | ||
for _, tt := range matchV2ProtoTests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
var got Matcher | ||
var didPanic bool | ||
defer func() { | ||
if rec := recover(); rec != nil { | ||
fmt.Println(rec) | ||
didPanic = true | ||
} | ||
if tt.wantPanic != didPanic { | ||
t.Errorf("Match() - '%s': didPanic = %v, want %v", tt.name, didPanic, tt.wantPanic) | ||
} else if !didPanic && !reflect.DeepEqual(got, tt.want) { | ||
t.Errorf("Match() - '%s': = %v, want %v", tt.name, got, tt.want) | ||
} | ||
}() | ||
|
||
got = customMatcher.MatchV2(tt.args.src) | ||
log.Println("Got matcher: ", got) | ||
}) | ||
} | ||
} | ||
|
||
// ProtoJsonFieldStrategyFunc provides an example to extend the matchers.CustomMatchStructV2Args.Match | ||
// extension allows for parsing of custom structs tags, and this example demonstrates a | ||
// potential solution relating to protojson | ||
var protoJsonFieldStrategyFunc = func(field reflect.StructField) matchers.FieldMatchArgs { | ||
if fieldName, enum := fieldNameByTagStrategy(field); enum != "" { | ||
var pactTag string | ||
if _, ok := field.Tag.Lookup("pact"); ok { | ||
pactTag = field.Tag.Get("pact") | ||
} else { | ||
pactTag = generateDefaultTagForEnum(enum) | ||
} | ||
return matchers.FieldMatchArgs{Name: fieldName, MatchType: reflect.TypeOf("string"), PactTag: pactTag} | ||
} else if fieldName != "" { | ||
return matchers.FieldMatchArgs{Name: fieldName, MatchType: field.Type, PactTag: field.Tag.Get("pact")} | ||
} else { | ||
return matchers.DefaultFieldStrategyFunc(field) | ||
} | ||
} | ||
|
||
func fieldNameByTagStrategy(field reflect.StructField) (fieldName string, enum string) { | ||
var v string | ||
var ok bool | ||
if v, ok = field.Tag.Lookup("protobuf"); ok { | ||
arr := strings.Split(v, ",") | ||
for i := 0; i < len(arr); i++ { | ||
if strings.HasPrefix(arr[i], "json=") { | ||
fieldName = strings.Split(arr[i], "=")[1] | ||
} | ||
if strings.HasPrefix(arr[i], "enum=") { | ||
enum = strings.Split(arr[i], "=")[1] | ||
} | ||
} | ||
} | ||
|
||
if v, ok = field.Tag.Lookup("json"); ok { | ||
fieldName = strings.Split(v, ",")[0] | ||
} | ||
return fieldName, enum | ||
} | ||
|
||
func generateDefaultTagForEnum(enum string) string { | ||
var enumType protoreflect.EnumType | ||
var err error | ||
var example, regex string | ||
|
||
if enumType, err = protoregistry.GlobalTypes.FindEnumByName(protoreflect.FullName(enum)); err != nil { | ||
panic(errors.Wrapf(err, "could not find enum %s", enum)) | ||
} | ||
|
||
values := enumType.Descriptor().Values() | ||
enumNames := make([]string, 0) | ||
for i := 0; i < values.Len(); i++ { | ||
enumNames = append(enumNames, fmt.Sprintf("%s", values.Get(i).Name())) | ||
} | ||
if len(enumNames) > 0 { | ||
example = enumNames[0] | ||
} | ||
regex = strings.Join(enumNames, "|") | ||
return fmt.Sprintf("example=%s,regex=^(%s)$", example, regex) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package matchers | ||
|
||
import "reflect" | ||
|
||
type CustomMatchStructV2Args struct { | ||
StrategyFunc FieldStrategyFunc | ||
} | ||
|
||
type customMatchStructV2 struct { | ||
matchStruct matchStructV2 | ||
} | ||
|
||
func NewCustomMatchStructV2(args CustomMatchStructV2Args) customMatchStructV2 { | ||
return customMatchStructV2{matchStruct: matchStructV2{args.StrategyFunc}} | ||
} | ||
|
||
func (m *customMatchStructV2) MatchV2(src interface{}) Matcher { | ||
return m.matchStruct.match(reflect.TypeOf(src), getDefaults()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -289,6 +289,26 @@ func objectToString(obj interface{}) string { | |
} | ||
} | ||
|
||
type FieldMatchArgs struct { | ||
Name string | ||
MatchType reflect.Type | ||
PactTag string | ||
} | ||
type FieldStrategyFunc func(field reflect.StructField) FieldMatchArgs | ||
|
||
var DefaultFieldStrategyFunc = func(field reflect.StructField) FieldMatchArgs { | ||
var v, fieldName string | ||
var ok bool | ||
if v, ok = field.Tag.Lookup("json"); ok { | ||
fieldName = strings.Split(v, ",")[0] | ||
} | ||
return FieldMatchArgs{fieldName, field.Type, field.Tag.Get("pact")} | ||
} | ||
|
||
type matchStructV2 struct { | ||
FieldStrategyFunc FieldStrategyFunc | ||
} | ||
|
||
// Match recursively traverses the provided type and outputs a | ||
// matcher string for it that is compatible with the Pact dsl. | ||
// By default, it requires slices to have a minimum of 1 element. | ||
|
@@ -300,23 +320,28 @@ func objectToString(obj interface{}) string { | |
// Minimum Slice Size: `pact:"min=2"` | ||
// String RegEx: `pact:"example=2000-01-01,regex=^\\d{4}-\\d{2}-\\d{2}$"` | ||
func MatchV2(src interface{}) Matcher { | ||
return match(reflect.TypeOf(src), getDefaults()) | ||
m := &matchStructV2{FieldStrategyFunc: DefaultFieldStrategyFunc} | ||
return m.match(reflect.TypeOf(src), getDefaults()) | ||
} | ||
|
||
// match recursively traverses the provided type and outputs a | ||
// matcher string for it that is compatible with the Pact dsl. | ||
func match(srcType reflect.Type, params params) Matcher { | ||
func (m *matchStructV2) match(srcType reflect.Type, params params) Matcher { | ||
switch kind := srcType.Kind(); kind { | ||
case reflect.Ptr: | ||
return match(srcType.Elem(), params) | ||
return m.match(srcType.Elem(), params) | ||
case reflect.Slice, reflect.Array: | ||
return EachLike(match(srcType.Elem(), getDefaults()), params.slice.min) | ||
return EachLike(m.match(srcType.Elem(), getDefaults()), params.slice.min) | ||
case reflect.Struct: | ||
result := StructMatcher{} | ||
|
||
for i := 0; i < srcType.NumField(); i++ { | ||
field := srcType.Field(i) | ||
result[strings.Split(field.Tag.Get("json"), ",")[0]] = match(field.Type, pluckParams(field.Type, field.Tag.Get("pact"))) | ||
args := m.FieldStrategyFunc(field) | ||
if args.Name == "" { | ||
continue | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is kind of a bug currently and now it will get skipped explicitly |
||
} | ||
result[args.Name] = m.match(args.MatchType, pluckParams(args.MatchType, args.PactTag)) | ||
} | ||
return result | ||
case reflect.String: | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extracted your original code as a default strategy and exported so i can fall back on this when needed