Merge branch '1.0.x'

Stub / Test Matchers (#186)
Without this change we're forcing users to embed their dynamic properties inside the body. For some this is natural and acceptable, but especially for the users coming from the Pact world this sounds bizarre. Also some other people have a problem with remembering who the consumer / producer is etc.

With this change we're introducing the stubMatchers and testMatchers section. Thanks to this one can separate the body from defining the dynamic properties. Especially for Pact users this is more natural. Speaking of which this is a prerequisite for #96

fixes #185
This commit is contained in:
Marcin Grzejszczak
2017-01-10 17:21:38 +01:00
25 changed files with 1346 additions and 54 deletions

View File

@@ -0,0 +1,39 @@
package org.springframework.cloud.contract.spec.internal
/**
* A jsonPathMatchers for the given path.
*
* @author Marcin Grzejszczak
* @since 1.0.3
*/
interface BodyMatcher {
/**
* What kind of matching are we dealing with
*/
MatchingType matchingType()
/**
* Path to the path. Example for JSON it will be JSON Path
*/
String path()
/**
* Optional value that the given path should be checked against.
* If there is no value then presence will be checked only together with
* type check. Example if we expect a JSON Path path {@code $.a} to be matched
* by type, the defined response body contained an integer but the actual one
* contained a string then the assertion should fail
*/
String value()
/**
* Min no of occurrence when matching by type. In all other cases it will be ignored
*/
Integer minTypeOccurrence()
/**
* Max no of occurrence when matching by type. In all other cases it will be ignored
*/
Integer maxTypeOccurrence()
}

View File

@@ -0,0 +1,103 @@
package org.springframework.cloud.contract.spec.internal
import groovy.transform.Canonical
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
/**
* Matching strategy of dynamic parts of the body.
*
* @author Marcin Grzejszczak
* @since 1.0.3
*/
@CompileStatic
class BodyMatchers {
private final RegexPatterns regexPatterns = new RegexPatterns()
private final List<BodyMatcher> jsonPathRegexMatchers = []
void jsonPath(String path, MatchingTypeValue matchingType) {
this.jsonPathRegexMatchers << new JsonPathBodyMatcher(path, matchingType)
}
boolean hasMatchers() {
return !this.jsonPathRegexMatchers.empty
}
List<BodyMatcher> jsonPathMatchers() {
return this.jsonPathRegexMatchers
}
MatchingTypeValue byDate() {
return new MatchingTypeValue(MatchingType.DATE, this.regexPatterns.isoDate())
}
MatchingTypeValue byTime() {
return new MatchingTypeValue(MatchingType.TIME, this.regexPatterns.isoTime())
}
MatchingTypeValue byTimestamp() {
return new MatchingTypeValue(MatchingType.TIMESTAMP, this.regexPatterns.isoDateTime())
}
MatchingTypeValue byRegex(String regex) {
assert regex
return new MatchingTypeValue(MatchingType.REGEX, regex)
}
}
@ToString
@EqualsAndHashCode
@Canonical
@CompileStatic
class JsonPathBodyMatcher implements BodyMatcher {
String jsonPath
MatchingTypeValue matchingTypeValue
@Override
MatchingType matchingType() {
return this.matchingTypeValue.type
}
@Override
String path() {
return this.jsonPath
}
@Override
String value() {
return this.matchingTypeValue.value
}
@Override
Integer minTypeOccurrence() {
return this.matchingTypeValue.minTypeOccurrence
}
@Override
Integer maxTypeOccurrence() {
return this.matchingTypeValue.maxTypeOccurrence
}
}
/**
* Matching type with corresponding values
*/
@Canonical
class MatchingTypeValue {
MatchingType type
/**
* Value of regular expression
*/
String value
/**
* Min occurrence when matching by type
*/
Integer minTypeOccurrence
/**
* Max occurrence when matching by type
*/
Integer maxTypeOccurrence
}

View File

@@ -40,6 +40,7 @@ class Input extends Common {
Headers messageHeaders
BodyType messageBody
ExecutionProperty assertThat
BodyMatchers matchers
Input() {}
@@ -104,6 +105,12 @@ class Input extends Common {
void assertThat(String assertThat) {
this.assertThat = new ExecutionProperty(assertThat)
}
void stubMatchers(@DelegatesTo(BodyMatchers) Closure closure) {
this.matchers = new BodyMatchers()
closure.delegate = this.matchers
closure()
}
}

View File

@@ -0,0 +1,15 @@
package org.springframework.cloud.contract.spec.internal
import groovy.transform.CompileStatic
/**
* Represents the type of matching the should be done against
* the body of the request or response.
*
* @author Marcin Grzejszczak
* @since 1.0.3
*/
@CompileStatic
enum MatchingType {
EQUALITY, TYPE, DATE, TIME, TIMESTAMP, REGEX
}

View File

@@ -39,9 +39,8 @@ class OptionalProperty {
return "($value)?"
}
@Override
public String toString() {
String toString() {
return optionalPattern()
}
}

View File

@@ -33,6 +33,7 @@ class OutputMessage extends Common {
Headers headers
DslProperty body
ExecutionProperty assertThat
ResponseBodyMatchers matchers
OutputMessage() {}
@@ -75,6 +76,12 @@ class OutputMessage extends Common {
}
return new DslProperty(value, server.serverValue)
}
void testMatchers(@DelegatesTo(ResponseBodyMatchers) Closure closure) {
this.matchers = new ResponseBodyMatchers()
closure.delegate = this.matchers
closure()
}
}
@CompileStatic
@@ -94,3 +101,4 @@ class ClientOutputMessage extends OutputMessage {
super(request)
}
}

