diff --git a/Makefile b/Makefile index 6aa352e7a..38b3a06f9 100755 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ pact: install docker pactv3: clean @echo "--- 🔨 Running Pact examples" - go test -v -tags=consumer -count=1 github.com/pact-foundation/pact-go/examples/v3/... -run TestConsumerV2 + go test -v -tags=consumer -count=1 github.com/pact-foundation/pact-go/examples/v3/... -run TestConsumerV3 release: echo "--- 🚀 Releasing it" diff --git a/examples/v3/consumer_test.go b/examples/v3/consumer_test.go index ba91f8e57..d3b229e30 100644 --- a/examples/v3/consumer_test.go +++ b/examples/v3/consumer_test.go @@ -124,9 +124,15 @@ func TestConsumerV3(t *testing.T) { Headers: v3.MapMatcher{"Content-Type": s("application/json")}, // Body: v3.Match(&User{}), Body: v3.MapMatcher{ - "dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"), - "name": s("FirstName"), - "lastName": s("LastName"), + "dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"), + "name": s("FirstName"), + "lastName": s("LastName"), + "superstring": v3.Includes("foo"), + "id": v3.Integer(12), + "accountBalance": v3.Decimal(123.76), + "itemsMinMax": v3.ArrayMinMaxLike(27, 3, 5), + "itemsMin": v3.ArrayMinLike("min", 3), + "equality": v3.Equality("a thing"), }, }) diff --git a/examples/v3/pacts/consumer-provider.json b/examples/v3/pacts/consumer-provider.json new file mode 100644 index 000000000..d57477faf --- /dev/null +++ b/examples/v3/pacts/consumer-provider.json @@ -0,0 +1,234 @@ +{ + "consumer": { + "name": "consumer" + }, + "interactions": [ + { + "description": "A request to do a foo", + "providerStates": [ + { + "name": "User foo exists", + "params": { + "id": "foo" + } + } + ], + "request": { + "body": { + "name": "billy" + }, + "headers": { + "Authorization": "Bearer 1234", + "Content-Type": "application/json" + }, + "matchingRules": { + "body": { + "$.name": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "headers": { + "$.Authorization": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.Content-Type": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "path": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "\\/foo.*" + } + ] + }, + "query": { + "$.baz[0]": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[a-z]+" + } + ] + }, + "$.baz[1]": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[a-z]+" + } + ] + }, + "$.baz[2]": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[a-z]+" + } + ] + } + } + }, + "method": "POST", + "path": "/foobar", + "query": { + "baz": [ + "bar", + "bat", + "baz" + ] + } + }, + "response": { + "body": { + "accountBalance": 123.76, + "dateTime": "2020-01-01", + "equality": "a thing", + "id": 12, + "itemsMin": [ + "min", + "min", + "min" + ], + "itemsMinMax": [ + 27, + 27, + 27, + 27, + 27 + ], + "lastName": "LastName", + "name": "FirstName", + "superstring": "foo" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "body": { + "$.accountBalance": { + "combine": "AND", + "matchers": [ + { + "match": "decimal" + } + ] + }, + "$.dateTime": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "[0-9\\-]+" + } + ] + }, + "$.equality": { + "combine": "AND", + "matchers": [ + { + "match": "equality" + } + ] + }, + "$.id": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + }, + "$.itemsMin": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.itemsMinMax": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "max": 5 + } + ] + }, + "$.lastName": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.name": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.superstring": { + "combine": "AND", + "matchers": [ + { + "match": "include", + "value": "foo" + } + ] + } + }, + "headers": { + "$.Content-Type": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + }, + "status": 200 + } + } + ], + "metadata": { + "pactGo": { + "version": "v1.4.3" + }, + "pactRust": { + "version": "0.6.3" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "provider" + } +} \ No newline at end of file diff --git a/v3/matcher.go b/v3/matcher.go new file mode 100644 index 000000000..1372883e2 --- /dev/null +++ b/v3/matcher.go @@ -0,0 +1,61 @@ +package v3 + +import "time" + +// Term Matcher regexes +const ( + hexadecimal = `[0-9a-fA-F]+` + ipAddress = `(\d{1,3}\.)+\d{1,3}` + ipv6Address = `(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,6}\Z)|(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\Z)|(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\Z)|(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}\Z)|(\A([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}\Z)|(\A([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4}){1,1}\Z)|(\A(([0-9a-f]{1,4}:){1,7}|:):\Z)|(\A:(:[0-9a-f]{1,4}){1,7}\Z)|(\A((([0-9a-f]{1,4}:){6})(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A(([0-9a-f]{1,4}:){5}[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A([0-9a-f]{1,4}:){5}:[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,3}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,2}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,1}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A(([0-9a-f]{1,4}:){1,5}|:):(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A:(:[0-9a-f]{1,4}){1,5}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)` + uuid = `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` + timestamp = `^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$` + date = `^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))?)` + timeRegex = `^(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?$` +) + +var timeExample = time.Date(2000, 2, 1, 12, 30, 0, 0, time.UTC) + +// MatcherClass is used to differentiate the various matchers when serialising +type MatcherClass string + +// Matcher Types used to discriminate when serialising the rules +const ( + // likeMatcher is the ID for the Like Matcher + likeMatcher MatcherClass = "likeMatcher" + + // regexMatcher is the ID for the Term Matcher + regexMatcher = "regexMatcher" + + // arrayMinLikeMatcher is the ID for the ArrayMinLike Matcher + arrayMinLikeMatcher = "arrayMinLikeMatcher" + + // arrayMaxLikeMatcher is the ID for the arrayMaxLikeMatcher Matcher + // arrayMaxLikeMatcher = "arrayMaxLikeMatcher" + + // arrayMinMaxLikeMatcher sets lower and upper bounds on the array size + // https://github.com/pact-foundation/pact-specification/tree/version-3#add-a-minmax-type-matcher + arrayMinMaxLikeMatcher = "arrayMinMaxLikeMatcher" + + // Matches map[string]interface{} types is basically a container for other matchers + structTypeMatcher = "structTypeMatcher" + + // Matches integers + // https://github.com/pact-foundation/pact-specification/tree/version-3#add-more-specific-type-matchers + integerMatcher = "intMatcher" + + // Matches decimals + // https://github.com/pact-foundation/pact-specification/tree/version-3#add-more-specific-type-matchers + decimalMatcher = "decimalMatcher" + + // Matches nulls + // https://github.com/pact-foundation/pact-specification/tree/version-3#add-more-specific-type-matchers + nullMatcher = "nullMatcher" + + // Equality matcher + // https://github.com/pact-foundation/pact-specification/tree/version-3#add-an-equality-matcher + equalityMatcher = "equalityMatcher" + + // includes matcher + // https://github.com/pact-foundation/pact-specification/tree/version-3#add-an-include-matcher + includesMatcher = "includesMatcher" +) diff --git a/v3/matcher_v2.go b/v3/matcher_v2.go index eff570e6a..228b89f81 100644 --- a/v3/matcher_v2.go +++ b/v3/matcher_v2.go @@ -10,23 +10,9 @@ import ( "time" ) -// Term Matcher regexes -const ( - hexadecimal = `[0-9a-fA-F]+` - ipAddress = `(\d{1,3}\.)+\d{1,3}` - ipv6Address = `(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,6}\Z)|(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\Z)|(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\Z)|(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}\Z)|(\A([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}\Z)|(\A([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4}){1,1}\Z)|(\A(([0-9a-f]{1,4}:){1,7}|:):\Z)|(\A:(:[0-9a-f]{1,4}){1,7}\Z)|(\A((([0-9a-f]{1,4}:){6})(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A(([0-9a-f]{1,4}:){5}[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A([0-9a-f]{1,4}:){5}:[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,3}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,2}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,1}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A(([0-9a-f]{1,4}:){1,5}|:):(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A:(:[0-9a-f]{1,4}){1,5}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)` - uuid = `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` - timestamp = `^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$` - date = `^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))?)` - timeRegex = `^(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?$` -) - -var timeExample = time.Date(2000, 2, 1, 12, 30, 0, 0, time.UTC) - type eachLike struct { Contents interface{} `json:"contents"` Min int `json:"min,omitempty"` - Max int `json:"max,omitempty"` } func (m eachLike) GetValue() interface{} { @@ -36,24 +22,18 @@ func (m eachLike) GetValue() interface{} { func (m eachLike) isMatcher() {} func (m eachLike) Type() MatcherClass { - if m.Max != 0 { - return arrayMaxLikeMatcher - } return arrayMinLikeMatcher } func (m eachLike) MatchingRule() rule { - matcher := rule{ + r := rule{ "match": "type", } - - if m.Max != 0 { - matcher["max"] = m.Max - } else { - matcher["min"] = m.Min + if m.Min == 0 { + r["min"] = 1 } - return matcher + return r } type like struct { @@ -118,16 +98,6 @@ func EachLike(content interface{}, min int) MatcherV2 { var ArrayMinLike = EachLike -// ArrayMaxLike matches nested arrays in request bodies. -// Ensure that each item in the list matches the provided example and the list -// is no greater than the provided max. -func ArrayMaxLike(content interface{}, max int) MatcherV2 { - return eachLike{ - Contents: content, - Max: max, - } -} - // Like specifies that the given content type should be matched based // on type (int, string etc.) instead of a verbatim match. func Like(content interface{}) MatcherV2 { @@ -154,14 +124,11 @@ func HexValue() MatcherV2 { return Regex("3F", hexadecimal) } -// Identifier defines a matcher that accepts integer values. +// Identifier defines a matcher that accepts number values. func Identifier() MatcherV2 { return Like(42) } -// Integer defines a matcher that accepts ints. Identical to Identifier. -var Integer = Identifier - // IPAddress defines a matcher that accepts valid IPv4 addresses. func IPAddress() MatcherV2 { return Regex("127.0.0.1", ipAddress) @@ -175,11 +142,6 @@ func IPv6Address() MatcherV2 { return Regex("::ffff:192.0.2.128", ipAddress) } -// Decimal defines a matcher that accepts any decimal value. -func Decimal() MatcherV2 { - return Like(42.0) -} - // Timestamp matches a pattern corresponding to the ISO_DATETIME_FORMAT, which // is "yyyy-MM-dd'T'HH:mm:ss". The current date and time is used as the eaxmple. func Timestamp() MatcherV2 { @@ -225,27 +187,6 @@ type MatcherV2 interface { MatchingRule() rule } -// MatcherClass is used to differentiate the various matchers when serialising -type MatcherClass string - -// Matcher Types used to discriminate when serialising the rules -const ( - // likeMatcher is the ID for the Like Matcher - likeMatcher MatcherClass = "likeMatcher" - - // regexMatcher is the ID for the Term Matcher - regexMatcher = "regexMatcher" - - // arrayMinLikeMatcher is the ID for the ArrayMinLike Matcher - arrayMinLikeMatcher = "arrayMinLikeMatcher" - - // arrayMaxLikeMatcher is the ID for the arrayMaxLikeMatcher Matcher - arrayMaxLikeMatcher = "arrayMaxLikeMatcher" - - // Matches map[string]interface{} types is basically a container for other matchers - structTypeMatcher = "structTypeMatcher" -) - // S is the string primitive wrapper (alias) for the Matcher type, // it allows plain strings to be matched type S string diff --git a/v3/matcher_v2_test.go b/v3/matcher_v2_test.go index 4cdeb5556..e8abb8efe 100644 --- a/v3/matcher_v2_test.go +++ b/v3/matcher_v2_test.go @@ -394,16 +394,16 @@ func TestMatcher_SugarMatchers(t *testing.T) { return }, }, - "Integer": { - matcher: Integer(), - testCase: func(v interface{}) (err error) { - _, valid := v.(float64) // JSON converts numbers to float64 in anonymous structs - if !valid { - err = fmt.Errorf("want int, got '%v'", reflect.TypeOf(v)) - } - return - }, - }, + // "Integer": { + // matcher: Integer(), + // testCase: func(v interface{}) (err error) { + // _, valid := v.(float64) // JSON converts numbers to float64 in anonymous structs + // if !valid { + // err = fmt.Errorf("want int, got '%v'", reflect.TypeOf(v)) + // } + // return + // }, + // }, "IPAddress": { matcher: IPAddress(), testCase: func(v interface{}) (err error) { @@ -431,16 +431,16 @@ func TestMatcher_SugarMatchers(t *testing.T) { return }, }, - "Decimal": { - matcher: Decimal(), - testCase: func(v interface{}) (err error) { - _, valid := v.(float64) - if !valid { - err = fmt.Errorf("want float64, got '%v'", reflect.TypeOf(v)) - } - return - }, - }, + // "Decimal": { + // matcher: Decimal(), + // testCase: func(v interface{}) (err error) { + // _, valid := v.(float64) + // if !valid { + // err = fmt.Errorf("want float64, got '%v'", reflect.TypeOf(v)) + // } + // return + // }, + // }, "Timestamp": { matcher: Timestamp(), testCase: func(v interface{}) (err error) { diff --git a/v3/matcher_v3.go b/v3/matcher_v3.go index f8fc0efbb..4caaf0ef6 100644 --- a/v3/matcher_v3.go +++ b/v3/matcher_v3.go @@ -12,25 +12,166 @@ type MatcherV3 interface { isV3Matcher() } -// S is the string primitive wrapper (alias) for the Matcher type, -// it allows plain strings to be matched -type I32 int32 +// Integer defines a matcher that accepts any integer value. +type Integer int -func (s I32) isMatcher() {} -func (s I32) isV3Matcher() {} +func (i Integer) isMatcher() {} +func (i Integer) isV3Matcher() {} // GetValue returns the raw generated value for the matcher // without any of the matching detail context -func (s I32) GetValue() interface{} { - return int(s) +func (i Integer) GetValue() interface{} { + return int(i) } -func (s I32) Type() MatcherClass { - return likeMatcher +func (i Integer) Type() MatcherClass { + return integerMatcher } -func (s I32) MatchingRule() rule { +func (i Integer) MatchingRule() rule { return rule{ + "match": "integer", + } +} + +// Decimal is a matcher that accepts a decimal type +type Decimal float64 + +func (d Decimal) GetValue() interface{} { + return float64(d) +} + +func (d Decimal) isMatcher() {} +func (d Decimal) isV3Matcher() {} + +func (d Decimal) Type() MatcherClass { + return decimalMatcher +} + +func (d Decimal) MatchingRule() rule { + return rule{ + "match": "decimal", + } +} + +// Null is a matcher that only accepts nulls +type Null float64 + +func (n Null) GetValue() interface{} { + return float64(n) +} + +func (n Null) isMatcher() {} +func (n Null) isV3Matcher() {} + +func (n Null) Type() MatcherClass { + return nullMatcher +} + +func (n Null) MatchingRule() rule { + return rule{ + "match": "null", + } +} + +// equality resets matching cascades back to equality +// see https://github.com/pact-foundation/pact-specification/tree/version-3#add-an-equality-matcher +type equality struct { + contents interface{} +} + +func (e equality) GetValue() interface{} { + return e.contents +} + +func (e equality) isMatcher() {} +func (e equality) isV3Matcher() {} + +func (e equality) Type() MatcherClass { + return equalityMatcher +} + +func (e equality) MatchingRule() rule { + return rule{ + "match": "equality", + } +} + +// Equality resets matching cascades back to equality +// see https://github.com/pact-foundation/pact-specification/tree/version-3#add-an-equality-matcher +func Equality(content interface{}) MatcherV3 { + return equality{ + contents: content, + } +} + +// Includes checks if the given string is contained by the actual value +// see https://github.com/pact-foundation/pact-specification/tree/version-3#add-an-equality-matcher +type Includes string + +func (i Includes) GetValue() interface{} { + return string(i) +} +func (i Includes) isMatcher() {} +func (i Includes) isV3Matcher() {} +func (i Includes) Type() MatcherClass { + return includesMatcher +} + +func (i Includes) MatchingRule() rule { + return rule{ + "match": "include", + "value": string(i), + } +} + +type minMaxLike struct { + Contents interface{} `json:"contents"` + Min int `json:"min,omitempty"` + Max int `json:"max,omitempty"` // NOTE: only used for V3 +} + +func (m minMaxLike) GetValue() interface{} { + return m.Contents +} + +func (m minMaxLike) isMatcher() {} +func (m minMaxLike) isV3Matcher() {} + +func (m minMaxLike) Type() MatcherClass { + return arrayMinMaxLikeMatcher +} + +func (m minMaxLike) MatchingRule() rule { + r := rule{ "match": "type", } + if m.Min == 0 { + r["min"] = 1 + } + if m.Max != 0 { + r["max"] = m.Max + } + + return r +} + +// ArrayMinMaxLike is like EachLike except has a bounds on the max and the min +// https://github.com/pact-foundation/pact-specification/tree/version-3#add-a-minmax-type-matcher +func ArrayMinMaxLike(content interface{}, min int, max int) MatcherV3 { + return minMaxLike{ + Contents: content, + Min: min, + Max: max, + } +} + +// ArrayMaxLike is like EachLike except has a bounds on the max +// https://github.com/pact-foundation/pact-specification/tree/version-3#add-a-minmax-type-matcher +func ArrayMaxLike(content interface{}, max int) MatcherV3 { + return minMaxLike{ + Contents: content, + Min: 1, + Max: max, + } } diff --git a/v3/pact_v2.go b/v3/pact_v2.go index 7e20f13a8..4a6bb2bb6 100644 --- a/v3/pact_v2.go +++ b/v3/pact_v2.go @@ -173,23 +173,17 @@ func buildPactPartV2(key string, value interface{}, body map[string]interface{}, case MatcherV2: switch t.Type() { - case arrayMinLikeMatcher, arrayMaxLikeMatcher: - log.Println("[TRACE] generate pact: ArrayMikeLikeMatcher/ArrayMaxLikeMatcher") + case arrayMinLikeMatcher: + log.Println("[TRACE] generate pact: ArrayMinLikeMatcher") times := 1 m := t.(eachLike) - if m.Max > 0 { - times = m.Max - } else if m.Min > 0 { - times = m.Min - } - arrayMap := make(map[string]interface{}) - minArray := make([]interface{}, times) + minArray := make([]interface{}, m.Min) builtPath := path + buildPath(key, allListItems) buildPactPartV2("0", t.GetValue(), arrayMap, builtPath, matchingRules) - log.Println("[TRACE] generate pact: ArrayMikeLikeMatcher/ArrayMaxLikeMatcher =>", builtPath) + log.Println("[TRACE] generate pact: ArrayMinLikeMatcher =>", builtPath) matchingRules[path+buildPath(key, "")] = m.MatchingRule() for i := 0; i < times; i++ { diff --git a/v3/pact_v3.go b/v3/pact_v3.go index 8ccc902ee..6936380fd 100644 --- a/v3/pact_v3.go +++ b/v3/pact_v3.go @@ -168,7 +168,6 @@ func recurseMapTypeV3(key string, value interface{}, body object, path string, } func wrapMatchingRule(r rule) matchers { - fmt.Println("[DEBUG] wrapmatchingrule") return matchers{ Combine: AND, Matchers: []rule{r}, @@ -201,26 +200,62 @@ func buildPactPartV3(key string, value interface{}, body object, path string, switch t := value.(type) { - case MatcherV2: + case MatcherV3: switch t.Type() { - case arrayMinLikeMatcher, arrayMaxLikeMatcher: - log.Println("[TRACE] generate pact: ArrayMikeLikeMatcher/ArrayMaxLikeMatcher") - times := 1 + case decimalMatcher, integerMatcher, nullMatcher, equalityMatcher, includesMatcher: + log.Println("[TRACE] generate pact: decimal/integer/null matcher") + builtPath := path + buildPath(key, "") + body[key] = t.GetValue() + log.Println("[TRACE] generate pact: decimal/integer/null matcher => ", builtPath) + matchingRules[builtPath] = wrapMatchingRule(t.MatchingRule()) - m := t.(eachLike) + case arrayMinMaxLikeMatcher: + times := 1 + m := t.(minMaxLike) if m.Max > 0 { times = m.Max } else if m.Min > 0 { times = m.Min } + log.Println("[TRACE] generate pact: ArrayMinMaxLikeMatcher") + + arrayMap := make(map[string]interface{}) + minArray := make([]interface{}, times) + + builtPath := path + buildPath(key, allListItems) + buildPactPartV3("0", t.GetValue(), arrayMap, builtPath, matchingRules, generators) + log.Println("[TRACE] generate pact: ArrayMinLikeMatcher =>", builtPath) + matchingRules[path+buildPath(key, "")] = wrapMatchingRule(t.MatchingRule()) + + for i := 0; i < times; i++ { + minArray[i] = arrayMap["0"] + } + + // TODO: I think this assignment is working, but the next step seems to recurse again and this never writes + // probably just a bad terminal case handling? + body[key] = minArray + fmt.Printf("Updating body: %+v, minArray: %+v", body, minArray) + path = path + buildPath(key, "") + + default: + log.Fatalf("unexpected matcher (%s) for current specification format (3.0.0)", t.Type()) + } + case MatcherV2: + switch t.Type() { + + case arrayMinLikeMatcher, arrayMinMaxLikeMatcher: + log.Println("[TRACE] generate pact: ArrayMinLikeMatcher") + m := t.(eachLike) + times := m.Min + arrayMap := make(map[string]interface{}) minArray := make([]interface{}, times) builtPath := path + buildPath(key, allListItems) buildPactPartV3("0", t.GetValue(), arrayMap, builtPath, matchingRules, generators) - log.Println("[TRACE] generate pact: ArrayMikeLikeMatcher/ArrayMaxLikeMatcher =>", builtPath) + log.Println("[TRACE] generate pact: ArrayMinLikeMatcher =>", builtPath) matchingRules[path+buildPath(key, "")] = wrapMatchingRule(m.MatchingRule()) for i := 0; i < times; i++ {