Added support for Stub Runner and Pact Broker

fixes gh-191
commit 533f1d3d0e8f6dcd37677bfef555fc2ea86bc1b1
Author: Tim Ysewyn <Tim.Ysewyn@me.com>
Date:   Wed Mar 7 15:56:48 2018 +0100

    Upgraded pact-jvm-model to 3.5.13
This commit is contained in:
Marcin Grzejszczak
2018-04-02 22:50:40 +02:00
parent de529e222e
commit 6402e14f3c
82 changed files with 953 additions and 395 deletions

View File

@@ -19,7 +19,7 @@
<modules>
<module>spring-cloud-contract-converters</module>
<module>spring-cloud-contract-spec-pact</module>
<module>spring-cloud-contract-pact</module>
<module>spring-cloud-contract-maven-plugin</module>
<module>spring-cloud-contract-gradle-plugin</module>
</modules>

View File

@@ -415,7 +415,7 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -90,7 +90,7 @@ class MavenContractsDownloader {
contractDownloader().updatePropertiesWithInclusion(downloadedContractsDir, config);
return downloadedContractsDir;
} else if (shouldDownloadContracts()) {
this.log.info("Download dependency is provided - will download contract jars");
this.log.info("Download dependency is provided - will retrieve contracts from a remote location");
File downloadedContracts = contractDownloader().unpackedDownloadedContracts(config);
this.project.getProperties().setProperty(CONTRACTS_DIRECTORY_PROP, downloadedContracts.getAbsolutePath());
return downloadedContracts;

View File

@@ -46,7 +46,7 @@
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${spring.cloud.contract.version}</version>
</dependency>
<dependency>

View File

@@ -8,10 +8,10 @@
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<artifactId>spring-cloud-contract-pact</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Contract Spec Pact</name>
<description>Spring Cloud Contract Spec Pact</description>
<name>Spring Cloud Contract Pact</name>
<description>Spring Cloud Contract Pact</description>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
@@ -19,7 +19,11 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-verifier</artifactId>
<artifactId>spring-cloud-contract-converters</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -37,6 +41,10 @@
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-consumer-java8_2.12</artifactId>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-junit_2.12</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
@@ -52,6 +60,11 @@
<artifactId>spock-global-unroll</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
@@ -62,8 +75,13 @@
<execution>
<goals>
<goal>addSources</goal>
<goal>addTestSources</goal>
<goal>generateStubs</goal>
<goal>compile</goal>
<goal>testGenerateStubs</goal>
<goal>testCompile</goal>
<goal>removeStubs</goal>
<goal>removeTestStubs</goal>
</goals>
</execution>
</executions>

View File

@@ -0,0 +1,376 @@
/*
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import au.com.dius.pact.model.Pact;
import au.com.dius.pact.model.PactSpecVersion;
import au.com.dius.pact.provider.junit.loader.PactBroker;
import au.com.dius.pact.provider.junit.loader.PactBrokerAuth;
import au.com.dius.pact.provider.junit.loader.PactBrokerLoader;
import au.com.dius.pact.provider.junit.loader.PactLoader;
import au.com.dius.pact.provider.junit.sysprops.SystemPropertyResolver;
import au.com.dius.pact.provider.junit.sysprops.ValueResolver;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.cloud.contract.verifier.converter.StubGenerator;
import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider;
import org.springframework.cloud.contract.verifier.file.ContractMetadata;
import org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringUtils;
/**
* Allows downloading of Pact files from the Pact Broker.
*
* @author Marcin Grzejszczak
* @since 2.0.0
*/
public final class PactStubDownloaderBuilder implements StubDownloaderBuilder {
private static final List<String> ACCEPTABLE_PROTOCOLS = Collections
.singletonList("pact");
/**
* Does any of the accepted protocols matches the URL of the repository
* @param url - of the repository
*/
private static boolean isProtocolAccepted(String url) {
return ACCEPTABLE_PROTOCOLS.stream().anyMatch(url::startsWith);
}
@Override public StubDownloader build(StubRunnerOptions stubRunnerOptions) {
if (stubRunnerOptions.getStubsMode() == StubRunnerProperties.StubsMode.CLASSPATH ||
stubRunnerOptions.getStubRepositoryRoot() == null) {
return null;
}
Resource resource = stubRunnerOptions.getStubRepositoryRoot();
if (!(resource instanceof PactResource)) {
return null;
}
return new PactStubDownloader(stubRunnerOptions);
}
@Override public Resource resolve(String location, ResourceLoader resourceLoader) {
if (StringUtils.isEmpty(location) || !isProtocolAccepted(location)) {
return null;
}
return new PactResource(location);
}
}
class PactResource extends AbstractResource {
private final String rawLocation;
PactResource(String location) {
this.rawLocation = location;
}
@Override public String getDescription() {
return this.rawLocation;
}
@Override public InputStream getInputStream() {
return null;
}
@Override public URI getURI() {
return URI.create(this.rawLocation);
}
}
class PactStubDownloader implements StubDownloader {
private static final String TEMP_DIR_PREFIX = "pact";
private static final Log log = LogFactory.getLog(PactStubDownloader.class);
// Preloading class for the shutdown hook not to throw ClassNotFound
private static final Class CLAZZ = TemporaryFileStorage.class;
private static final String ARTIFICIAL_NAME_ENDING_WITH_GROOVY = "name.groovy";
private final StubRunnerOptions stubRunnerOptions;
private final boolean deleteStubsAfterTest;
private final ObjectMapper objectMapper;
private static final String PROVIDER_NAME_WITH_GROUP_ID = "pactbroker.provider-name-with-group-id";
PactStubDownloader(StubRunnerOptions stubRunnerOptions) {
this.stubRunnerOptions = stubRunnerOptions;
this.objectMapper = new ObjectMapper();
this.deleteStubsAfterTest = stubRunnerOptions.isDeleteStubsAfterTest();
registerShutdownHook();
}
@Override public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
StubConfiguration stubConfiguration) {
String version = stubConfiguration.version;
final FromPropsThenFromSysEnv resolver = new FromPropsThenFromSysEnv(this.stubRunnerOptions);
List<String> tags = tags(version, resolver);
try {
PactLoader loader = pactBrokerLoader(resolver, tags);
String providerName = providerName(stubConfiguration);
List<Pact> pacts = loader.load(providerName);
if (pacts.isEmpty()) {
log.warn("No pact definitions found for provider [" + providerName + "]");
return null;
}
File tmpDirWhereStubsWillBeUnzipped = TemporaryFileStorage.createTempDir(TEMP_DIR_PREFIX);
// make the groupid / artifactid folders
String coordinatesFolderName = stubConfiguration.getGroupId().replace(".", File.separator) +
File.separator + stubConfiguration.getArtifactId();
File contractsFolder = new File(tmpDirWhereStubsWillBeUnzipped,
coordinatesFolderName + File.separator + "contracts");
File mappingsFolder = new File(tmpDirWhereStubsWillBeUnzipped,
coordinatesFolderName + File.separator + "mappings");
boolean createdContractsDirs = contractsFolder.mkdirs();
boolean createdMappingsDirs = mappingsFolder.mkdirs();
if (!createdContractsDirs || !createdMappingsDirs) {
throw new IllegalStateException("Failed to create mandatory [contracts] or [mappings] folders under [" + coordinatesFolderName + "]");
}
storePacts(providerName, pacts, contractsFolder, mappingsFolder);
return new AbstractMap.SimpleEntry<>(stubConfiguration, tmpDirWhereStubsWillBeUnzipped);
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
private void storePacts(String providerName, List<Pact> pacts, File contractsFolder,
File mappingsFolder) {
for (int i = 0; i < pacts.size(); i++) {
String json = toJson(pacts.get(i).toMap(PactSpecVersion.V3));
File file = new File(contractsFolder, i + "_" +
providerName.replace(":", "_") + "_pact.json");
storeFile(file.toPath(), json.getBytes());
try {
storeMapping(mappingsFolder, file);
} catch (Exception e) {
log.warn("Exception occurred while trying to store the mapping", e);
}
}
}
private void storeMapping(File mappingsFolder, File file) {
Collection<Contract> contracts = new PactContractConverter()
.convertFrom(file);
if (log.isDebugEnabled()) {
log.debug("Converted pact file [" + file + "] to [" + contracts.size() + "] contracts");
}
StubGeneratorProvider provider = new StubGeneratorProvider();
Collection<StubGenerator> stubGenerators = provider
.converterForName(ARTIFICIAL_NAME_ENDING_WITH_GROOVY);
if (log.isDebugEnabled()) {
log.debug("Found following matching stub generators " + stubGenerators);
}
for (StubGenerator stubGenerator : stubGenerators) {
Map<Contract, String> map = stubGenerator
.convertContents(file.getName(),
new ContractMetadata(file.toPath(), false,
contracts.size(), null, contracts));
for (Map.Entry<Contract, String> entry : map.entrySet()) {
String value = entry.getValue();
File mapping = new File(mappingsFolder,
StringUtils.stripFilenameExtension(file.getName()) + "_" +
Math.abs(entry.getKey().hashCode()) + ".json");
storeFile(mapping.toPath(), value.getBytes());
}
}
}
private void storeFile(Path path, byte[] contents) {
try {
Files.write(path, contents);
if (log.isDebugEnabled()) {
log.debug("Stored file [" + path.toString() + "]");
}
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
private String providerName(StubConfiguration stubConfiguration) {
boolean providerNameWithGroupId = Boolean.parseBoolean(
StubRunnerPropertyUtils.getProperty(this.stubRunnerOptions.getProperties(),
PROVIDER_NAME_WITH_GROUP_ID));
if (providerNameWithGroupId) {
return stubConfiguration.getGroupId() + ":" + stubConfiguration.getArtifactId();
}
return stubConfiguration.getArtifactId();
}
@NotNull PactLoader pactBrokerLoader(ValueResolver resolver,
List<String> tags) throws IOException {
Resource repo = this.stubRunnerOptions.getStubRepositoryRoot();
String schemeSpecificPart = schemeSpecificPart(repo.getURI());
URI pactBrokerUrl = URI.create(schemeSpecificPart);
return new PactBrokerLoader(new PactBroker() {
@Override public Class<? extends Annotation> annotationType() {
return PactBroker.class;
}
@Override public String host() {
return resolver.resolveValue("pactbroker.host:" + pactBrokerUrl.getHost());
}
@Override public String port() {
return resolver.resolveValue("pactbroker.port:" + pactBrokerUrl.getPort());
}
@Override public String protocol() {
return resolver.resolveValue("pactbroker.protocol:" + pactBrokerUrl.getScheme());
}
@Override public String[] tags() {
return tags.toArray(new String[0]);
}
@Override public boolean failIfNoPactsFound() {
return true;
}
@Override public PactBrokerAuth authentication() {
return new PactBrokerAuth() {
@Override public Class<? extends Annotation> annotationType() {
return PactBrokerAuth.class;
}
@Override public String scheme() {
return resolver.resolveValue("pactbroker.auth.scheme:basic");
}
@Override public String username() {
return resolver.resolveValue("pactbroker.auth.username:");
}
@Override public String password() {
return resolver.resolveValue("pactbroker.auth.password:");
}
};
}
@Override public Class<? extends ValueResolver> valueResolver() {
return SystemPropertyResolver.class;
}
});
}
private String schemeSpecificPart(URI uri) {
String part = uri.getSchemeSpecificPart();
if (StringUtils.isEmpty(part)) {
return part;
}
return part.startsWith("//") ? part.substring(2) : part;
}
@NotNull private List<String> tags(String version, ValueResolver resolver) {
String defaultTag = StubConfiguration.DEFAULT_VERSION.equals(version)
? "latest" : version;
return new ArrayList<>(Arrays.asList(StringUtils
.commaDelimitedListToStringArray(
resolver.resolveValue("pactbroker.tags:" + defaultTag + ""))));
}
private String toJson(Map map) {
try {
return this.objectMapper.writeValueAsString(map);
}
catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
private void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(
() -> TemporaryFileStorage.cleanup(PactStubDownloader.this.deleteStubsAfterTest)));
}
}
class FromPropsThenFromSysEnv implements ValueResolver {
final SystemPropertyResolver resolver = new SystemPropertyResolver();
StubRunnerOptions options;
FromPropsThenFromSysEnv(StubRunnerOptions options) {
this.options = options;
}
@Override public String resolveValue(String expression) {
PropertyValueTuple tuple = new PropertyValueTuple(expression).invoke();
String propertyName = tuple.getPropertyName();
String property = StubRunnerPropertyUtils
.getProperty(this.options.getProperties(), propertyName);
if (StringUtils.hasText(property)) {
return property;
}
return this.resolver.resolveValue(expression);
}
@Override public boolean propertyDefined(String property) {
PropertyValueTuple tuple = new PropertyValueTuple(property).invoke();
String propertyName = tuple.getPropertyName();
boolean hasProperty = StubRunnerPropertyUtils
.hasProperty(this.options.getProperties(), propertyName);
if (hasProperty) {
return true;
}
return this.resolver.propertyDefined(property);
}
}
// taken from pact - au.com.dius.pact.provider.junit.sysprops.SystemPropertyResolver.PropertyValueTuple
class PropertyValueTuple {
private String propertyName;
PropertyValueTuple(String property) {
this.propertyName = property;
}
String getPropertyName() {
return this.propertyName;
}
PropertyValueTuple invoke() {
if (this.propertyName.contains(":")) {
String[] kv = org.apache.commons.lang3.StringUtils
.splitPreserveAllTokens(this.propertyName, ':');
this.propertyName = kv[0];
}
return this;
}
}

View File

@@ -0,0 +1,5 @@
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
org.springframework.cloud.contract.stubrunner.PactStubDownloaderBuilder

View File

@@ -0,0 +1,114 @@
package org.springframework.cloud.contract.stubrunner
import java.nio.file.Files
import au.com.dius.pact.model.Pact
import au.com.dius.pact.model.PactSource
import au.com.dius.pact.provider.junit.loader.PactLoader
import au.com.dius.pact.provider.junit.sysprops.ValueResolver
import com.github.tomakehurst.wiremock.stubbing.StubMapping
import org.jetbrains.annotations.NotNull
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
/**
* @author Marcin Grzejszczak
*/
class PactStubDownloaderBuilderSpec extends Specification {
def "should retrieve pacts from broker"() throws IOException {
given:
Collection<Pact> pacts = new PactContractConverter().convertTo([Contract.make {
request {
url "/foo"
method GET()
}
response {
status OK()
}
},Contract.make {
request {
url "/bar"
method GET()
}
response {
status OK()
}
}])
StubRunnerOptions options = new StubRunnerOptionsBuilder()
.withProperties(props())
.build()
PactStubDownloader downloader = new PactStubDownloader(options) {
@NotNull @Override PactLoader pactBrokerLoader(ValueResolver resolver,
List<String> tags) {
return new PactLoader() {
@Override List<Pact> load(String providerName) {
return pacts
}
@Override PactSource getPactSource() {
return null
}
}
}
}
when:
Map.Entry<StubConfiguration, File> entry = downloader
.downloadAndUnpackStubJar(new StubConfiguration("com.example:bobby:+:classifier"))
then:
entry != null
entry.getValue().exists()
File contracts = new File(entry.getValue(), "com/example/bobby/contracts")
contracts.exists()
contracts.list() != null
File mappings = new File(entry.getValue(), "com/example/bobby/mappings")
mappings.exists()
mappings.list() != null
mappings.list().size() == 2
StubMapping.buildFrom(new String(Files.readAllBytes(mappings.listFiles()[0].toPath())))
StubMapping.buildFrom(new String(Files.readAllBytes(mappings.listFiles()[1].toPath())))
}
Map<String, String> props() {
Map<String, String> map = new HashMap<>()
// map.put("pactbroker.host", "localhost")
// map.put("pactbroker.port", String.valueOf(this.port))
// map.put("pactbroker.host", "test.pact.dius.com.au")
// map.put("pactbroker.port", "443")
// map.put("pactbroker.protocol", "https")
// map.put("pactbroker.auth.scheme", "Basic")
// map.put("pactbroker.auth.username", "dXfltyFMgNOFZAxr8io9wJ37iUpY42M")
// map.put("pactbroker.auth.password", "O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1")
return map
}
// @After
// void tearDown() {
// SnapshotRecordResult recording = WireMock.stopRecording()
// List<StubMapping> mappings = recording.getStubMappings()
// storeMappings(mappings)
// }
// private void recordFromBroker() {
// WireMock.startRecording(WireMock.recordSpec()
// .forTarget("https://test.pact.dius.com.au")
// .extractTextBodiesOver(9999999L)
// .extractBinaryBodiesOver(9999999L)
// .makeStubsPersistent(false))
// }
// private void storeMappings(List<StubMapping> mappings) {
// try {
// File proxiedStubs = new File("target/stubs")
// proxiedStubs.mkdirs()
// for (StubMapping mapping : mappings) {
// File stub = new File(proxiedStubs, "foo" + ".json")
// stub.createNewFile()
// Files.write(stub.toPath(), mapping.toString().getBytes())
// }
// } catch (Exception e) {
// throw new RuntimeException(e)
// }
// }
}

View File

@@ -1,2 +0,0 @@
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter

View File

@@ -1,68 +0,0 @@
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url '/'
body([
someInteger: 1234567890,
someDecimal: 123.123,
someHex: 'DEADC0DE',
someAlphaNumeric: 'Some alpha numeric string with 1234567890',
someUUID: '00000000-0000-0000-0000-000000000000',
someDate: '2018-03-26',
someTime: '13:37:00',
someDateTime: '2018-03-26 13:37:00',
someBoolean: 'true',
someNullValue: null
])
headers {
contentType('application/json')
header("Some-Header", $(c(regex('[a-zA-Z]{9}')), p('someValue')))
header("Header-Without-Matcher", 'someValue')
}
bodyMatchers {
jsonPath('$.someInteger', byRegex(anInteger()))
jsonPath('$.someDecimal', byRegex(aDouble()))
jsonPath('$.someHex', byRegex('[a-fA-F0-9]+'))
jsonPath('$.someAlphaNumeric', byRegex(alphaNumeric()))
jsonPath('$.someUUID', byRegex(uuid()))
jsonPath('$.someDate', byDate())
jsonPath('$.someTime', byTime())
jsonPath('$.someDateTime', byTimestamp())
jsonPath('$.someBoolean', byRegex(anyBoolean()))
}
}
response {
status OK()
body([
someInteger: 1234567890,
someDecimal: 123.123,
someHex: 'DEADC0DE',
someAlphaNumeric: 'Some alpha numeric string with 1234567890',
someUUID: '00000000-0000-0000-0000-000000000000',
someDate: '2018-03-26',
someTime: '13:37:00',
someDateTime: '2018-03-26 13:37:00',
someBoolean: 'true',
someRegex: 1234567890
])
headers {
contentType('application/json')
header("Some-Header", $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
header("Header-Without-Matcher", 'someValue')
}
bodyMatchers {
jsonPath('$.someInteger', byRegex(anInteger()))
jsonPath('$.someDecimal', byRegex(aDouble()))
jsonPath('$.someHex', byRegex('[a-fA-F0-9]+'))
jsonPath('$.someAlphaNumeric', byRegex(alphaNumeric()))
jsonPath('$.someUUID', byRegex(uuid()))
jsonPath('$.someDate', byDate())
jsonPath('$.someTime', byTime())
jsonPath('$.someDateTime', byTimestamp())
jsonPath('$.someBoolean', byRegex(anyBoolean()))
jsonPath('$.someNullValue', byNull())
}
}
}

View File

@@ -1,276 +0,0 @@
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "POST",
"path": "/",
"headers": {
"Content-Type": "application/json",
"Header-Without-Matcher": "someValue",
"Some-Header": "someValue"
},
"body": {
"someHex": "DEADC0DE",
"someTime": "13:37:00",
"someNullValue": null,
"someInteger": 1234567890,
"someBoolean": "true",
"someDate": "2018-03-26",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDecimal": 123.123,
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someDateTime": "2018-03-26 13:37:00"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
},
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
],
"combine": "AND"
}
},
"body": {
"$.someInteger": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
},
"$.someDecimal": {
"matchers": [
{
"match": "decimal"
}
],
"combine": "AND"
},
"$.someHex": {
"matchers": [
{
"match": "regex",
"regex": "[a-fA-F0-9]+"
}
],
"combine": "AND"
},
"$.someAlphaNumeric": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z0-9]+"
}
],
"combine": "AND"
},
"$.someUUID": {
"matchers": [
{
"match": "regex",
"regex": "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
}
],
"combine": "AND"
},
"$.someDate": {
"matchers": [
{
"match": "date",
"date": "yyyy-MM-dd"
}
],
"combine": "AND"
},
"$.someTime": {
"matchers": [
{
"match": "time",
"time": "HH:mm:ss"
}
],
"combine": "AND"
},
"$.someDateTime": {
"matchers": [
{
"match": "timestamp",
"timestamp": "yyyy-MM-dd HH:mm:ssZZZ"
}
],
"combine": "AND"
},
"$.someBoolean": {
"matchers": [
{
"match": "regex",
"regex": "(true|false)"
}
],
"combine": "AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json",
"Header-Without-Matcher": "someValue",
"Some-Header": "someValue"
},
"body": {
"someHex": "DEADC0DE",
"someTime": "13:37:00",
"someInteger": 1234567890,
"someBoolean": "true",
"someRegex": 1234567890,
"someDate": "2018-03-26",
"someUUID": "00000000-0000-0000-0000-000000000000",
"someDecimal": 123.123,
"someAlphaNumeric": "Some alpha numeric string with 1234567890",
"someDateTime": "2018-03-26 13:37:00"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json.*"
}
],
"combine": "AND"
},
"Some-Header": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z]{9}"
}
],
"combine": "AND"
}
},
"body": {
"$.someInteger": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
},
"$.someDecimal": {
"matchers": [
{
"match": "decimal"
}
],
"combine": "AND"
},
"$.someHex": {
"matchers": [
{
"match": "regex",
"regex": "[a-fA-F0-9]+"
}
],
"combine": "AND"
},
"$.someAlphaNumeric": {
"matchers": [
{
"match": "regex",
"regex": "[a-zA-Z0-9]+"
}
],
"combine": "AND"
},
"$.someUUID": {
"matchers": [
{
"match": "regex",
"regex": "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
}
],
"combine": "AND"
},
"$.someDate": {
"matchers": [
{
"match": "date",
"date": "yyyy-MM-dd"
}
],
"combine": "AND"
},
"$.someTime": {
"matchers": [
{
"match": "time",
"time": "HH:mm:ss"
}
],
"combine": "AND"
},
"$.someDateTime": {
"matchers": [
{
"match": "timestamp",
"timestamp": "yyyy-MM-dd HH:mm:ssZZZ"
}
],
"combine": "AND"
},
"$.someBoolean": {
"matchers": [
{
"match": "regex",
"regex": "(true|false)"
}
],
"combine": "AND"
},
"$.someNullValue": {
"matchers": [
{
"match": "null"
}
],
"combine": "AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}