From d7f80b75f70f8c0b2b4e2b9e275e112b248c91ad Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Mon, 2 Apr 2018 12:52:06 +1000 Subject: [PATCH] wip: reverse complex, nested Matchers into their generated form --- dsl/matcher.go | 176 ++++++++++++++++++++++++++++++++++++++++++++ dsl/matcher_test.go | 52 +++++++++++++ dsl/pact.go | 1 + 3 files changed, 229 insertions(+) diff --git a/dsl/matcher.go b/dsl/matcher.go index 4bbfd0509..75c6bd971 100644 --- a/dsl/matcher.go +++ b/dsl/matcher.go @@ -2,7 +2,9 @@ package dsl import ( "encoding/json" + "fmt" "log" + "reflect" "time" ) @@ -142,26 +144,200 @@ type StringMatcher interface { // isMatcher is how we tell the compiler that strings // and other types are the same / allowed isMatcher() + + // GetValue returns the raw generated value for the matcher + // without any of the matching detail context + GetValue() interface{} } // S is the string primitive wrapper (alias) for the StringMatcher type, // it allows plain strings to be matched +// To keep backwards compatible with previous versions +// we aren't using an alias here type S string func (s S) isMatcher() {} +// GetValue returns the raw generated value for the matcher +// without any of the matching detail context +func (s S) GetValue() interface{} { + return s +} + // String is the longer named form of the string primitive wrapper, // it allows plain strings to be matched type String string func (s String) isMatcher() {} +// GetValue returns the raw generated value for the matcher +// without any of the matching detail context +func (s String) GetValue() interface{} { + return s +} + // Matcher matches a complex object structure, which may itself // contain nested Matchers type Matcher map[string]interface{} func (m Matcher) isMatcher() {} +// GetValue returns the raw generated value for the matcher +// without any of the matching detail context +func (m Matcher) GetValue() interface{} { + class, ok := m["json_class"] + + if !ok { + return nil + } + + // extract out the value + switch class { + case "Pact::ArrayLike": + contents := m["contents"] + min := m["min"].(int) + data := make([]interface{}, min) + + for i := 0; i < min; i++ { + data[i] = contents + } + + case "Pact::SomethingLike": + return m["contents"] + case "Pact::Term": + data := m["data"].(map[string]interface{}) + return data["generate"] + } + + return nil +} + +// GetValue returns the raw generated value for the matcher +// without any of the matching detail context +func getMatcherValue(m interface{}) interface{} { + matcher, ok := getMatcher(m) + if !ok { + return nil + } + + class, ok := matcher["json_class"] + + if !ok { + return nil + } + + // extract out the value + switch class { + case "Pact::ArrayLike": + contents := matcher["contents"] + min := matcher["min"].(int) + data := make([]interface{}, min) + + for i := 0; i < min; i++ { + data[i] = contents + } + return data + + case "Pact::SomethingLike": + return matcher["contents"] + case "Pact::Term": + data := matcher["data"].(map[string]interface{}) + return data["generate"] + } + + return nil +} + +// func isMatcher(obj map[string]interface{}) bool { +func isMatcher(obj interface{}) bool { + m, ok := obj.(map[string]interface{}) + + if ok { + if _, match := m["json_class"]; match { + return true + } + } + + if _, match := obj.(Matcher); match { + return true + } + + return false +} + +func getMatcher(obj interface{}) (Matcher, bool) { + // If an object, but not a map[string]interface{} then just return? + m, ok := obj.(map[string]interface{}) + + if ok { + if _, match := m["json_class"]; match { + return m, true + } + } + + m, ok = obj.(Matcher) + if ok { + return m, true + } + + fmt.Println("NOT a matcher") + return nil, false +} + +func extractPayload(obj interface{}) interface{} { + fmt.Println("extractpaload") + + // special case: top level matching object + // we need to strip the properties + matcher, ok := getMatcher(obj) + + if ok { + fmt.Println("top level matcher", matcher, "returning value:", getMatcherValue(matcher)) + return extractPayload(getMatcherValue(matcher)) + } + + fmt.Println("not a top level matcher", matcher, "returning value:", obj) + return extractPayloadRecursive(obj, make(map[string]interface{})) +} + +// Recurse the object removing any underlying matching guff, returning +// the raw example content (ready for JSON marshalling) +// NOTE: type information is going to be lost here which is OK +// because it must be mapped to JSON encodable types +// It is expected that any object is marshalled to JSON and into a map[string]interface{} +// for use here +// It will probably break custom, user-supplied types? e.g. a User{} or ShoppingCart{}? +// But then any enclosed Matchers will likely break them anyway +func extractPayloadRecursive(obj interface{}, stack map[string]interface{}) map[string]interface{} { + fmt.Println("extracting payload recursively") + + objectMap, ok := obj.(map[string]interface{}) + if !ok { + return nil + } + + // recurse the (remaining) object, replacing Matchers with their + // actual contents + for k, rawValue := range objectMap { + fmt.Println(k, "=>", rawValue, "(raw)") + // v, ok := rawValue.(map[string]interface{}) + // fmt.Println(k, "=>", v) + + if ok && isMatcher(rawValue) { + fmt.Println("v is Matcher") + matcherValue := getMatcherValue(rawValue) + stack[k] = matcherValue + extractPayloadRecursive(matcherValue, stack) + } else { + fmt.Println("v is not Matcher but of type", reflect.TypeOf(rawValue)) + stack[k] = rawValue + extractPayloadRecursive(rawValue, stack) + } + } + + return stack +} + // MapMatcher allows a map[string]string-like object // to also contain complex matchers type MapMatcher map[string]StringMatcher diff --git a/dsl/matcher_test.go b/dsl/matcher_test.go index f87809c86..ea3c53326 100644 --- a/dsl/matcher_test.go +++ b/dsl/matcher_test.go @@ -8,6 +8,8 @@ import ( "reflect" "regexp" "testing" + + "github.com/google/go-cmp/cmp" ) func TestMatcher_TermString(t *testing.T) { @@ -462,6 +464,56 @@ func TestMatcher_SugarMatchers(t *testing.T) { } } +func TestMatcher_extractPayloadTopLevelMatcher(t *testing.T) { + m := Matcher{ + "json_class": "Pact::SomethingLike", + "contents": "something", + } + if extractPayload(m) != "something" { + t.Fatal("want 'something', got", extractPayload(m)) + } + +} + +func TestMatcher_extractPayloadComplex(t *testing.T) { + m := map[string]interface{}{ + "foo": Like("bar"), + "bar": Term("baz", "baz|bat"), + "baz": EachLike(map[string]interface{}{ + "bing": "bong", + "boing": 1, + }, 2), + } + want := map[string]interface{}{ + "foo": "bar", + "bar": "baz", + "baz": []interface{}{ + map[string]interface{}{ + "bing": "bong", + "boing": 1, + }, + map[string]interface{}{ + "bing": "bong", + "boing": 1, + }, + }, + } + + got := extractPayload(m) + if !cmp.Equal(want, got) { + t.Fatalf("want '%v', got '%v'. Diff: \n %v", want, got, cmp.Diff(want, got)) + } +} + +// func TestMatcher_getMatcher(t *testing.T) { +// m, ok := getMatcher(Matcher{ +// "json_class": "Pact::SomethingLike", +// "contents": "something", +// }) +// fmt.Println(m, ok) +// log.Println(m, ok) +// } + func ExampleLike_string() { match := Like("myspecialvalue") fmt.Println(formatJSON(match)) diff --git a/dsl/pact.go b/dsl/pact.go index 86b814a1b..ed0954a02 100644 --- a/dsl/pact.go +++ b/dsl/pact.go @@ -425,6 +425,7 @@ func (p *Pact) VerifyMessageConsumer(message *Message, handler func(...Message) // Yield message, and send through handler function // TODO: for now just call the handler + // TODO: unwrap the message back to its "generated" form err := handler(*message) if err != nil { return err