diff --git a/.travis.yml b/.travis.yml index 974a57214..5db5ad8e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go dist: focal go: -- 1.10.x +# - 1.10.x #TODO: reflect.MapRange() - 1.11.x - 1.12.x - 1.13.x diff --git a/examples/v3/consumer_test.go b/examples/v3/consumer_test.go index e3ff43bae..cedfdc5cf 100644 --- a/examples/v3/consumer_test.go +++ b/examples/v3/consumer_test.go @@ -20,16 +20,21 @@ type s = v3.String func TestConsumerV2(t *testing.T) { v3.SetLogLevel("TRACE") - // Create Pact connecting to local Daemon mockProvider, err := v3.NewHTTPMockProviderV2(v3.MockHTTPProviderConfigV2{ - Consumer: "MyConsumer", - Provider: "MyProvider", - Host: "127.0.0.1", - Port: 8080, - SpecificationVersion: v3.V2, - TLS: true, + Consumer: "MyConsumer", + Provider: "MyProvider", + Host: "127.0.0.1", + Port: 8080, + TLS: true, }) + // Override default matching behaviour + // mockProvider.SetMatchingConfig(v3.PactSerialisationOptionsV2{ + // QueryStringStyle: v3.AlwaysArray, + // QueryStringStyle: v3.Array, + // QueryStringStyle: v3.Default, + // }) + // TODO: probably better than deferring to the execute test phase, but not sure if err != nil { t.Fatal(err) @@ -38,12 +43,20 @@ func TestConsumerV2(t *testing.T) { // Set up our expected interactions. mockProvider. AddInteraction(). - // Given("User foo exists"). + Given("User foo exists"). UponReceiving("A request to do a foo"). WithRequest(v3.Request{ - Method: "POST", - Path: s("/foobar"), + Method: "POST", + Path: s("/foobar"), + // Path: v3.Regex("/foobar", `/\/foo+/`), Headers: v3.MapMatcher{"Content-Type": s("application/json"), "Authorization": s("Bearer 1234")}, + Query: v3.QueryMatcher{ + "baz": []interface{}{ + v3.Regex("bar", "[a-z]+"), + v3.Regex("bat", "[a-z]+"), + v3.Regex("baz", "[a-z]+"), + }, + }, Body: v3.MapMatcher{ "name": s("billy"), }, @@ -68,20 +81,52 @@ func TestConsumerV2(t *testing.T) { func TestConsumerV3(t *testing.T) { v3.SetLogLevel("TRACE") - // Create Pact connecting to local Daemon mockProvider, err := v3.NewHTTPMockProviderV3(v3.MockHTTPProviderConfigV2{ - Consumer: "MyConsumer", - Provider: "MyProvider", - Host: "127.0.0.1", - Port: 8080, - SpecificationVersion: v3.V2, - TLS: true, + Consumer: "MyConsumer", + Provider: "MyProvider", + Host: "127.0.0.1", + Port: 8080, + TLS: true, }) if err != nil { t.Fatal(err) } + // Set up our expected interactions. + mockProvider. + AddInteraction(). + Given(v3.ProviderStateV3{ + Description: "User foo exists", + Parameters: map[string]string{ + "id": "foo", + }, + }). + UponReceiving("A request to do a foo"). + WithRequest(v3.Request{ + Method: "POST", + Path: s("/foobar"), + Headers: v3.MapMatcher{"Content-Type": s("application/json"), "Authorization": s("Bearer 1234")}, + Body: v3.MapMatcher{ + "name": s("billy"), + }, + Query: v3.QueryMatcher{ + "baz": []interface{}{ + v3.Regex("bat", "ba+"), + }, + }, + }). + WillRespondWith(v3.Response{ + Status: 200, + 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"), + }, + }) + // Execute pact test if err := mockProvider.ExecuteTest(test); err != nil { log.Fatalf("Error on Verify: %v", err) @@ -104,9 +149,11 @@ var test = func(config v3.MockServerConfig) error { req := &http.Request{ Method: "POST", URL: &url.URL{ - Host: fmt.Sprintf("%s:%d", "localhost", config.Port), - Scheme: "https", - Path: "/foobar", + Host: fmt.Sprintf("%s:%d", "localhost", config.Port), + Scheme: "https", + Path: "/foobar", + RawQuery: "baz=bat&baz=foo&baz=something", // Default behaviour + // RawQuery: "baz[]=bat&baz[]=foo&baz[]=something", // AlwaysArray example }, Body: ioutil.NopCloser(strings.NewReader(`{"name":"billy"}`)), Header: make(http.Header), diff --git a/examples/v3/pacts/consumer-provider.json b/examples/v3/pacts/consumer-provider.json index fbd6aaad8..20e1606e6 100644 --- a/examples/v3/pacts/consumer-provider.json +++ b/examples/v3/pacts/consumer-provider.json @@ -5,6 +5,7 @@ "interactions": [ { "description": "A request to do a foo", + "providerState": "User foo exists", "request": { "body": { "name": "billy" @@ -16,10 +17,29 @@ "matchingRules": { "$.body.name": { "match": "type" + }, + "$.header.Authorization": { + "match": "type" + }, + "$.header.Content-Type": { + "match": "type" + }, + "$.query.baz[0]": { + "match": "regex", + "regex": "[a-z]+" + }, + "$.query.baz[1]": { + "match": "regex", + "regex": "[a-z]+" + }, + "$.query.baz[2]": { + "match": "regex", + "regex": "[a-z]+" } }, "method": "POST", - "path": "/foobar" + "path": "/foobar", + "query": "baz=bar&baz=bat&baz=baz" }, "response": { "body": { @@ -40,6 +60,9 @@ }, "$.body.name": { "match": "type" + }, + "$.header.Content-Type": { + "match": "type" } }, "status": 200 diff --git a/go.mod b/go.mod index 923e210d9..c0cfa8e0d 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/mattn/go-isatty v0.0.3 // indirect - github.com/mattn/goveralls v0.0.5 // indirect + github.com/mattn/goveralls v0.0.6 // indirect github.com/mitchellh/gox v1.0.1 // indirect github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 // indirect github.com/spf13/cobra v0.0.0-20160604044732-f447048345b6 @@ -19,7 +19,7 @@ require ( github.com/stretchr/testify v1.4.0 github.com/twinj/uuid v1.0.0 github.com/ugorji/go v1.1.1 // indirect - golang.org/x/tools v0.0.0-20200305224536-de023d59a5d1 // indirect + golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect ) diff --git a/go.sum b/go.sum index db69386f8..911b9e1bf 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/goveralls v0.0.5 h1:spfq8AyZ0cCk57Za6/juJ5btQxeE1FaEGMdfcI+XO48= github.com/mattn/goveralls v0.0.5/go.mod h1:Xg2LHi51faXLyKXwsndxiW6uxEEQT9+3sjGzzwU4xy0= +github.com/mattn/goveralls v0.0.6 h1:cr8Y0VMo/MnEZBjxNN/vh6G90SZ7IMb6lms1dzMoO+Y= +github.com/mattn/goveralls v0.0.6/go.mod h1:h8b4ow6FxSPMQHF6o2ve3qsclnffZjYTNEKmLesRwqw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -200,6 +202,8 @@ github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMj github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w= github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -212,6 +216,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -232,6 +237,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -249,6 +255,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -260,6 +267,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEha golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -276,6 +284,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -306,6 +315,10 @@ golang.org/x/tools v0.0.0-20200113040837-eac381796e91 h1:OOkytthzFBKHY5EfEgLUabp golang.org/x/tools v0.0.0-20200113040837-eac381796e91/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200305224536-de023d59a5d1 h1:A6Mu2vcvuNXbBiGKuVHG74fmEPmzsZ5dzG0WhV2GcqI= golang.org/x/tools v0.0.0-20200305224536-de023d59a5d1/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375 h1:SjQ2+AKWgZLc1xej6WSzL+Dfs5Uyd5xcZH1mGC411IA= +golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d h1:XZxUC4/ZNKTjrT4/Oc9gCgIYnzPW3/CefdPjsndrVWM= +golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/v3/http.go b/v3/http.go index 2fb5d7706..934bce29d 100644 --- a/v3/http.go +++ b/v3/http.go @@ -29,6 +29,29 @@ func init() { native.Init() } +// QueryStringStyle allows a user to specific the v2 query string serialisation format +// Different frameworks have different ways to serialise this, which is why +// v3 moved to storing this as a map +type QueryStringStyle int + +const ( + // Default uses the param=value1¶m=value2 style + Default QueryStringStyle = iota + + // AlwaysArray uses the [] style even if a parameter only has a single value + // e.g. param[]=value1¶m[]=value2 + AlwaysArray + + // Array uses the [] style only if a parameter only has multiple values + // e.g. param[]=value1¶m[]=value2¶m2=value + Array +) + +// PactSerialisationOptionsV2 allows a user to override specific pact serialisation options +type PactSerialisationOptionsV2 struct { + QueryStringStyle QueryStringStyle +} + type mockHTTPProviderConfig struct { // Consumer is the name of the Consumer/Client. Consumer string @@ -75,11 +98,11 @@ type mockHTTPProviderConfig struct { // Defaults to 10s ClientTimeout time.Duration + matchingConfig PactSerialisationOptionsV2 + // Check if CLI tools are up to date toolValidityCheck bool - SpecificationVersion SpecificationVersion - // TLS enables a mock service behind a self-signed certificate // TODO: document and test this TLS bool @@ -94,7 +117,8 @@ type MockHTTPProviderConfigV3 = mockHTTPProviderConfig // httpMockProvider is the entrypoint for http consumer tests and provides the base capability for the // exported types HTTPMockProviderV2 and HTTPMockProviderV3 type httpMockProvider struct { - config mockHTTPProviderConfig + specificationVersion SpecificationVersion + config mockHTTPProviderConfig v2Interactions []*InteractionV2 v3Interactions []*InteractionV3 @@ -169,9 +193,10 @@ func (p *httpMockProvider) ExecuteTest(integrationTest func(MockServerConfig) er // Generate interactions for Pact file var serialisedPact interface{} - if p.config.SpecificationVersion == V2 { - serialisedPact = newPactFileV2(p.config.Consumer, p.config.Provider, p.v2Interactions) + if p.specificationVersion == V2 { + serialisedPact = newPactFileV2(p.config.Consumer, p.config.Provider, p.v2Interactions, p.config.matchingConfig) } else { + serialisedPact = newPactFileV3(p.config.Consumer, p.config.Provider, p.v3Interactions) } diff --git a/v3/http_v2.go b/v3/http_v2.go index 9334d34d5..a23cb498f 100644 --- a/v3/http_v2.go +++ b/v3/http_v2.go @@ -13,7 +13,8 @@ type HTTPMockProviderV2 struct { func NewHTTPMockProviderV2(config MockHTTPProviderConfigV2) (*HTTPMockProviderV2, error) { provider := &HTTPMockProviderV2{ httpMockProvider: &httpMockProvider{ - config: config, + config: config, + specificationVersion: V2, }, } err := provider.validateConfig() @@ -33,3 +34,10 @@ func (p *HTTPMockProviderV2) AddInteraction() *InteractionV2 { p.httpMockProvider.v2Interactions = append(p.httpMockProvider.v2Interactions, i) return i } + +// SetMatchingConfig allows specific contract file serialisation adjustments +func (p *HTTPMockProviderV2) SetMatchingConfig(config PactSerialisationOptionsV2) *HTTPMockProviderV2 { + p.config.matchingConfig = config + + return p +} diff --git a/v3/http_v3.go b/v3/http_v3.go index f5b31cfd0..51058bcb5 100644 --- a/v3/http_v3.go +++ b/v3/http_v3.go @@ -13,7 +13,8 @@ type HTTPMockProviderV3 struct { func NewHTTPMockProviderV3(config MockHTTPProviderConfigV3) (*HTTPMockProviderV3, error) { provider := &HTTPMockProviderV3{ httpMockProvider: &httpMockProvider{ - config: config, + config: config, + specificationVersion: V3, }, } err := provider.validateConfig() diff --git a/v3/interaction.go b/v3/interaction.go index 245b629d2..854101954 100644 --- a/v3/interaction.go +++ b/v3/interaction.go @@ -15,16 +15,6 @@ type Interaction struct { // Description to be written into the Pact file Description string `json:"description"` - - // Provider state to be written into the Pact file - State string `json:"providerState,omitempty"` -} - -// Given specifies a provider state. Optional. -func (i *Interaction) Given(state string) *Interaction { - i.State = state - - return i } // UponReceiving specifies the name of the test case. This becomes the name of diff --git a/v3/matcher_v2.go b/v3/matcher_v2.go index 29a42f144..5bc0a56f4 100644 --- a/v3/matcher_v2.go +++ b/v3/matcher_v2.go @@ -262,7 +262,7 @@ 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 + return string(s) } func (s S) Type() MatcherClass { @@ -305,7 +305,13 @@ func (s StructMatcher) MatchingRule() ruleValue { // MapMatcher allows a map[string]string-like object // to also contain complex matchers -type MapMatcher map[string]Matcher +// TODO: bring back this type? +// type MapMatcher map[string]Matcher +type MapMatcher map[string]interface{} +type HeadersMatcher = MapMatcher + +// QueryMatcher matches a query string interface +type QueryMatcher map[string][]interface{} // Takes an object and converts it to a JSON representation func objectToString(obj interface{}) string { diff --git a/v3/pact_file.go b/v3/pact_file.go index 149c97dbb..141df4346 100644 --- a/v3/pact_file.go +++ b/v3/pact_file.go @@ -1,6 +1,8 @@ package v3 import ( + "log" + version "github.com/pact-foundation/pact-go/command" ) @@ -15,22 +17,6 @@ const ( V3 = "3.0.0" ) -// ruleValue is essentially a key value JSON pairs for serialisation -// TODO: this is actually more typed than this -// once we understand the model better, let's make it more type-safe -type ruleValue map[string]interface{} - -// Matching Rule -type rule struct { - Body ruleValue `json:"body,omitempty"` - Headers ruleValue `json:"headers,omitempty"` - Query ruleValue `json:"query,omitempty"` - Path ruleValue `json:"path,omitempty"` -} - -type matchingRule = rule -type generator = rule - var pactGoMetadata = map[string]interface{}{ "pactGo": map[string]string{ "version": version.Version, @@ -38,7 +24,7 @@ var pactGoMetadata = map[string]interface{}{ } // newPactFileV2 generates a v2 formated pact file from the given interactions -func newPactFileV2(consumer string, provider string, interactions []*InteractionV2) pactFileV2 { +func newPactFileV2(consumer string, provider string, interactions []*InteractionV2, options PactSerialisationOptionsV2) pactFileV2 { p := pactFileV2{ Interactions: make([]pactInteractionV2, 0), interactions: interactions, @@ -46,6 +32,7 @@ func newPactFileV2(consumer string, provider string, interactions []*Interaction Consumer: consumer, Provider: provider, SpecificationVersion: V2, + Options: options, } p.generateV2PactFile() @@ -59,6 +46,7 @@ func newPactFileV2(consumer string, provider string, interactions []*Interaction // newPactFileV3 generates a v3 formated pact file from the given interactions func newPactFileV3(consumer string, provider string, interactions []*InteractionV3) pactFileV3 { + log.Println("[DEBUG] creating v3 pact file") p := pactFileV3{ Interactions: make([]pactInteractionV3, 0), interactions: interactions, diff --git a/v3/pact_v2.go b/v3/pact_v2.go index 3f0ae0b62..0511f911e 100644 --- a/v3/pact_v2.go +++ b/v3/pact_v2.go @@ -5,26 +5,25 @@ import ( "log" "reflect" "strconv" + "strings" ) +type requestQueryV2 map[string][]string + type pactRequestV2 struct { - Method string `json:"method"` - Path Matcher `json:"path"` - Query MapMatcher `json:"query,omitempty"` - Headers MapMatcher `json:"headers,omitempty"` - Body interface{} `json:"body"` - MatchingRules map[string]interface{} `json:"matchingRules"` - MatchingRules2 matchingRule `json:"matchingRules2,omitempty"` - Generators generator `json:"generators"` + Method string `json:"method"` + Path string `json:"path"` + Query string `json:"query,omitempty"` + Headers map[string]interface{} `json:"headers,omitempty"` + Body interface{} `json:"body"` + MatchingRules map[string]interface{} `json:"matchingRules"` } type pactResponseV2 struct { - Status int `json:"status"` - Headers MapMatcher `json:"headers,omitempty"` - Body interface{} `json:"body,omitempty"` - MatchingRules map[string]interface{} `json:"matchingRules"` - MatchingRules2 matchingRule `json:"matchingRules2,omitempty"` - Generators generator `json:"generators"` + Status int `json:"status"` + Headers map[string]interface{} `json:"headers,omitempty"` + Body interface{} `json:"body,omitempty"` + MatchingRules map[string]interface{} `json:"matchingRules"` } type pactInteractionV2 struct { @@ -48,12 +47,15 @@ type pactFileV2 struct { // SpecificationVersion is the version of the Pact Spec this implementation supports SpecificationVersion SpecificationVersion `json:"-"` + // Raw incoming interactions from the consumer test interactions []*InteractionV2 - // Interactions are all of the request/response expectations, with matching rules and generators + // Interactions are the annotated set the request/response expectations, with matching rules and generators Interactions []pactInteractionV2 `json:"interactions"` Metadata map[string]interface{} `json:"metadata"` + + Options PactSerialisationOptionsV2 `json:"-"` } func pactInteractionFromV2Interaction(interaction InteractionV2) pactInteractionV2 { @@ -64,8 +66,6 @@ func pactInteractionFromV2Interaction(interaction InteractionV2) pactInteraction Method: interaction.Request.Method, Body: interaction.Request.Body, Headers: interaction.Request.Headers, - Query: interaction.Request.Query, - Path: interaction.Request.Path, MatchingRules: make(ruleValue), }, Response: pactResponseV2{ @@ -77,19 +77,33 @@ func pactInteractionFromV2Interaction(interaction InteractionV2) pactInteraction } } +func mergeRules(a ruleValue, b ruleValue) { + for k, v := range b { + a[k] = v + } +} + func (p *pactFileV2) generateV2PactFile() *pactFileV2 { for _, interaction := range p.interactions { fmt.Printf("Serialising interaction: %+v \n", *interaction) serialisedInteraction := pactInteractionFromV2Interaction(*interaction) - // TODO: haven't done matchers for headers, path and status code - _, serialisedInteraction.Request.Body, serialisedInteraction.Request.MatchingRules, _ = buildPactBody("", interaction.Request.Body, make(map[string]interface{}), "$.body", make(ruleValue), make(ruleValue)) - _, serialisedInteraction.Response.Body, serialisedInteraction.Response.MatchingRules, _ = buildPactBody("", interaction.Response.Body, make(map[string]interface{}), "$.body", make(ruleValue), make(ruleValue)) + var requestBodyMatchingRules, requestHeaderMatchingRules, requestQueryMatchingRules, responseBodyMatchingRules, responseHeaderMatchingRules ruleValue + var requestQuery map[string]interface{} + + _, requestQuery, requestQueryMatchingRules, _ = buildPactPart("", interaction.Request.Query, make(map[string]interface{}), "$.query", make(ruleValue), make(ruleValue)) + _, serialisedInteraction.Request.Headers, requestHeaderMatchingRules, _ = buildPactPart("", interaction.Request.Headers, make(map[string]interface{}), "$.headers", make(ruleValue), make(ruleValue)) + _, serialisedInteraction.Request.Body, requestBodyMatchingRules, _ = buildPactPart("", interaction.Request.Body, make(map[string]interface{}), "$.body", make(ruleValue), make(ruleValue)) + _, serialisedInteraction.Response.Body, responseBodyMatchingRules, _ = buildPactPart("", interaction.Response.Body, make(map[string]interface{}), "$.body", make(ruleValue), make(ruleValue)) + _, serialisedInteraction.Response.Headers, responseHeaderMatchingRules, _ = buildPactPart("", interaction.Response.Headers, make(map[string]interface{}), "$.headers", make(ruleValue), make(ruleValue)) - // TODO - buildPactHeaders() - buildPactQuery() - buildPactPath() + buildPactRequestQueryV2(requestQuery, interaction, &serialisedInteraction, p.Options) + buildPactRequestPathV2(interaction, &serialisedInteraction) + mergeRules(serialisedInteraction.Request.MatchingRules, requestHeaderMatchingRules) + mergeRules(serialisedInteraction.Request.MatchingRules, requestQueryMatchingRules) + mergeRules(serialisedInteraction.Request.MatchingRules, requestBodyMatchingRules) + mergeRules(serialisedInteraction.Response.MatchingRules, responseBodyMatchingRules) + mergeRules(serialisedInteraction.Response.MatchingRules, responseHeaderMatchingRules) fmt.Printf("appending interaction: %+v \n", serialisedInteraction) p.Interactions = append(p.Interactions, serialisedInteraction) @@ -98,17 +112,6 @@ func (p *pactFileV2) generateV2PactFile() *pactFileV2 { return p } -// pactBodyBuilder takes a map containing recursive Matchers and generates the rules -// to be serialised into the Pact file. -func pactBodyBuilder(root map[string]interface{}) pactFileV2 { - // Generators: make(generatorType), - // MatchingRules: make(matchingRuleType), - // Metadata: pactGoMetadata, - // return file - return pactFileV2{} - -} - const pathSep = "." const allListItems = "[*]" const startList = "[" @@ -128,16 +131,16 @@ func recurseMapType(key string, value interface{}, body map[string]interface{}, // Starting position if key == "" { - _, body, matchingRules, generators = buildPactBody(k.String(), v.Interface(), copyMap(body), path, matchingRules, generators) + _, body, matchingRules, generators = buildPactPart(k.String(), v.Interface(), copyMap(body), path, matchingRules, generators) } else { - _, body[key], matchingRules, generators = buildPactBody(k.String(), v.Interface(), entry, path, matchingRules, generators) + _, body[key], matchingRules, generators = buildPactPart(k.String(), v.Interface(), entry, path, matchingRules, generators) } } return path, body, matchingRules, generators } -// Recurse the Matcher tree and buildPactBody up an example body and set of matchers for +// Recurse the Matcher tree and buildPactPart up an example body and set of matchers for // the Pact file. Ideally this stays as a pure function, but probably might need // to store matchers externally. // @@ -149,9 +152,10 @@ func recurseMapType(key string, value interface{}, body map[string]interface{}, // - body => Current state of the body map to be built up (body will be the returned Pact body for serialisation) // - path => Path to the current key // - matchingRules => Current set of matching rules (matching rules will also be serialised into the Pact) +// - generators => Current set of generators rules (generators rules will also be serialised into the Pact) // // Returns path, body, matchingRules, generators -func buildPactBody(key string, value interface{}, body map[string]interface{}, path string, +func buildPactPart(key string, value interface{}, body map[string]interface{}, path string, matchingRules ruleValue, generators ruleValue) (string, map[string]interface{}, ruleValue, ruleValue) { log.Println("[TRACE] generate pact => key:", key, ", body:", body, ", value:", value, ", path:", path) @@ -176,7 +180,7 @@ func buildPactBody(key string, value interface{}, body map[string]interface{}, p // TODO: why does this exist? -> Umm, it's what recurses the array item values! builtPath := path + buildPath(key, allListItems) - buildPactBody("0", t.GetValue(), arrayMap, builtPath, matchingRules, generators) + buildPactPart("0", t.GetValue(), arrayMap, builtPath, matchingRules, generators) log.Println("[TRACE] generate pact: ArrayMikeLikeMatcher/ArrayMaxLikeMatcher =>", builtPath) matchingRules[path+buildPath(key, "")] = m.MatchingRule() @@ -222,13 +226,13 @@ func buildPactBody(key string, value interface{}, body map[string]interface{}, p k := fmt.Sprintf("%d", i) builtPath := path + buildPath(key, fmt.Sprintf("%s%d%s", startList, i, endList)) log.Println("[TRACE] generate pact: []interface{}: recursing into =>", builtPath) - buildPactBody(k, el, arrayMap, builtPath, matchingRules, generators) + buildPactPart(k, el, arrayMap, builtPath, matchingRules, generators) arrayValues[i] = arrayMap[k] } body[key] = arrayValues // Map -> Recurse keys (All objects start here!) - case map[string]interface{}, MapMatcher: + case map[string]interface{}, MapMatcher, QueryMatcher: log.Println("[TRACE] generate pact: MapMatcher") _, body, matchingRules, generators = recurseMapType(key, t, body, path, matchingRules, generators) @@ -243,9 +247,69 @@ func buildPactBody(key string, value interface{}, body map[string]interface{}, p return path, body, matchingRules, generators } -func buildPactHeaders() {} -func buildPactPath() {} -func buildPactQuery() {} +func buildPactRequestHeadersV2() {} + +// V2 query strings are stored as strings +// "age=30&children=Mary+Jane&children=James" +// +// * Are these two things equivalent? +// * baz[]=bat +// * baz=bat +// * Are these two things equivalent? +// * baz[]=bat&baz[]=bar +// * baz=bat&baz=bar +// See https://stackoverflow.com/questions/6243051/how-to-pass-an-array-within-a-query-string +// TODO: allow a specific generator to be provided? +func buildPactRequestQueryV2(input map[string]interface{}, sourceInteraction *InteractionV2, destInteraction *pactInteractionV2, options PactSerialisationOptionsV2) { + // matching rules already added + + var parts []string + for k, v := range input { + rt := reflect.TypeOf(v) + switch rt.Kind() { + case reflect.Slice, reflect.Array: + slice := v.([]interface{}) + length := len(slice) + + for _, data := range slice { + switch options.QueryStringStyle { + case Array: + if length == 1 { + parts = append(parts, fmt.Sprintf("%s=%s", k, data)) + } else { + parts = append(parts, fmt.Sprintf("%s[]=%s", k, data)) + } + case AlwaysArray: + parts = append(parts, fmt.Sprintf("%s[]=%s", k, data)) + default: + parts = append(parts, fmt.Sprintf("%s=%s", k, data)) + } + } + default: + parts = append(parts, fmt.Sprintf("%s=%s", k, v)) + } + } + + destInteraction.Request.Query = strings.Join(parts, "&") +} + +func buildPactRequestPathV2(sourceInteraction *InteractionV2, destInteraction *pactInteractionV2) { + fmt.Printf("[DEBUG] path matching rule %+v\n", sourceInteraction.Request.Path) + + switch val := sourceInteraction.Request.Path.(type) { + case String: + destInteraction.Request.Path = val.GetValue().(string) + case Matcher: + switch val.Type() { + case likeMatcher, regexMatcher: + destInteraction.Request.MatchingRules["$.path"] = sourceInteraction.Request.Path.MatchingRule() + destInteraction.Request.Path = val.GetValue().(string) + } + default: + delete(destInteraction.Request.MatchingRules, "path") + log.Print("[WARN] ignoring unsupported matcher for request path:", val) + } +} // TODO: allow regex in request paths. func buildPath(name string, children string) string { diff --git a/v3/pact_v3.go b/v3/pact_v3.go index 29dad6163..f85e67a7b 100644 --- a/v3/pact_v3.go +++ b/v3/pact_v3.go @@ -1,54 +1,50 @@ package v3 -// Example matching rule / generated doc -// { -// "method": "POST", -// "path": "/", -// "query": "", -// "headers": {"Content-Type": "application/json"}, -// "matchingRules": { -// "$.body.animals": {"min": 1, "match": "type"}, -// "$.body.animals[*].*": {"match": "type"}, -// "$.body.animals[*].children": {"min": 1, "match": "type"}, -// "$.body.animals[*].children[*].*": {"match": "type"} -// }, -// "body": { -// "animals": [ -// { -// "name" : "Fred", -// "children": [ -// { -// "age": 9 -// } -// ] -// } -// ] -// } -// } +import ( + "fmt" + "log" + "reflect" +) + +// ruleValue is essentially a key value JSON pairs for serialisation +// TODO: this is actually more typed than this +// once we understand the model better, let's make it more type-safe +type ruleValue map[string]interface{} + +// Matching Rule +type ruleV3 struct { + Body ruleValue `json:"body,omitempty"` + Headers ruleValue `json:"headers,omitempty"` + Query ruleValue `json:"query,omitempty"` + Path ruleValue `json:"path,omitempty"` +} + +type matchingRuleV3 = ruleV3 +type generatorV3 = ruleV3 type pactRequestV3 struct { - Method string `json:"method"` - Path Matcher `json:"path"` - Query MapMatcher `json:"query,omitempty"` - Headers MapMatcher `json:"headers,omitempty"` - Body interface{} `json:"body"` - MatchingRules matchingRule `json:"matchingRules,omitempty"` - Generators generator `json:"generators"` + Method string `json:"method"` + Path string `json:"path"` + Query map[string][]string `json:"query,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Body interface{} `json:"body"` + MatchingRules matchingRuleV3 `json:"matchingRules,omitempty"` + Generators generatorV3 `json:"generators"` } type pactResponseV3 struct { - Status int `json:"status"` - Headers MapMatcher `json:"headers,omitempty"` - Body interface{} `json:"body,omitempty"` - MatchingRules matchingRule `json:"matchingRules,omitempty"` - Generators generator `json:"generators"` + Status int `json:"status"` + Headers map[string]string `json:"headers,omitempty"` + Body interface{} `json:"body,omitempty"` + MatchingRules matchingRuleV3 `json:"matchingRules,omitempty"` + Generators generatorV3 `json:"generators"` } type pactInteractionV3 struct { - Description string `json:"description"` - State string `json:"providerState,omitempty"` - Request pactRequestV3 `json:"request"` - Response pactResponseV3 `json:"response"` + Description string `json:"description"` + States []ProviderStateV3 `json:"providerStates,omitempty"` + Request pactRequestV3 `json:"request"` + Response pactResponseV3 `json:"response"` } // pactFileV3 is what will be serialised to the Pactfile in the request body examples and matching rules @@ -73,18 +69,19 @@ type pactFileV3 struct { Metadata map[string]interface{} `json:"metadata"` } -func pactInteractionFromV3Interaction(interaction InteractionV2) pactInteractionV3 { +func pactInteractionFromV3Interaction(interaction InteractionV3) pactInteractionV3 { return pactInteractionV3{ Description: interaction.Description, - State: interaction.State, + States: interaction.States, Request: pactRequestV3{ - Method: interaction.Request.Method, - Body: interaction.Request.Body, - Headers: interaction.Request.Headers, - Query: interaction.Request.Query, - Path: interaction.Request.Path, - // Generators: make(generatorType), - MatchingRules: generator{ + Method: interaction.Request.Method, + Generators: generatorV3{ + Body: make(ruleValue), + Headers: make(ruleValue), + Path: make(ruleValue), + Query: make(ruleValue), + }, + MatchingRules: generatorV3{ Body: make(ruleValue), Headers: make(ruleValue), Path: make(ruleValue), @@ -92,11 +89,14 @@ func pactInteractionFromV3Interaction(interaction InteractionV2) pactInteraction }, }, Response: pactResponseV3{ - Status: interaction.Response.Status, - Body: interaction.Response.Body, - Headers: interaction.Response.Headers, - // Generators: make(generatorType), - MatchingRules: matchingRule{ + Status: interaction.Response.Status, + Generators: generatorV3{ + Body: make(ruleValue), + Headers: make(ruleValue), + Path: make(ruleValue), + Query: make(ruleValue), + }, + MatchingRules: matchingRuleV3{ Body: make(ruleValue), Headers: make(ruleValue), Path: make(ruleValue), @@ -107,31 +107,173 @@ func pactInteractionFromV3Interaction(interaction InteractionV2) pactInteraction } func (p *pactFileV3) generateV3PactFile() *pactFileV3 { - return nil - // for _, interaction := range p.interactions { - // fmt.Printf("Serialising interaction: %+v \n", *interaction) - // serialisedInteraction := PactInteractionFromV3Interaction(*interaction) + for _, interaction := range p.interactions { + fmt.Printf("Serialising interaction: %+v \n", *interaction) + serialisedInteraction := pactInteractionFromV3Interaction(*interaction) + + // TODO: haven't done matchers for headers, query, path and status code + // _, serialisedInteraction.Request.Headers, serialisedInteraction.Request.MatchingRules.Headers, _ = buildPactBody("", interaction.Request.Headers, make(map[string]interface{}), "$", make(ruleValue), make(ruleValue)) + _, serialisedInteraction.Request.Body, serialisedInteraction.Request.MatchingRules.Body, _ = buildPactPart("", interaction.Request.Body, make(map[string]interface{}), "$", make(ruleValue), make(ruleValue)) + // _, serialisedInteraction.Response.Headers, serialisedInteraction.Response.MatchingRules.Headers, _ = buildPactBody("", interaction.Response.Headers, make(map[string]interface{}), "$", make(ruleValue), make(ruleValue)) + _, serialisedInteraction.Response.Body, serialisedInteraction.Response.MatchingRules.Body, _ = buildPactPart("", interaction.Response.Body, make(map[string]interface{}), "$", make(ruleValue), make(ruleValue)) + + // // TODO + // buildPactHeaders() + // buildPactQuery() + buildPactPathV3(interaction, &serialisedInteraction) + + fmt.Printf("appending interaction: %+v \n", serialisedInteraction) + p.Interactions = append(p.Interactions, serialisedInteraction) + } + + return p +} + +func recurseMapTypeV3(key string, value interface{}, body map[string]interface{}, path string, + matchingRules ruleValue, generators ruleValue) (string, map[string]interface{}, ruleValue, ruleValue) { + mapped := reflect.ValueOf(value) + entry := make(map[string]interface{}) + path = path + buildPath(key, "") + + iter := mapped.MapRange() + for iter.Next() { + k := iter.Key() + v := iter.Value() + log.Println("[TRACE] generate pact: map[string]interface{}: recursing map type into key =>", k) + + // Starting position + if key == "" { + _, body, matchingRules, generators = buildPactPart(k.String(), v.Interface(), copyMap(body), path, matchingRules, generators) + } else { + _, body[key], matchingRules, generators = buildPactPart(k.String(), v.Interface(), entry, path, matchingRules, generators) + } + } + + return path, body, matchingRules, generators +} + +// Recurse the Matcher tree and buildPactBody up an example body and set of matchers for +// the Pact file. Ideally this stays as a pure function, but probably might need +// to store matchers externally. +// +// See PactBody.groovy line 96 for inspiration/logic. +// +// Arguments: +// - key => Current key in the body to set +// - value => Value for the current key, may be a primitive, object or another Matcher +// - body => Current state of the body map to be built up (body will be the returned Pact body for serialisation) +// - path => Path to the current key +// - matchingRules => Current set of matching rules (matching rules will also be serialised into the Pact) +// - generators => Current set of generators rules (generators rules will also be serialised into the Pact) +// +// Returns path, body, matchingRules, generators +func buildPactBodyV3(key string, value interface{}, body map[string]interface{}, path string, + matchingRules ruleValue, generators ruleValue) (string, map[string]interface{}, ruleValue, ruleValue) { + log.Println("[TRACE] generate pact => key:", key, ", body:", body, ", value:", value, ", path:", path) + + switch t := value.(type) { + + case Matcher: + switch t.Type() { + + case arrayMinLikeMatcher, arrayMaxLikeMatcher: + log.Println("[TRACE] generate pact: ArrayMikeLikeMatcher/ArrayMaxLikeMatcher") + 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) + + // TODO: why does this exist? -> Umm, it's what recurses the array item values! + builtPath := path + buildPath(key, allListItems) + buildPactPart("0", t.GetValue(), arrayMap, builtPath, matchingRules, generators) + log.Println("[TRACE] generate pact: ArrayMikeLikeMatcher/ArrayMaxLikeMatcher =>", builtPath) + matchingRules[path+buildPath(key, "")] = m.MatchingRule() + + // TODO: Need to understand the .* notation before implementing it. Notably missing from Groovy DSL + // log.Println("[TRACE] generate pact: matcher (type) =>", path+buildPath(key, allListItems)+".*") + // matchingRules[path+buildPath(key, allListItems)+".*"] = m.MatchingRule() + + for i := 0; i < times; i++ { + minArray[i] = arrayMap["0"] + } - // // TODO: haven't done matchers for headers, path and status code - // _, serialisedInteraction.Request.Body, serialisedInteraction.Request.MatchingRules, _ = buildPactBody("", interaction.Request.Body, make(map[string]interface{}), "$.body", make(ruleValue), make(ruleValue)) - // _, serialisedInteraction.Response.Body, serialisedInteraction.Response.MatchingRules, _ = buildPactBody("", interaction.Response.Body, make(map[string]interface{}), "$.body", make(ruleValue), make(ruleValue)) + // 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, "") - // // v3 - // // serialisedInteraction.Request.MatchingRules = requestBodyMatchingRules - // // serialisedInteraction.Response.MatchingRules = responseBodyMatchingRules + case regexMatcher, likeMatcher: + log.Println("[TRACE] generate pact: Regex/LikeMatcher") + builtPath := path + buildPath(key, "") + body[key] = t.GetValue() + log.Println("[TRACE] generate pact: Regex/LikeMatcher =>", builtPath) + matchingRules[builtPath] = t.MatchingRule() - // // v3 only - // // serialisedInteraction.Request.MatchingRules.Body = requestBodyMatchingRules - // // serialisedInteraction.Response.MatchingRules.Body = responseBodyMatchingRules + // This exists to server the v3.Match() interface + case structTypeMatcher: + log.Println("[TRACE] generate pact: StructTypeMatcher") + _, body, matchingRules, generators = recurseMapTypeV3(key, t.GetValue().(StructMatcher), body, path, matchingRules, generators) - // // TODO - // buildPactHeaders() - // buildPactQuery() - // buildPactPath() + default: + log.Fatalf("unexpected matcher (%s) for current specification format (2.0.0)", t.Type()) + } - // fmt.Printf("appending interaction: %+v \n", serialisedInteraction) - // p.Interactions = append(p.Interactions, serialisedInteraction) - // } + // Slice/Array types + case []interface{}: + log.Println("[TRACE] generate pact: []interface{}") + arrayValues := make([]interface{}, len(t)) + arrayMap := make(map[string]interface{}) + + // This is a real hack. I don't like it + // I also had to do it for the Array*LikeMatcher's, which I also don't like + for i, el := range t { + k := fmt.Sprintf("%d", i) + builtPath := path + buildPath(key, fmt.Sprintf("%s%d%s", startList, i, endList)) + log.Println("[TRACE] generate pact: []interface{}: recursing into =>", builtPath) + buildPactPart(k, el, arrayMap, builtPath, matchingRules, generators) + arrayValues[i] = arrayMap[k] + } + body[key] = arrayValues + + // Map -> Recurse keys (All objects start here!) + case map[string]interface{}, MapMatcher: + log.Println("[TRACE] generate pact: MapMatcher") + _, body, matchingRules, generators = recurseMapTypeV3(key, t, body, path, matchingRules, generators) + + // Primitives (terminal cases) + default: + log.Printf("[TRACE] generate pact: unknown type or primitive (%+v): %+v\n", reflect.TypeOf(t), value) + body[key] = value + } + + log.Printf("[TRACE] generate pact => returning body: %+v\n", body) + + return path, body, matchingRules, generators +} + +func buildPactPathV3(sourceInteraction *InteractionV3, destInteraction *pactInteractionV3) *pactInteractionV3 { + + destInteraction.Request.MatchingRules.Path = sourceInteraction.Request.Path.MatchingRule() + + switch val := sourceInteraction.Request.Path.GetValue().(type) { + case String: + destInteraction.Request.Path = val.GetValue().(string) + case like: + destInteraction.Request.Path = val.GetValue().(string) + case term: + destInteraction.Request.Path = val.GetValue().(string) + default: + destInteraction.Request.MatchingRules.Path = nil + log.Print("[WARN] ignoring unsupported matcher for request path:", val) + } - // return p + return destInteraction } diff --git a/v3/request.go b/v3/request.go index 482710720..eb5a363fe 100644 --- a/v3/request.go +++ b/v3/request.go @@ -2,9 +2,9 @@ package v3 // Request is the default implementation of the Request interface. type Request struct { - Method string `json:"method"` - Path Matcher `json:"path"` - Query MapMatcher `json:"query,omitempty"` - Headers MapMatcher `json:"headers,omitempty"` - Body interface{} `json:"body,omitempty"` + Method string `json:"method"` + Path Matcher `json:"path"` + Query QueryMatcher `json:"query,omitempty"` + Headers HeadersMatcher `json:"headers,omitempty"` + Body interface{} `json:"body,omitempty"` }