Skip to content

Commit

Permalink
feat: add time related generators
Browse files Browse the repository at this point in the history
  • Loading branch information
mefellows committed Aug 24, 2020
1 parent 2b0b5d6 commit 7587781
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 55 deletions.
13 changes: 10 additions & 3 deletions examples/v3/consumer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ func TestConsumerV2(t *testing.T) {
"dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"),
"name": s("FirstName"),
"lastName": s("LastName"),
// "id": v3.I32(1), // Add this to demonstrate adding a v3 matcher failing the build (not at the type system level unfortunately)
"itemsMin": v3.ArrayMinLike("min", 3),
// Add any of these this to demonstrate adding a v3 matcher failing the build (not at the type system level unfortunately)
// "id": v3.Integer(1),
// "superstring": v3.Includes("foo"),
// "accountBalance": v3.Decimal(123.76),
// "itemsMinMax": v3.ArrayMinMaxLike(27, 3, 5),
// "equality": v3.Equality("a thing"),
},
})

Expand Down Expand Up @@ -109,7 +115,8 @@ func TestConsumerV3(t *testing.T) {
Path: v3.Regex("/foobar", `\/foo.*`),
Headers: v3.MapMatcher{"Content-Type": s("application/json"), "Authorization": s("Bearer 1234")},
Body: v3.MapMatcher{
"name": s("billy"),
"name": s("billy"),
"dateTime": v3.DateTimeGenerated("2020-02-02", "YYYY-MM-dd"),
},
Query: v3.QueryMatcher{
"baz": []interface{}{
Expand Down Expand Up @@ -164,7 +171,7 @@ var test = func(config v3.MockServerConfig) error {
RawQuery: "baz=bat&baz=foo&baz=something", // Default behaviour
// RawQuery: "baz[]=bat&baz[]=foo&baz[]=something", // TODO: Rust v3 does not support this syntax
},
Body: ioutil.NopCloser(strings.NewReader(`{"name":"billy"}`)),
Body: ioutil.NopCloser(strings.NewReader(`{"name":"billy", "dateTime":"2020-02-02"}`)),
Header: make(http.Header),
}

Expand Down
9 changes: 9 additions & 0 deletions examples/v3/pacts/consumer-provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@
],
"request": {
"body": {
"dateTime": "2020-02-02",
"name": "billy"
},
"generators": {
"body": {
"$.dateTime": {
"format": "YYYY-MM-dd",
"type": "DateTime"
}
}
},
"headers": {
"Authorization": "Bearer 1234",
"Content-Type": "application/json"
Expand Down
32 changes: 24 additions & 8 deletions v3/matcher.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package v3

import "time"
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)?)?$`
hexadecimalRegex = `[0-9a-fA-F]+`
ipAddressRegex = `(\d{1,3}\.)+\d{1,3}`
ipv6AddressRegex = `(\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)`
uuidRegex = `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`
timestampRegex = `^([\+-]?\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)?)?)?)?$`
dateRegex = `^([\+-]?\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)
Expand Down Expand Up @@ -58,4 +60,18 @@ const (
// includes matcher
// https://github.com/pact-foundation/pact-specification/tree/version-3#add-an-include-matcher
includesMatcher = "includesMatcher"

// string generator
// https://github.com/pact-foundation/pact-specification/tree/version-3#introduce-example-generators
stringGeneratorMatcher = "stringGeneratorMatcher"
)

// MatcherClass is used to differentiate the various matchers when serialising
type generatorType string

// Matcher Types used to discriminate when serialising the rules
const (
dateTimeGenerator generatorType = "DateTime"
dateGenerator = "Date"
timeGenerator = "Time"
)
26 changes: 6 additions & 20 deletions v3/matcher_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ func (m eachLike) GetValue() interface{} {
return m.Contents
}

func (m eachLike) isMatcher() {}

func (m eachLike) Type() MatcherClass {
return arrayMinLikeMatcher
}
Expand All @@ -44,8 +42,6 @@ func (m like) GetValue() interface{} {
return m.Contents
}

func (m like) isMatcher() {}

func (m like) Type() MatcherClass {
return likeMatcher
}
Expand All @@ -64,8 +60,6 @@ func (m term) GetValue() interface{} {
return m.Data.Generate
}

func (m term) isMatcher() {}