View File

@@ -43,6 +43,7 @@ class Request extends Common {
RequestHeaders headers
Body body
Multipart multipart
BodyMatchers matchers
Request() {
}
@@ -204,6 +205,12 @@ class Request extends Common {
return value(client)
}
void stubMatchers(@DelegatesTo(BodyMatchers) Closure closure) {
this.matchers = new BodyMatchers()
closure.delegate = this.matchers
closure()
}
@Override
DslProperty value(ClientDslProperty client, ServerDslProperty server) {
if (server.clientValue instanceof Pattern) {

View File

@@ -41,6 +41,7 @@ class Response extends Common {
ResponseHeaders headers
Body body
boolean async
ResponseBodyMatchers matchers
Response() {
}
@@ -111,6 +112,12 @@ class Response extends Common {
return value(server)
}
void testMatchers(@DelegatesTo(ResponseBodyMatchers) Closure closure) {
this.matchers = new ResponseBodyMatchers()
closure.delegate = this.matchers
closure()
}
@Override
DslProperty value(ClientDslProperty client, ServerDslProperty server) {
if (client.clientValue instanceof Pattern) {

View File

@@ -0,0 +1,42 @@
package org.springframework.cloud.contract.spec.internal
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
/**
* Body matchers for the response side (output message, REST response)
*
* @author Marcin Grzejszczak
* @since 1.0.3
*/
@CompileStatic
@EqualsAndHashCode
@ToString(includePackage = false)
class ResponseBodyMatchers extends BodyMatchers {
MatchingTypeValue byType() {
return new MatchingTypeValue(MatchingType.TYPE)
}
MatchingTypeValue byType(@DelegatesTo(MatchingTypeValueHolder) Closure closure) {
MatchingTypeValueHolder matchingTypeValue = new MatchingTypeValueHolder()
closure.delegate = matchingTypeValue
return closure() as MatchingTypeValue
}
}
@CompileStatic
class MatchingTypeValueHolder {
MatchingTypeValue matchingTypeValue = new MatchingTypeValue()
MatchingTypeValue minOccurrence(int number) {
this.matchingTypeValue.minTypeOccurrence = number
return this.matchingTypeValue
}
MatchingTypeValue maxOccurrence(int number) {
this.matchingTypeValue.maxTypeOccurrence = number
return this.matchingTypeValue
}
}