Skip to content

Commit

Permalink
Make the pact-proxy support v3 matching rule pattern (#20)
Browse files Browse the repository at this point in the history
* support v3 matching rule pattern
* fix docker build for M1 architecture

Co-authored-by: Sven Vermeulen <[email protected]>
  • Loading branch information
1 parent 21c1ef8 commit 6565fd8
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 17 deletions.
2 changes: 0 additions & 2 deletions build/package/pact-proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,4 @@ WORKDIR /app
COPY build/package/$APPNAME/entrypoint.sh /app/
COPY --from=build-env /go/bin/$APPNAME /app/

RUN ip -4 route list match 0/0 | awk '{print $3 "host.docker.internal"}' >> /etc/hosts

ENTRYPOINT ./entrypoint.sh
2 changes: 1 addition & 1 deletion build/package/pact-proxy/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/dumb-init /bin/sh
set -e

ip -4 route list match 0/0 | awk '{print $3 "host.docker.internal"}' >> /etc/hosts
./pact-proxy
122 changes: 111 additions & 11 deletions internal/app/pactproxy/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"mime"
"reflect"
"regexp"
"strings"
"sync"
"sync/atomic"

Expand Down Expand Up @@ -71,20 +72,22 @@ func LoadInteraction(data []byte, alias string) (*interaction, error) {

var matcher pathMatcher = &stringPathMatcher{val: request["path"].(string)}

matchingRules, hasRules := request["matchingRules"]
if !hasRules {
matchingRules = make(map[string]interface{})
matchingRules := getMatchingRules(request)
regexString, err := getPathRegex(matchingRules)
if err != nil {
return nil, err
}

if pathRule, hasPathRule := matchingRules.(map[string]interface{})["$.path"]; hasPathRule {
regexRule := pathRule.(map[string]interface{})["regex"].(string)
regex, err := regexp.Compile("^" + regexRule + "$")
if regexString != "" {
regex, err := regexp.Compile("^" + regexString + "$")

if err != nil {
return nil, errors.Wrap(err, "unable to parse interaction definition, cannot parse path regex rule")
}

matcher = &regexPathMatcher{val: regex}
}
propertiesWithMatchingRule := getBodyPropertiesWithMatchingRules(matchingRules)

interaction := &interaction{
pathMatcher: matcher,
Expand Down Expand Up @@ -112,20 +115,117 @@ func LoadInteraction(data []byte, alias string) (*interaction, error) {
switch mediaType {
case mediaTypeJSON:
if jsonRequestBody, ok := requestBody.(map[string]interface{}); ok {
interaction.addJSONConstraintsFromPact("$.body", matchingRules.(map[string]interface{}), jsonRequestBody)
interaction.addJSONConstraintsFromPact("$.body", propertiesWithMatchingRule, jsonRequestBody)
return interaction, nil
}
return nil, fmt.Errorf("media type is %s but body is not json", mediaType)
case mediaTypeText:
if plainTextRequestBody, ok := requestBody.(string); ok {
interaction.addTextConstraintsFromPact(matchingRules.(map[string]interface{}), plainTextRequestBody)
interaction.addTextConstraintsFromPact(propertiesWithMatchingRule, plainTextRequestBody)
return interaction, nil
}
return nil, fmt.Errorf("media type is %s but body is not text", mediaType)
}
return nil, fmt.Errorf("unsupported media type %s", mediaType)
}

// looks for a matching rule for key "$.path" in the supplied map
// if the found element is a map, it is treated as a pacs v2 style matching rule (i.e. "$.path": { "regex": "<expression>" } )
// if the found element is an array, it is treated as a pacs v3 list of matchers (i.e. "path": { "matchers": [ {"match": "regex", "regex": "<exp>"}]} )
func getPathRegex(matchingRules map[string]interface{}) (string, error) {
var regexString string

if rule, hasPathV2Rule := matchingRules["$.path"]; hasPathV2Rule {
val, ok := rule.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid v2 pathRegex invalid content")
}
regexType, ok := val["regex"]
if !ok {
return "", fmt.Errorf("invalid v2 pathRegex does not have regex value")
}
regexString, ok = regexType.(string)
if !ok {
return "", fmt.Errorf("invalid v2 pathRegex invalid regex type")
}

return regexString, nil
}

if rule, hasPathV3Rule := matchingRules["path"]; hasPathV3Rule {
val, ok := rule.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid v3 pathRegex invalid content")
}
matchers, ok := val["matchers"]
if !ok {
return "", fmt.Errorf("invalid v3 pathRegex - no matchers found")
}
matchersArray, ok := matchers.([]interface{})
if !ok || len(matchersArray) == 0 {
return "", fmt.Errorf("invalid v3 pathRegex - invalid matchers")
}

for _, matcher := range matchersArray {
matchersStruct := matcher.(map[string]interface{})

if match, ok := matchersStruct["match"]; !ok || match.(string) != "regex" {
continue
}
regex, ok := matchersStruct["regex"]
if !ok {
return "", fmt.Errorf("invalid v3 pathRegex - \"regex\" field is not found")
}
_, ok = regex.(string)
if !ok {
return "", fmt.Errorf("invalid v3 pathRegex - invalid regex type")
}

return regex.(string), nil
}

return "", fmt.Errorf("invalid v3 pathRegex - regex matcher is not found")
}

// no path rule present
return regexString, nil
}

func getMatchingRules(request map[string]interface{}) map[string]interface{} {
rules, hasRules := request["matchingRules"]
if !hasRules {
rules = make(map[string]interface{})
}
rulesMap := rules.(map[string]interface{})
return rulesMap
}

// finds the paths of the body properties for which the matchingRules map
// contains matching rules
// It understands both v2 style matching rules (' "$.body.data.id": { "regex": "<exp>" } )
// and v3 style matching rules ( '"body": { "$.data.id": { "matchers": [...] } } } )
func getBodyPropertiesWithMatchingRules(matchingRules map[string]interface{}) map[string]bool {
results := map[string]bool{}
for k, v := range matchingRules {
if strings.HasPrefix(k, "$.body") {
// v2 style matchingRules
results[k] = true
} else if k == "body" {
// this contains a map with the keys being the property names
// and the values being the related matchers. We are only interested
// in the property names here.
if properties, ok := v.(map[string]interface{}); ok {
for propertyname := range properties {
path := strings.TrimPrefix(propertyname, "$.")
path = "$.body." + path
results[path] = true
}
}
}
}
return results
}

func parseMediaType(request map[string]interface{}) (string, error) {
headers, hasHeaders := request["headers"]
if !hasHeaders {
Expand Down Expand Up @@ -159,7 +259,7 @@ func parseMediaType(request map[string]interface{}) (string, error) {

// This function adds constraints for all the fields in the JSON request body which do not
// have a corresponding matching rule
func (i *interaction) addJSONConstraintsFromPact(path string, matchingRules, values map[string]interface{}) {
func (i *interaction) addJSONConstraintsFromPact(path string, matchingRules map[string]bool, values map[string]interface{}) {
for k, v := range values {
switch val := v.(type) {
case map[string]interface{}:
Expand All @@ -182,8 +282,8 @@ func (i *interaction) addJSONConstraintsFromPact(path string, matchingRules, val

// This function adds constraints for the entire plain text request body if
// it doesn't have a corresponding matching rule
func (i *interaction) addTextConstraintsFromPact(matchingRules interface{}, constraint string) {
if _, present := matchingRules.(map[string]interface{})["$.body"]; !present {
func (i *interaction) addTextConstraintsFromPact(matchingRules map[string]bool, constraint string) {
if _, present := matchingRules["$.body"]; !present {
i.AddConstraint(interactionConstraint{
Path: "$.body",
Format: "%v",
Expand Down
Loading

0 comments on commit 6565fd8

Please sign in to comment.