func (m term) Type() MatcherClass {
return regexMatcher
}
Expand Down Expand Up @@ -121,7 +115,7 @@ func Term(generate string, matcher string) MatcherV2 {

// HexValue defines a matcher that accepts hexidecimal values.
func HexValue() MatcherV2 {
return Regex("3F", hexadecimal)
return Regex("3F", hexadecimalRegex)
}

// Identifier defines a matcher that accepts number values.
Expand All @@ -131,27 +125,27 @@ func Identifier() MatcherV2 {

// IPAddress defines a matcher that accepts valid IPv4 addresses.
func IPAddress() MatcherV2 {
return Regex("127.0.0.1", ipAddress)
return Regex("127.0.0.1", ipAddressRegex)
}

// IPv4Address matches valid IPv4 addresses.
var IPv4Address = IPAddress

// IPv6Address defines a matcher that accepts IP addresses.
func IPv6Address() MatcherV2 {
return Regex("::ffff:192.0.2.128", ipAddress)
return Regex("::ffff:192.0.2.128", ipAddressRegex)
}

// 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 {
return Regex(timeExample.Format(time.RFC3339), timestamp)
return Regex(timeExample.Format(time.RFC3339), timestampRegex)
}

// Date matches a pattern corresponding to the ISO_DATE_FORMAT, which
// is "yyyy-MM-dd". The current date is used as the eaxmple.
func Date() MatcherV2 {
return Regex(timeExample.Format("2006-01-02"), date)
return Regex(timeExample.Format("2006-01-02"), dateRegex)
}

// Time matches a pattern corresponding to the ISO_DATE_FORMAT, which
Expand All @@ -162,7 +156,7 @@ func Time() MatcherV2 {

// UUID defines a matcher that accepts UUIDs. Produces a v4 UUID as the example.
func UUID() MatcherV2 {
return Regex("fc763eba-0905-41c5-a27f-3934ab26786c", uuid)
return Regex("fc763eba-0905-41c5-a27f-3934ab26786c", uuidRegex)
}

// Regex is a more appropriately named alias for the "Term" matcher
Expand All @@ -173,10 +167,6 @@ var Regex = Term
// We use the strategy outlined at http://www.jerf.org/iri/post/2917
// to create a "sum" or "union" type.
type MatcherV2 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{}
Expand All @@ -191,8 +181,6 @@ type MatcherV2 interface {
// it allows plain strings to be matched
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{} {
Expand All @@ -219,8 +207,6 @@ type StructMatcher map[string]MatcherV2

// type StructMatcher map[string]Matcher

func (m StructMatcher) isMatcher() {}

// GetValue returns the raw generated value for the matcher
// without any of the matching detail context
func (m StructMatcher) GetValue() interface{} {
Expand Down
2 changes: 1 addition & 1 deletion v3/matcher_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ func TestMatcher_SugarMatchers(t *testing.T) {
"UUID": {
matcher: UUID(),
testCase: func(v interface{}) (err error) {
match, err := regexp.MatchString(uuid, v.(string))
match, err := regexp.MatchString(uuidRegex, v.(string))

if !match {
err = fmt.Errorf("want string, got '%v'. Err: %v", v, err)
Expand Down
68 changes: 58 additions & 10 deletions v3/matcher_v3.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package v3

type v3Matcher interface {
isV3Matcher()
}

// MatcherV3 denotes a V3 specific Matcher
type MatcherV3 interface {
MatcherV2
Expand All @@ -12,10 +8,13 @@ type MatcherV3 interface {
isV3Matcher()
}

type generator interface {
Generator() rule
}

// Integer defines a matcher that accepts any integer value.
type Integer int

func (i Integer) isMatcher() {}
func (i Integer) isV3Matcher() {}

// GetValue returns the raw generated value for the matcher
Expand All @@ -41,7 +40,6 @@ func (d Decimal) GetValue() interface{} {
return float64(d)
}

func (d Decimal) isMatcher() {}
func (d Decimal) isV3Matcher() {}

func (d Decimal) Type() MatcherClass {
Expand All @@ -61,7 +59,6 @@ func (n Null) GetValue() interface{} {
return float64(n)
}

func (n Null) isMatcher() {}
func (n Null) isV3Matcher() {}

func (n Null) Type() MatcherClass {
Expand All @@ -84,7 +81,6 @@ func (e equality) GetValue() interface{} {
return e.contents
}

func (e equality) isMatcher() {}
func (e equality) isV3Matcher() {}

func (e equality) Type() MatcherClass {
Expand Down Expand Up @@ -112,7 +108,6 @@ 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
Expand All @@ -135,7 +130,6 @@ func (m minMaxLike) GetValue() interface{} {
return m.Contents
}

func (m minMaxLike) isMatcher() {}
func (m minMaxLike) isV3Matcher() {}

func (m minMaxLike) Type() MatcherClass {
Expand Down Expand Up @@ -175,3 +169,57 @@ func ArrayMaxLike(content interface{}, max int) MatcherV3 {
Max: max,
}
}

type stringGenerator struct {
contents string
generator generatorType
format string
}

func (s stringGenerator) GetValue() interface{} {
return s.contents
}

func (s stringGenerator) isV3Matcher() {}

func (s stringGenerator) Type() MatcherClass {
return stringGeneratorMatcher
}

func (s stringGenerator) Generator() rule {
r := rule{
"type": s.generator,
}
if s.format != "" {
r["format"] = s.format
}

return r
}
func (s stringGenerator) MatchingRule() rule {
return nil
}

func DateGenerated(example string, format string) MatcherV2 {
return stringGenerator{
contents: example,
generator: dateGenerator,
format: format,
}
}

func TimeGenerated(example string, format string) MatcherV2 {
return stringGenerator{
contents: example,
generator: timeGenerator,
format: format,
}
}

func DateTimeGenerated(example string, format string) MatcherV2 {
return stringGenerator{
contents: example,
generator: dateTimeGenerator,
format: format,
}
}
Loading

0 comments on commit 7587781

Please sign in to comment.