Skip to content

Commit 633eb1f

Browse files
author
Alex Tsibulya
committed
feat(api): use static types for built-in matchers
Define EachLike, Like, Term matchers as structs instead of using generic Matcher and provide respective overrides for GetValue method to replace the switch with polymorphism and create a more expressive type system BREAKING CHANGE: The return type of Match func as well as the return type of various matcher constructors such as `Decimal`, `Date`, `IPAddress`, etc. has changed from Matcher struct to StringMatcher interface
1 parent 6ec2571 commit 633eb1f

File tree

2 files changed

+75
-64
lines changed

2 files changed

+75
-64
lines changed

dsl/matcher.go

Lines changed: 66 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -24,114 +24,139 @@ const (
2424
var timeExample = time.Date(2000, 2, 1, 12, 30, 0, 0, time.UTC)
2525

2626
type eachLike struct {
27-
Type string `json:"json_class"`
2827
Contents interface{} `json:"contents"`
28+
Type string `json:"json_class"`
2929
Min int `json:"min"`
3030
}
3131

32+
func (m eachLike) GetValue() interface{} {
33+
return m.Contents
34+
}
35+
36+
func (m eachLike) isMatcher() {
37+
}
38+
3239
type like struct {
33-
Type string `json:"json_class"`
3440
Contents interface{} `json:"contents"`
41+
Type string `json:"json_class"`
42+
}
43+
44+
func (m like) GetValue() interface{} {
45+
return m.Contents
46+
}
47+
48+
func (m like) isMatcher() {
3549
}
3650

3751
type term struct {
38-
Type string `json:"json_class"`
39-
Data struct {
40-
Generate interface{} `json:"generate"`
41-
Matcher struct {
42-
Type string `json:"json_class"`
43-
O int `json:"o"`
44-
Regex interface{} `json:"s"`
45-
} `json:"matcher"`
46-
} `json:"data"`
52+
Data termData `json:"data"`
53+
Type string `json:"json_class"`
54+
}
55+
56+
func (m term) GetValue() interface{} {
57+
return m.Data.Generate
58+
}
59+
60+
func (m term) isMatcher() {
61+
}
62+
63+
type termData struct {
64+
Generate interface{} `json:"generate"`
65+
Matcher termMatcher `json:"matcher"`
66+
}
67+
68+
type termMatcher struct {
69+
Type string `json:"json_class"`
70+
O int `json:"o"`
71+
Regex interface{} `json:"s"`
4772
}
4873

4974
// EachLike specifies that a given element in a JSON body can be repeated
5075
// "minRequired" times. Number needs to be 1 or greater
51-
func EachLike(content interface{}, minRequired int) Matcher {
52-
return Matcher{
53-
"json_class": "Pact::ArrayLike",
54-
"contents": content,
55-
"min": minRequired,
76+
func EachLike(content interface{}, minRequired int) StringMatcher {
77+
return eachLike{
78+
Type: "Pact::ArrayLike",
79+
Contents: content,
80+
Min: minRequired,
5681
}
5782
}
5883

5984
// Like specifies that the given content type should be matched based
6085
// on type (int, string etc.) instead of a verbatim match.
61-
func Like(content interface{}) Matcher {
62-
return Matcher{
63-
"json_class": "Pact::SomethingLike",
64-
"contents": content,
86+
func Like(content interface{}) StringMatcher {
87+
return like{
88+
Type: "Pact::SomethingLike",
89+
Contents: content,
6590
}
6691
}
6792

6893
// Term specifies that the matching should generate a value
6994
// and also match using a regular expression.
70-
func Term(generate string, matcher string) Matcher {
71-
return Matcher{
72-
"json_class": "Pact::Term",
73-
"data": map[string]interface{}{
74-
"generate": generate,
75-
"matcher": map[string]interface{}{
76-
"json_class": "Regexp",
77-
"o": 0,
78-
"s": matcher,
95+
func Term(generate string, matcher string) StringMatcher {
96+
return term{
97+
Type: "Pact::Term",
98+
Data: termData{
99+
Generate: generate,
100+
Matcher: termMatcher{
101+
Type: "Regexp",
102+
O: 0,
103+
Regex: matcher,
79104
},
80105
},
81106
}
82107
}
83108

84109
// HexValue defines a matcher that accepts hexidecimal values.
85-
func HexValue() Matcher {
110+
func HexValue() StringMatcher {
86111
return Regex("3F", hexadecimal)
87112
}
88113

89114
// Identifier defines a matcher that accepts integer values.
90-
func Identifier() Matcher {
115+
func Identifier() StringMatcher {
91116
return Like(42)
92117
}
93118

94119
// Integer defines a matcher that accepts ints. Identical to Identifier.
95120
var Integer = Identifier
96121

97122
// IPAddress defines a matcher that accepts valid IPv4 addresses.
98-
func IPAddress() Matcher {
123+
func IPAddress() StringMatcher {
99124
return Regex("127.0.0.1", ipAddress)
100125
}
101126

102127
// IPv4Address matches valid IPv4 addresses.
103128
var IPv4Address = IPAddress
104129

105130
// IPv6Address defines a matcher that accepts IP addresses.
106-
func IPv6Address() Matcher {
131+
func IPv6Address() StringMatcher {
107132
return Regex("::ffff:192.0.2.128", ipAddress)
108133
}
109134

110135
// Decimal defines a matcher that accepts any decimal value.
111-
func Decimal() Matcher {
136+
func Decimal() StringMatcher {
112137
return Like(42.0)
113138
}
114139

115140
// Timestamp matches a pattern corresponding to the ISO_DATETIME_FORMAT, which
116141
// is "yyyy-MM-dd'T'HH:mm:ss". The current date and time is used as the eaxmple.
117-
func Timestamp() Matcher {
142+
func Timestamp() StringMatcher {
118143
return Regex(timeExample.Format(time.RFC3339), timestamp)
119144
}
120145

121146
// Date matches a pattern corresponding to the ISO_DATE_FORMAT, which
122147
// is "yyyy-MM-dd". The current date is used as the eaxmple.
123-
func Date() Matcher {
148+
func Date() StringMatcher {
124149
return Regex(timeExample.Format("2006-01-02"), date)
125150
}
126151

127152
// Time matches a pattern corresponding to the ISO_DATE_FORMAT, which
128153
// is "'T'HH:mm:ss". The current tem is used as the eaxmple.
129-
func Time() Matcher {
154+
func Time() StringMatcher {
130155
return Regex(timeExample.Format("T15:04:05"), timeRegex)
131156
}
132157

133158
// UUID defines a matcher that accepts UUIDs. Produces a v4 UUID as the example.
134-
func UUID() Matcher {
159+
func UUID() StringMatcher {
135160
return Regex("fc763eba-0905-41c5-a27f-3934ab26786c", uuid)
136161
}
137162

@@ -187,20 +212,6 @@ func (m Matcher) isMatcher() {}
187212
// GetValue returns the raw generated value for the matcher
188213
// without any of the matching detail context
189214
func (m Matcher) GetValue() interface{} {
190-
switch m["json_class"] {
191-
default:
192-
return nil
193-
case "Pact::ArrayLike":
194-
return m["contents"]
195-
case "Pact::SomethingLike":
196-
return m["contents"]
197-
case "Pact::Term":
198-
data, ok := m["data"].(map[string]interface{})
199-
if ok {
200-
return data["generate"]
201-
}
202-
}
203-
204215
return nil
205216
}
206217

@@ -233,20 +244,20 @@ func objectToString(obj interface{}) string {
233244
// Supported Tag Formats
234245
// Minimum Slice Size: `pact:"min=2"`
235246
// String RegEx: `pact:"example=2000-01-01,regex=^\\d{4}-\\d{2}-\\d{2}$"`
236-
func Match(src interface{}) Matcher {
247+
func Match(src interface{}) StringMatcher {
237248
return match(reflect.TypeOf(src), getDefaults())
238249
}
239250

240251
// match recursively traverses the provided type and outputs a
241252
// matcher string for it that is compatible with the Pact dsl.
242-
func match(srcType reflect.Type, params params) Matcher {
253+
func match(srcType reflect.Type, params params) StringMatcher {
243254
switch kind := srcType.Kind(); kind {
244255
case reflect.Ptr:
245256
return match(srcType.Elem(), params)
246257
case reflect.Slice, reflect.Array:
247258
return EachLike(match(srcType.Elem(), getDefaults()), params.slice.min)
248259
case reflect.Struct:
249-
result := make(map[string]interface{})
260+
result := Matcher{}
250261

251262
for i := 0; i < srcType.NumField(); i++ {
252263
field := srcType.Field(i)

dsl/matcher_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ func TestMatcher_NestAllTheThings(t *testing.T) {
318318
Matcher{
319319
"colour": Term("red", "red|green|blue"),
320320
"size": Like(10),
321-
"tag": EachLike([]Matcher{Like("jumper"), Like("shirt")}, 2),
321+
"tag": EachLike([]StringMatcher{Like("jumper"), Like("shirt")}, 2),
322322
},
323323
1),
324324
1))
@@ -346,7 +346,7 @@ func formatJSON(object interface{}) interface{} {
346346

347347
// Instrument the Matcher type to be able to assert the
348348
// values and regexs contained within!
349-
func (m Matcher) getValue() interface{} {
349+
func getMatcherValue(m interface{}) interface{} {
350350
mString := objectToString(m)
351351

352352
// try like
@@ -369,7 +369,7 @@ func (m Matcher) getValue() interface{} {
369369
func TestMatcher_SugarMatchers(t *testing.T) {
370370

371371
type matcherTestCase struct {
372-
matcher Matcher
372+
matcher StringMatcher
373373
testCase func(val interface{}) error
374374
}
375375
matchers := map[string]matcherTestCase{
@@ -483,7 +483,7 @@ func TestMatcher_SugarMatchers(t *testing.T) {
483483
}
484484
var err error
485485
for k, v := range matchers {
486-
if err = v.testCase(v.matcher.getValue()); err != nil {
486+
if err = v.testCase(getMatcherValue(v.matcher)); err != nil {
487487
t.Fatalf("error validating matcher '%s': %v", k, err)
488488
}
489489
}
@@ -570,7 +570,7 @@ func TestMatch(t *testing.T) {
570570
tests := []struct {
571571
name string
572572
args args
573-
want Matcher
573+
want StringMatcher
574574
wantPanic bool
575575
}{
576576
{
@@ -599,7 +599,7 @@ func TestMatch(t *testing.T) {
599599
args: args{
600600
src: wordDTO{},
601601
},
602-
want: map[string]interface{}{
602+
want: Matcher{
603603
"word": Like(`"string"`),
604604
"length": Like(1),
605605
},
@@ -609,7 +609,7 @@ func TestMatch(t *testing.T) {
609609
args: args{
610610
src: dateDTO{},
611611
},
612-
want: map[string]interface{}{
612+
want: Matcher{
613613
"date": Term("2000-01-01", `^\\d{4}-\\d{2}-\\d{2}$`),
614614
},
615615
},
@@ -618,7 +618,7 @@ func TestMatch(t *testing.T) {
618618
args: args{
619619
src: wordsDTO{},
620620
},
621-
want: map[string]interface{}{
621+
want: Matcher{
622622
"words": EachLike(Like(`"string"`), 2),
623623
},
624624
},
@@ -730,7 +730,7 @@ func TestMatch(t *testing.T) {
730730
}
731731
for _, tt := range tests {
732732
t.Run(tt.name, func(t *testing.T) {
733-
var got Matcher
733+
var got StringMatcher
734734
var didPanic bool
735735
defer func() {
736736
if rec := recover(); rec != nil {

0 commit comments

Comments
 (0)