Skip to content

Commit

Permalink
Implemented wildcards for map keys in the Groovy DSL #313
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronald Holshausen committed Sep 10, 2016
1 parent 1fa05bd commit b483572
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 14 deletions.
37 changes: 37 additions & 0 deletions pact-jvm-consumer-groovy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,43 @@ withBody {
}
```

### Matching any key in a map (3.3.1+)

The DSL has been extended for cases where the keys in a map are IDs. For an example of this, see
[#313](https://github.com/DiUS/pact-jvm/issues/131). In this case you can use the `keyLike` method, which takes an
example key as a parameter.

For example:

```groovy
withBody {
example {
one {
keyLike '001', 'value' // key like an id mapped to a value
}
two {
keyLike 'ABC001', regexp('\\w+') // key like an id mapped to a matcher
}
three {
keyLike 'XYZ001', { // key like an id mapped to a closure
id identifier()
}
}
four {
keyLike '001XYZ', eachLike { // key like an id mapped to an array where each item is matched by the following
id identifier() // example
}
}
}
}
```

For an example, have a look at [WildcardPactSpec](src/test/au/com/dius/pact/consumer/groovy/WildcardPactSpec.groovy).

**NOTE:** The `keyLike` method adds a `*` to the matching path, so the matching definition will be applied to all keys
of the map if there is not a more specific matcher defined for a particular key. Having more than one `keyLike` condition
applied to a map will result in only one being applied when the pact is verified (probably the last).

## Changing the directory pact files are written to (2.1.9+)

By default, pact files are written to `target/pacts`, but this can be overwritten with the `pact.rootDir` system property.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class PactBodyBuilder extends BaseBuilder {
public static final String START_LIST = '['
public static final String END_LIST = ']'
public static final String ALL_LIST_ITEMS = '[*]'
public static final int TWO = 2
public static final String STAR = '*'

def matchers = [:]
def mimetype = null
Expand All @@ -41,8 +43,10 @@ class PactBodyBuilder extends BaseBuilder {
}

def methodMissing(String name, args) {
if (args.size() > 0) {
addAttribute(name, args[0], args.size() > 1 ? args[1] : null)
if (name == 'keyLike') {
addAttribute(args[0], STAR, args[1], args.size() > TWO ? args[TWO] : null)
} else if (args.size() > 0) {
addAttribute(name, name, args[0], args.size() > 1 ? args[1] : null)
} else {
bodyRepresentation[name] = [:]
}
Expand Down Expand Up @@ -90,52 +94,53 @@ class PactBodyBuilder extends BaseBuilder {
}

def propertyMissing(String name, def value) {
addAttribute(name, value)
addAttribute(name, name, value)
}

private void addAttribute(String name, def value, def value2 = null) {
private void addAttribute(String name, String matcherName, def value, def value2 = null) {
if (value instanceof Pattern) {
def matcher = regexp(value as Pattern, value2)
bodyRepresentation[name] = setMatcherAttribute(matcher, path + buildPath(name))
bodyRepresentation[name] = setMatcherAttribute(matcher, path + buildPath(matcherName))
} else if (value instanceof LikeMatcher) {
setMatcherAttribute(value, path + buildPath(name))
setMatcherAttribute(value, path + buildPath(matcherName))
bodyRepresentation[name] = []
value.numberExamples.times {
def exampleValue = value.values.last()
if (exampleValue instanceof Closure) {
bodyRepresentation[name] << invokeClosure(exampleValue, buildPath(name, ALL_LIST_ITEMS))
bodyRepresentation[name] << invokeClosure(exampleValue, buildPath(matcherName, ALL_LIST_ITEMS))
} else if (exampleValue instanceof Matcher) {
bodyRepresentation[name] << setMatcherAttribute(exampleValue, path + buildPath(name, ALL_LIST_ITEMS))
bodyRepresentation[name] << setMatcherAttribute(exampleValue, path + buildPath(matcherName, ALL_LIST_ITEMS))
} else if (exampleValue instanceof Pattern) {
def matcher = regexp(exampleValue as Pattern, null)
bodyRepresentation[name] << setMatcherAttribute(matcher, path + buildPath(name, ALL_LIST_ITEMS))
bodyRepresentation[name] << setMatcherAttribute(matcher, path + buildPath(matcherName, ALL_LIST_ITEMS))
} else {
bodyRepresentation[name] << exampleValue
}
}
} else if (value instanceof Matcher) {
bodyRepresentation[name] = setMatcherAttribute(value, path + buildPath(name))
bodyRepresentation[name] = setMatcherAttribute(value, path + buildPath(matcherName))
} else if (value instanceof List) {
bodyRepresentation[name] = []
value.eachWithIndex { entry, i ->
if (entry instanceof Matcher) {
bodyRepresentation[name] << setMatcherAttribute(entry, path + buildPath(name, START_LIST + i + END_LIST))
bodyRepresentation[name] << setMatcherAttribute(entry, path + buildPath(matcherName,
START_LIST + i + END_LIST))
} else if (entry instanceof Closure) {
bodyRepresentation[name] << invokeClosure(entry, buildPath(name, START_LIST + i + END_LIST))
bodyRepresentation[name] << invokeClosure(entry, buildPath(matcherName, START_LIST + i + END_LIST))
} else {
bodyRepresentation[name] << entry
}
}
} else if (value instanceof Closure) {
bodyRepresentation[name] = invokeClosure(value, buildPath(name))
bodyRepresentation[name] = invokeClosure(value, buildPath(matcherName))
} else {
bodyRepresentation[name] = value
}
}

private String buildPath(String name, String children = '') {
def key = PATH_SEP + name
if (!(name ==~ Parser$.MODULE$.FieldRegex().toString())) {
if (name != STAR && !(name ==~ Parser$.MODULE$.FieldRegex().toString())) {
key = "['" + name + "']"
}
key + children
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package au.com.dius.pact.consumer.groovy

@SuppressWarnings('UnusedImport')
import au.com.dius.pact.consumer.PactVerified$
import au.com.dius.pact.consumer.VerificationResult
import au.com.dius.pact.model.PactSpecVersion
import groovyx.net.http.RESTClient
import spock.lang.Specification

import static groovyx.net.http.ContentType.JSON

@SuppressWarnings(['AbcMetric'])
class WildcardPactSpec extends Specification {

@SuppressWarnings(['NestedBlockDepth'])
def 'pact test requiring wildcards'() {
given:
def articleService = new PactBuilder()
articleService {
serviceConsumer 'ArticleConsumer'
hasPactWith 'ArticleService'
port 1234
}

articleService {
uponReceiving('a request for an article')
withAttributes(method: 'get', path: '/')
willRespondWith(status: 200)
withBody(mimeType: JSON.toString()) {
articles eachLike {
variants eachLike {
keyLike '001', eachLike {
bundles eachLike {
keyLike('001-A') {
description string('some description')
referencedArticles eachLike {
bundleId identifier()
keyLike '001-A-1', identifier()
}
}
}
}
}
}
}
}

when:
VerificationResult result = articleService.run(specificationVersion: PactSpecVersion.V3) {
def client = new RESTClient('http://localhost:1234/')
def response = client.get(requestContentType: JSON)

assert response.status == 200
assert response.data.articles.size() == 1
assert response.data.articles[0].variants.size() == 1
assert response.data.articles[0].variants[0].keySet() == ['001'] as Set
assert response.data.articles[0].variants[0].'001'.size() == 1
assert response.data.articles[0].variants[0].'001'[0].bundles.size() == 1
assert response.data.articles[0].variants[0].'001'[0].bundles[0].keySet() == ['001-A'] as Set
}

then:
result == PactVerified$.MODULE$
articleService.interactions.size() == 1
articleService.interactions[0].response.matchingRules == [
'$.body.articles': [match: 'type'],
'$.body.articles[*].variants': [match: 'type'],
'$.body.articles[*].variants[*].*': [match: 'type'],
'$.body.articles[*].variants[*].*[*].bundles': [match: 'type'],
'$.body.articles[*].variants[*].*[*].bundles[*].*.description': [match: 'type'],
'$.body.articles[*].variants[*].*[*].bundles[*].*.referencedArticles': [match: 'type'],
'$.body.articles[*].variants[*].*[*].bundles[*].*.referencedArticles[*].bundleId': [match: 'type'],
'$.body.articles[*].variants[*].*[*].bundles[*].*.referencedArticles[*].*': [match: 'type']
]

}
}

0 comments on commit b483572

Please sign in to comment.