Skip to content

Commit

Permalink
feat(matchers): add more matchers for more fun 🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
mefellows committed Mar 24, 2018
1 parent 3ac7544 commit 80f3f6f
Show file tree
Hide file tree
Showing 47 changed files with 2,279 additions and 3,761 deletions.
91 changes: 91 additions & 0 deletions dsl/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,45 @@ package dsl
import (
"encoding/json"
"log"
"time"
)

// 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 {
Type string `json:"json_class"`
Contents interface{} `json:"contents"`
Min int `json:"min"`
}

type like struct {
Type string `json:"json_class"`
Contents interface{} `json:"contents"`
}

type term struct {
Type string `json:"json_class"`
Data struct {
Generate interface{} `json:"generate"`
Matcher struct {
Type string `json:"json_class"`
O int `json:"o"`
Regex interface{} `json:"s"`
} `json:"matcher"`
} `json:"data"`
}

// EachLike specifies that a given element in a JSON body can be repeated
// "minRequired" times. Number needs to be 1 or greater
func EachLike(content interface{}, minRequired int) Matcher {
Expand Down Expand Up @@ -40,6 +77,60 @@ func Term(generate string, matcher string) Matcher {
}
}

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

// Identifier defines a matcher that accepts integer values.
func Identifier() Matcher {
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() Matcher {
return Regex("127.0.0.1", ipAddress)
}

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

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

// Decimal defines a matcher that accepts any decimal value.
func Decimal() Matcher {
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() Matcher {
return Regex(timeExample.Format(time.RFC3339), timestamp)
}

// 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() Matcher {
return Regex(timeExample.Format("2006-01-02"), date)
}

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

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

// Regex is a more appropriately named alias for the "Term" matcher
var Regex = Term

Expand Down
149 changes: 147 additions & 2 deletions dsl/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/json"
"fmt"
"log"
"reflect"
"regexp"
"testing"
)

Expand Down Expand Up @@ -331,8 +333,6 @@ func formatJSON(object interface{}) interface{} {
switch content := object.(type) {
case string:
json.Indent(&out, []byte(content), "", "\t")
// case StringMatcher:
// json.Indent(&out, []byte(content), "", "\t")
default:
jsonString, err := json.Marshal(object)
if err != nil {
Expand All @@ -344,6 +344,151 @@ func formatJSON(object interface{}) interface{} {
return string(out.Bytes())
}

// Instrument the Matcher type to be able to assert the
// values and regexs contained within!
func (m Matcher) getValue() interface{} {
mString := objectToString(m)

// try like
likeValue := &like{}
err := json.Unmarshal([]byte(mString), likeValue)
if err == nil && likeValue.Contents != nil {
return likeValue.Contents
}

// try term
termValue := &term{}
err = json.Unmarshal([]byte(mString), termValue)
if err == nil && termValue != nil {
return termValue.Data.Generate
}

return "no value found"
}

func TestMatcher_SugarMatchers(t *testing.T) {

type matcherTestCase struct {
matcher Matcher
testCase func(val interface{}) error
}
matchers := map[string]matcherTestCase{
"HexValue": matcherTestCase{
matcher: HexValue(),
testCase: func(v interface{}) (err error) {
if v.(string) != "3F" {
err = fmt.Errorf("want '3F', got '%v'", reflect.TypeOf(v))
}
return
},
},
"Identifier": matcherTestCase{
matcher: Identifier(),
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": matcherTestCase{
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": matcherTestCase{
matcher: IPAddress(),
testCase: func(v interface{}) (err error) {
if v.(string) != "127.0.0.1" {
err = fmt.Errorf("want '127.0.0.1', got '%v'", reflect.TypeOf(v))
}
return
},
},
"IPv4Address": matcherTestCase{
matcher: IPv4Address(),
testCase: func(v interface{}) (err error) {
if v.(string) != "127.0.0.1" {
err = fmt.Errorf("want '127.0.0.1', got '%v'", reflect.TypeOf(v))
}
return
},
},
"IPv6Address": matcherTestCase{
matcher: IPv6Address(),
testCase: func(v interface{}) (err error) {
if v.(string) != "::ffff:192.0.2.128" {
err = fmt.Errorf("want '::ffff:192.0.2.128', got '%v'", reflect.TypeOf(v))
}
return
},
},
"Decimal": matcherTestCase{
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": matcherTestCase{
matcher: Timestamp(),
testCase: func(v interface{}) (err error) {
_, valid := v.(string)
if !valid {
err = fmt.Errorf("want string, got '%v'", reflect.TypeOf(v))
}
return
},
},
"Date": matcherTestCase{
matcher: Date(),
testCase: func(v interface{}) (err error) {
_, valid := v.(string)
if !valid {
err = fmt.Errorf("want string, got '%v'", reflect.TypeOf(v))
}
return
},
},
"Time": matcherTestCase{
matcher: Time(),
testCase: func(v interface{}) (err error) {
_, valid := v.(string)
if !valid {
err = fmt.Errorf("want string, got '%v'", reflect.TypeOf(v))
}
return
},
},
"UUID": matcherTestCase{
matcher: UUID(),
testCase: func(v interface{}) (err error) {
match, err := regexp.MatchString(uuid, v.(string))

if !match {
err = fmt.Errorf("want string, got '%v'. Err: %v", v, err)
}
return
},
},
}
var err error
for k, v := range matchers {
if err = v.testCase(v.matcher.getValue()); err != nil {
t.Fatalf("error validating matcher '%s': %v", k, err)
}
}
}

func ExampleLike_string() {
match := Like("myspecialvalue")
fmt.Println(formatJSON(match))
Expand Down
5 changes: 5 additions & 0 deletions vendor/github.com/gin-gonic/gin/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions vendor/github.com/gin-gonic/gin/recovery.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions vendor/github.com/go-kit/kit/transport/http/encode_decode.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions vendor/github.com/json-iterator/go/reflect_struct_decoder.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 80f3f6f

Please sign in to comment.