Added support for writing contracts in Java

fixes gh-1161
This commit is contained in:
Marcin Grzejszczak
2019-08-12 13:40:23 +02:00
parent e26eff962e
commit 706044bfe5
32 changed files with 1677 additions and 93 deletions

View File

@@ -123,6 +123,9 @@ class TestGenerator {
void generateTestClasses(final String basePackageName) {
ListMultimap<Path, ContractMetadata> contracts = contractFileScanner.
findContracts()
if (log.isDebugEnabled()) {
log.debug("Found the following contracts " + contracts.keySet())
}
Set<Map.Entry<Path,Collection<ContractMetadata>>> inProgress = contracts.asMap().entrySet()
.findAll { Map.Entry<Path, Collection<ContractMetadata>> entry -> entry.value.any { it.anyInProgress() }}
if (!inProgress.isEmpty() && configProperties.failOnInProgress) {

View File

@@ -41,6 +41,7 @@ class ToYamlConverter {
}
private static YamlContractConverter yamlContractConverter = new YamlContractConverter()
private static final List<ContractConverter> CONTRACT_CONVERTERS = converters()
protected static void doReplaceContractWithYaml(ContractConverter converter, File file) {
@@ -105,27 +106,7 @@ class ToYamlConverter {
List<ContractConverter> converters =
SpringFactoriesLoader.loadFactories(ContractConverter, null)
converters.add(YamlContractConverter.INSTANCE)
converters.add(GroovyContractConverter.INSTANCE)
converters.add(ContractVerifierDslConverter.INSTANCE)
return converters
}
}
class GroovyContractConverter implements ContractConverter<Collection<Contract>> {
static final GroovyContractConverter INSTANCE = new GroovyContractConverter()
@Override
boolean isAccepted(File file) {
return file.name.endsWith(".groovy")
}
@Override
Collection<Contract> convertFrom(File file) {
return ContractVerifierDslConverter.convertAsCollection(file)
}
@Override
Collection<Contract> convertTo(Collection<Contract> contract) {
return contract
}
}

View File

@@ -139,6 +139,7 @@ class ContractFileScanner {
protected List<ContractConverter> convertersWithYml() {
List<ContractConverter> converters = converters()
converters.add(ContractVerifierDslConverter.INSTANCE)
converters.add(YamlContractConverter.INSTANCE)
return converters
}
@@ -213,22 +214,7 @@ class ContractFileScanner {
}
private boolean isContractFile(File file) {
return file.isFile() && getFilenameExtension(file.toString())?.equalsIgnoreCase("groovy")
}
private static String getFilenameExtension(String path) {
if (path == null) {
return null
}
int extIndex = path.lastIndexOf('.')
if (extIndex == -1) {
return null
}
int folderIndex = path.lastIndexOf('/')
if (folderIndex > extIndex) {
return null
}
return path.substring(extIndex + 1)
return file.isFile() && ContractVerifierDslConverter.INSTANCE.isAccepted(file)
}
/**

View File

@@ -85,7 +85,7 @@ final class ContractScanner {
contractDescriptors
.addAll(ContractVerifierDslConverter
.convertAsCollection(
file.getParentFile(), file))
file.getParentFile(), file).findAll { it })
}
else if (converter != null
&& converter.isAccepted(file)) {
@@ -120,6 +120,6 @@ final class ContractScanner {
}
private static boolean isContractDescriptor(File file) {
return file.isFile() && file.getName().endsWith(".groovy")
return ContractVerifierDslConverter.INSTANCE.isAccepted(file)
}
}

View File

@@ -16,16 +16,23 @@
package org.springframework.cloud.contract.verifier.util
import java.lang.reflect.Constructor
import java.util.function.Supplier
import java.util.regex.Matcher
import java.util.regex.Pattern
import groovy.transform.CompileStatic
import groovy.util.logging.Commons
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
import org.springframework.cloud.function.compiler.java.CompilationResult
import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler
import org.springframework.util.StringUtils
/**
* Converts a file or String into a {@link Contract}
* Converts a String or a Groovy or Java file into a {@link Contract}.
*
* @author Marcin Grzejszczak
*
@@ -33,7 +40,15 @@ import org.springframework.util.StringUtils
*/
@CompileStatic
@Commons
class ContractVerifierDslConverter {
class ContractVerifierDslConverter implements ContractConverter<Collection<Contract>> {
public static final ContractVerifierDslConverter INSTANCE = new ContractVerifierDslConverter()
private static final Pattern PACKAGE_PATTERN = Pattern.compile(".+?package (.+?);.+?", Pattern.DOTALL)
private static final Pattern CLASS_PATTERN = Pattern.compile(".+?class (.+?)( |\\{).+?", Pattern.DOTALL)
private static final RuntimeJavaCompiler COMPILER = new RuntimeJavaCompiler()
/**
* @deprecated - use {@link ContractVerifierDslConverter#convertAsCollection(java.io.File, java.lang.String)}
@@ -80,7 +95,7 @@ class ContractVerifierDslConverter {
ClassLoader classLoader = ContractVerifierDslConverter.getClassLoader()
try {
ClassLoader urlCl = updatedClassLoader(rootFolder, classLoader)
Object object = groovyShell(urlCl, rootFolder).evaluate(dsl)
Object object = toObject(urlCl, rootFolder, dsl)
return listOfContracts(dsl, object)
}
catch (DslParseException e) {
@@ -106,6 +121,65 @@ class ContractVerifierDslConverter {
return new GroovyShell(ContractVerifierDslConverter.classLoader, new CompilerConfiguration(sourceEncoding: 'UTF-8'))
}
private static Object toObject(ClassLoader cl, File rootFolder, File dsl) {
if (isJava(dsl)) {
try {
return parseJavaFile(dsl)
}
catch (Exception ex) {
if (log.isWarnEnabled()) {
log.warn("Exception occurred while trying to parse the file [" + dsl + "] as a contract. Will not parse it.", ex)
}
return null
}
}
return groovyShell(cl, rootFolder).evaluate(dsl)
}
private static Object parseJavaFile(File dsl) {
Constructor<?> constructor = classConstructor(dsl)
Object newInstance = constructor.newInstance()
if (!newInstance instanceof Supplier) {
if (log.isDebugEnabled()) {
log.debug("The class [" + dsl + "] is not instance of Supplier. Will not parse it as a contract")
}
return null
}
Supplier supplier = (Supplier) newInstance
return supplier.get()
}
private static Constructor<?> classConstructor(File dsl) {
String classText = dsl.text
String fqn = fqn(classText)
CompilationResult compilationResult = COMPILER
.compile(fqn, classText)
if (!compilationResult.wasSuccessful()) {
throw new IllegalStateException("Exceptions occurred while trying to compile the file " + compilationResult.compilationMessages)
}
Class<?> clazz = compilationResult.compiledClasses.find { it.name == fqn}
Constructor<?> constructor = clazz.getDeclaredConstructor()
constructor.setAccessible(true)
return constructor
}
private static boolean isJava(File dsl) {
return dsl.name.endsWith(".java")
}
private static String fqn(String classText) {
Matcher packageMatcher = PACKAGE_PATTERN.matcher(classText)
String fqn = "";
if (packageMatcher.matches()) {
fqn = packageMatcher.group(1) + "."
}
Matcher classMatcher = CLASS_PATTERN.matcher(classText)
if (!classMatcher.matches()) {
throw new IllegalAccessException("Can't parse the class name")
}
return fqn + classMatcher.group(1)
}
private static GroovyShell groovyShell(ClassLoader cl, File rootFolder) {
return new GroovyShell(cl,
new CompilerConfiguration(sourceEncoding: 'UTF-8',
@@ -123,7 +197,10 @@ class ContractVerifierDslConverter {
}
private static Collection<Contract> listOfContracts(File file, Object object) {
if (object instanceof Collection) {
if (object == null) {
return Collections.emptyList()
}
else if (isACollectionOfContracts(object)) {
return withName(file, object as Collection<Contract>)
}
else if (!object instanceof Contract) {
@@ -132,6 +209,10 @@ class ContractVerifierDslConverter {
return withName(file, [object] as Collection<Contract>)
}
private static boolean isACollectionOfContracts(object) {
return object instanceof Collection && ((Collection) object).every { it instanceof Contract }
}
private static Collection<Contract> withName(File file, Collection<Contract> contracts) {
int counter = 0
return contracts.collect {
@@ -146,4 +227,19 @@ class ContractVerifierDslConverter {
private static boolean contractNameEmpty(Contract it) {
return it != null && StringUtils.isEmpty(it.name)
}
@Override
boolean isAccepted(File file) {
return file.name.endsWith(".groovy") || file.name.endsWith(".gvy") || file.name.endsWith(".java")
}
@Override
Collection<Contract> convertFrom(File file) {
return convertAsCollection(file)
}
@Override
Collection<Contract> convertTo(Collection<Contract> contract) {
return contract
}
}

View File

@@ -22,6 +22,10 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
@@ -105,4 +109,95 @@ public final class ContractVerifierUtil {
}
}
/**
* @return a builder for map
*/
public static ContractVerifierMap map() {
return new ContractVerifierMap();
}
/**
* A map with a fluent interface.
*/
public static class ContractVerifierMap implements Map<Object, Object> {
private final Map<Object, Object> delegate = new HashMap<>();
public ContractVerifierMap entry(String key, Object value) {
put(key, value);
return this;
}
@Override
public int size() {
return this.delegate.size();
}
@Override
public boolean isEmpty() {
return this.delegate.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return this.delegate.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return this.delegate.containsValue(value);
}
@Override
public Object get(Object key) {
return this.delegate.get(key);
}
@Override
public Object put(Object key, Object value) {
return this.delegate.put(key, value);
}
@Override
public Object remove(Object key) {
return this.delegate.remove(key);
}
@Override
public void putAll(Map<? extends Object, ?> m) {
this.delegate.putAll(m);
}
@Override
public void clear() {
this.delegate.clear();
}
@Override
public Set<Object> keySet() {
return this.delegate.keySet();
}
@Override
public Collection<Object> values() {
return this.delegate.values();
}
@Override
public Set<Entry<Object, Object>> entrySet() {
return this.delegate.entrySet();
}
@Override
public boolean equals(Object o) {
return this.delegate.equals(o);
}
@Override
public int hashCode() {
return this.delegate.hashCode();
}
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.contract.verifier.builder
import org.junit.Rule
import org.mdkt.compiler.CompilationException
import spock.lang.Issue
import spock.lang.Shared
import spock.lang.Specification
@@ -237,10 +238,10 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
try {
SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test)
}
catch (ClassFormatError classFormatError) {
String output = outputCapture.toString()
assert output.contains('error: cannot find symbol')
assert output.contains('assertThatValueIsANumber(parsedJson.read("$.duck"));')
catch (CompilationException classFormatError) {
String output = classFormatError.message
assert output.contains('cannot find symbol')
assert output.contains('assertThatValueIsANumber')
}
where:
methodBuilderName | methodBuilder | rootElement

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2013-2019 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
*
* https://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.verifier.converter
import groovy.json.JsonSlurper
import spock.lang.Shared
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
import org.springframework.cloud.contract.spec.internal.FromFileProperty
import org.springframework.cloud.contract.spec.internal.RegexProperty
import org.springframework.cloud.contract.spec.internal.Url
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter
import org.springframework.cloud.contract.verifier.util.MapConverter
import static org.springframework.cloud.contract.spec.internal.MatchingType.COMMAND
import static org.springframework.cloud.contract.spec.internal.MatchingType.NULL
import static org.springframework.cloud.contract.spec.internal.MatchingType.REGEX
/**
* @author Marcin Grzejszczak
*/
class JavaContractConverterSpec extends Specification {
@Shared
URL javaRest = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest.java")
@Shared
File javaRestFile = new File(javaRest.toURI())
@Shared
URL javaRestWithTags = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_with_tags.java")
@Shared
File javaRestWithTagsFile = new File(javaRestWithTags.toURI())
@Shared
URL contractBody = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_from_file.java")
@Shared
File contractBodyFile = new File(contractBody.toURI())
@Shared
URL contractBodyBytes = JavaContractConverterSpec.getResource("/contractsToCompile/contract_rest_from_pdf.java")
@Shared
File contractBodyBytesFile = new File(contractBodyBytes.toURI())
@Shared
URL docs = JavaContractConverterSpec.getResource("/contractsToCompile/contract_docs_examples.java")
@Shared
File docsFile = new File(docs.toURI())
def "should convert Java DSL with REST to DSL for [#contractFile]"() {
when:
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), contractFile)
then:
Contract contract = contracts.first()
contract.description == "Some description"
contract.name == "some name"
contract.priority == 8
contract.ignored == true
Url url = contract.request.url
url.clientValue == "/foo"
url.queryParameters.parameters[0].name == "a"
url.queryParameters.parameters[0].serverValue == "b"
url.queryParameters.parameters[1].name == "b"
url.queryParameters.parameters[1].serverValue == "c"
contract.request.method.clientValue == "PUT"
contract.request.headers.entries.find {
it.name == "foo" &&
((RegexProperty) it.clientValue).pattern() == "bar" && it.serverValue == "bar"
}
contract.request.headers.entries.find {
it.name == "fooReq" &&
it.serverValue == "baz"
}
MapConverter.getStubSideValues(contract.request.body) == [foo: "bar"]
contract.request.bodyMatchers.matchers[0].path() == '$.foo'
contract.request.bodyMatchers.matchers[0].matchingType() == REGEX
contract.request.bodyMatchers.matchers[0].value().pattern() == 'bar'
and:
contract.response.status.clientValue == 200
contract.response.delay.clientValue == 1000
contract.response.headers.entries.find {
it.name == "foo2" &&
((RegexProperty) it.serverValue).pattern() == "bar" && it.clientValue == "bar"
}
contract.response.headers.entries.find {
it.name == "foo3" &&
((ExecutionProperty) it.serverValue).insertValue('foo') == "andMeToo(foo)"
}
contract.response.headers.entries.find {
it.name == "fooRes" &&
it.clientValue == "baz"
}
MapConverter.getStubSideValues(contract.response.body) == [foo2: "bar", foo3: "baz", nullValue: null]
contract.response.bodyMatchers.matchers[0].path() == '$.foo2'
contract.response.bodyMatchers.matchers[0].matchingType() == REGEX
contract.response.bodyMatchers.matchers[0].value().pattern() == 'bar'
contract.response.bodyMatchers.matchers[1].path() == '$.foo3'
contract.response.bodyMatchers.matchers[1].matchingType() == COMMAND
contract.response.bodyMatchers.matchers[1].value() == new ExecutionProperty('executeMe($it)')
contract.response.bodyMatchers.matchers[2].path() == '$.nullValue'
contract.response.bodyMatchers.matchers[2].matchingType() == NULL
contract.response.bodyMatchers.matchers[2].value() == null
where:
contractFile << [javaRestFile, javaRestWithTagsFile]
}
def "should convert java with REST with body from file"() {
when:
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), contractBodyFile)
then:
contracts.size() == 1
Contract contract = contracts.first()
new JsonSlurper().parseText(contract.request.body.clientValue.toString()) ==
new JsonSlurper().parseText('''{ "hello" : "request" }''')
and:
new JsonSlurper().parseText(contract.response.body.clientValue.toString()) ==
new JsonSlurper().parseText('''{ "hello" : "response" }''')
}
def "should convert java with REST with body as bytes"() {
when:
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), contractBodyBytesFile)
then:
contracts.size() == 1
Contract contract = contracts.first()
contract.request.body.clientValue instanceof FromFileProperty
and:
contract.response.body.clientValue instanceof FromFileProperty
}
def "should convert java with REST for docs"() {
when:
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), docsFile)
then:
contracts.size() == 1
}
}

View File

@@ -27,6 +27,10 @@ class ContractVerifierDslConverterSpec extends Specification {
URL single = ContractVerifierDslConverterSpec.getResource("/contract.groovy")
File singleContract = new File(single.toURI())
URL singleJava = ContractVerifierDslConverterSpec.getResource("/contractsToCompile/contract.java")
File singleContractJava = new File(singleJava.toURI())
URL singleRestJava = ContractVerifierDslConverterSpec.getResource("/contractsToCompile/contract_rest.java")
File singleContractRestJava = new File(singleRestJava.toURI())
URL multiple = ContractVerifierDslConverterSpec.getResource("/multiple_contracts.groovy")
File multipleContracts = new File(multiple.toURI())
URL invalid = ContractVerifierDslConverterSpec.getResource("/contract.yml")
@@ -51,6 +55,25 @@ class ContractVerifierDslConverterSpec extends Specification {
}
}
Contract expectedSingleContractForJava = Contract.make {
name("contract")
request {
method('PUT')
headers {
contentType(applicationJson())
}
body(""" { "status" : "OK" } """)
url("/1")
}
response {
status OK()
body(""" { "status" : "OK" } """)
headers {
contentType(textPlain())
}
}
}
Contract expectedSingleContractForText = Contract.make {
request {
method('PUT')
@@ -154,6 +177,20 @@ class ContractVerifierDslConverterSpec extends Specification {
contract == [expectedSingleContract]
}
def "should convert file to a list of Contracts when there's only one declared java contract"() {
when:
Collection<Contract> contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContractJava)
then:
contract == [expectedSingleContract]
}
def "should convert file to a list of Contracts for a REST contract for docs"() {
when:
Collection<Contract> contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContractRestJava)
then:
contract == [expectedSingleContractForJava]
}
def "should convert text to a list of Contracts when there's only one declared contract"() {
when:
Collection<Contract> contract = ContractVerifierDslConverter.convertAsCollection(new File("/"), singleContract.text)

View File

@@ -45,6 +45,7 @@ import org.springframework.cloud.contract.verifier.messaging.internal.ContractVe
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil
import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler
import org.springframework.util.ReflectionUtils
/**
* checking the syntax of produced scripts
@@ -53,7 +54,7 @@ import org.springframework.util.ReflectionUtils
@Commons
class SyntaxChecker {
Entity entity
public static final RuntimeJavaCompiler COMPILER = new RuntimeJavaCompiler()
private static final String[] DEFAULT_IMPORTS = [
Contract.name,
@@ -179,20 +180,26 @@ private void test(String test) {
String fqnClassName = "com.example.${className}"
test = test.replaceAll("class FooTest", "class " + className)
.replaceAll("import javax.ws.rs.core.Response", "import javax.ws.rs.core.Response; import javax.ws.rs.client.WebTarget;")
return InMemoryJavaCompiler.compile(fqnClassName, test)
return compileJava(fqnClassName, test)
}
private static Class<?> compileJava(String fqnClassName, String test) {
return InMemoryJavaCompiler.newInstance()
.ignoreWarnings()
.compile(fqnClassName, test)
}
private static String className(String test) {
Random random = new Random()
int first = Math.abs(random.nextInt())
int hashCode = Math.abs(test.hashCode())
StringBuffer sourceCode = new StringBuffer()
String className = "TestClass_${first}_${hashCode}"
return className
}
static boolean tryToCompileJavaWithoutImports(String fqn, String test) {
InMemoryJavaCompiler.compile(fqn, test)
compileJava(fqn, test)
return true
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2013-2019 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
*
* https://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 example;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.method(r.PUT());
r.headers(h -> {
h.contentType(h.applicationJson());
});
r.body(" { \"status\" : \"OK\" } ");
r.url("/1");
});
c.response(r -> {
r.status(r.OK());
r.body(" { \"status\" : \"OK\" } ");
r.headers(h -> {
h.contentType(h.textPlain());
});
});
}));
}
}

View File

@@ -0,0 +1,351 @@
/*
* Copyright 2013-2019 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
*
* https://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.
*/
//tag::class[]
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_docs_examples implements Supplier<Collection<Contract>> {
org.springframework.cloud.contract.spec.Contract httpDsl =
// tag::http_dsl[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
// Definition of HTTP request part of the contract
// (this can be a valid request or invalid depending
// on type of contract being specified).
c.request(r -> {
r.method(r.GET());
r.url("/foo");
// ...
});
// Definition of HTTP response part of the contract
// (a service implementing this contract should respond
// with following response after receiving request
// specified in "request" part above).
c.response(r -> {
r.status(200);
// ...
});
// Contract priority, which can be used for overriding
// contracts (1 is highest). Priority is optional.
c.priority(1);
});
// end::http_dsl[]
org.springframework.cloud.contract.spec.Contract methodDsl = org.springframework.cloud.contract.spec.Contract
.make(c -> {
c.request(r -> {
// tag::method[]
r.method(r.GET());
// end::method[]
r.url("/foo");
});
c.response(r -> {
r.status(200);
});
c.priority(1);
});
org.springframework.cloud.contract.spec.Contract request =
// tag::request[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// HTTP request method (GET/POST/PUT/DELETE).
r.method("GET");
// Path component of request URL is specified as follows.
r.urlPath("/users");
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::request[]
org.springframework.cloud.contract.spec.Contract url =
// tag::url[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
r.method("GET");
// Specifying `url` and `urlPath` in one contract is illegal.
r.url("http://localhost:8888/users");
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::url[]
org.springframework.cloud.contract.spec.Contract urlPaths =
// tag::urlpath[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.urlPath("/users", u -> {
// Each parameter is specified in form
// `'paramName' : paramValue` where parameter value
// may be a simple literal or one of matcher functions,
// all of which are used in this example.
u.queryParameters(q -> {
// If a simple literal is used as value
// default matcher function is used (equalTo)
q.parameter("limit", 100);
// `equalTo` function simply compares passed value
// using identity operator (==).
q.parameter("filter", r.equalTo("email"));
// `containing` function matches strings
// that contains passed substring.
q.parameter("gender", r.value(
r.consumer(r.containing("[mf]")), r.producer("mf")));
// `matching` function tests parameter
// against passed regular expression.
q.parameter("offset", r.value(
r.consumer(r.matching("[0-9]+")), r.producer(123)));
// `notMatching` functions tests if parameter
// does not match passed regular expression.
q.parameter("loginStartsWith", r.value(
r.consumer(r.notMatching(".{0,2}")), r.producer(3)));
});
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::urlpath[]
org.springframework.cloud.contract.spec.Contract headers =
// tag::headers[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Each header is added in form `'Header-Name' : 'Header-Value'`.
// there are also some helper methods
r.headers(h -> {
h.header("key", "value");
h.contentType(h.applicationJson());
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::headers[]
org.springframework.cloud.contract.spec.Contract cookies =
// tag::cookies[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
// there are also some helper methods
r.cookies(ck -> {
ck.cookie("key", "value");
ck.cookie("another_key", "another_value");
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::cookies[]
org.springframework.cloud.contract.spec.Contract body =
// tag::body[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Currently only JSON format of request body is supported.
// Format will be determined from a header or body's content.
r.body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }");
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::body[]
org.springframework.cloud.contract.spec.Contract bodyAsXml =
// tag::bodyAsXml[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// In this case body will be formatted as XML.
r.body(r.equalToXml(
"<user><login>john</login><name>John The Contract</name></user>"));
});
c.response(r -> {
// ...
r.status(200);
});
});
// end::bodyAsXml[]
org.springframework.cloud.contract.spec.Contract response =
// tag::response[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
});
c.response(r -> {
// Status code sent by the server
// in response to request specified above.
r.status(r.OK());
});
});
// end::response[]
org.springframework.cloud.contract.spec.Contract regex =
// tag::regex[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
r.method("GET");
r.url(r.$(r.consumer(r.regex("\\/[0-9]{2}")), r.producer("/12")));
});
c.response(r -> {
r.status(r.OK());
r.body(ContractVerifierUtil.map().entry("id", r.$(r.anyNumber()))
.entry("surname", r.$(r.consumer("Kowalsky"),
r.producer(r.regex("[a-zA-Z]+")))));
r.headers(h -> {
h.header("Content-Type", "text/plain");
});
});
});
// end::regex[]
org.springframework.cloud.contract.spec.Contract optionals =
// tag::optionals[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.priority(1);
c.name("optionals");
c.request(r -> {
r.method("POST");
r.url("/users/password");
r.headers(h -> {
h.contentType(h.applicationJson());
});
r.body(ContractVerifierUtil.map()
.entry("email",
r.$(r.consumer(r.optional(r.regex(r.email()))),
r.producer("abc@abc.com")))
.entry("callback_url", r.$(r.consumer(r.regex(r.hostname())),
r.producer("https://partners.com"))));
});
c.response(r -> {
r.status(404);
r.headers(h -> {
h.header("Content-Type", "application/json");
});
r.body(ContractVerifierUtil.map().entry("code", r.value(
r.consumer("123123"), r.producer(r.optional("123123")))));
});
});
// end::optionals[]
org.springframework.cloud.contract.spec.Contract method =
// tag::methodBuilder[]
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
r.method("PUT");
r.url(r.$(r.consumer(r.regex("^/api/[0-9]{2}$")),
r.producer("/api/12")));
r.headers(h -> {
h.header("Content-Type", "application/json");
});
r.body("[{\"text\": \"Gonna see you at Warsaw\" }]");
});
c.response(r -> {
r.body(ContractVerifierUtil.map()
.entry("path",
r.$(r.consumer("/api/12"),
r.producer(r.regex("^/api/[0-9]{2}$"))))
.entry("correlationId", r.$(r.consumer("1223456"), r
.producer(r.execute("isProperCorrelationId($it)")))));
r.status(r.OK());
});
});
// end::methodBuilder[]
@Override
public Collection<Contract> get() {
return Collections.singletonList(method);
}
}
// end::class[]

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2019 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
*
* https://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.
*/
//tag::class[]
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.Request;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_multipart implements Supplier<Collection<Contract>> {
private static Map<String, DslProperty> namedProps(Request r) {
Map<String, DslProperty> map = new HashMap<>();
// name of the file
map.put("name", r.$(r.c(r.regex(r.nonEmpty())), r.p("filename.csv")));
// content of the file
map.put("content", r.$(r.c(r.regex(r.nonEmpty())), r.p("file content")));
// content type for the part
map.put("contentType", r.$(r.c(r.regex(r.nonEmpty())), r.p("application/json")));
return map;
}
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.method("PUT");
r.url("/multipart");
r.headers(h -> {
h.contentType("multipart/form-data;boundary=AaB03x");
});
r.multipart(ContractVerifierUtil.map()
// key (parameter name), value (parameter value) pair
.entry("formParameter",
r.$(r.c(r.regex("\".+\"")),
r.p("\"formParameterValue\"")))
.entry("someBooleanParameter",
r.$(r.c(r.regex(r.anyBoolean())), r.p("true")))
// a named parameter (e.g. with `file` name) that represents file
// with
// `name` and `content`. You can also call `named("fileName",
// "fileContent")`
.entry("file", r.named(namedProps(r))));
});
c.response(r -> {
r.status(r.OK());
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2019 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
*
* https://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.
*/
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_rest implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.description("Some description");
c.name("some name");
c.priority(8);
c.ignored();
c.request(r -> {
r.url("/foo", u -> {
u.queryParameters(q -> {
q.parameter("a", "b");
q.parameter("b", "c");
});
});
r.method(r.PUT());
r.headers(h -> {
h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar")));
h.header("fooReq", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo", "bar"));
r.bodyMatchers(m -> {
m.jsonPath("$.foo", m.byRegex("bar"));
});
});
c.response(r -> {
r.fixedDelayMilliseconds(1000);
r.status(r.OK());
r.headers(h -> {
h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar")));
h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")),
r.client("foo33")));
h.header("fooRes", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo2", "bar")
.entry("foo3", "baz").entry("nullValue", null));
r.bodyMatchers(m -> {
m.jsonPath("$.foo2", m.byRegex("bar"));
m.jsonPath("$.foo3", m.byCommand("executeMe($it)"));
m.jsonPath("$.nullValue", m.byNull());
});
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2013-2019 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
*
* https://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.
*/
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract_rest_from_file implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.url("/foo");
r.method(r.GET());
r.body(r.file("request.json"));
});
c.response(r -> {
r.status(r.OK());
r.body(r.file("response.json"));
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2013-2019 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
*
* https://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.
*/
//tag::class[]
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract_rest_from_pdf implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.url("/1");
r.method(r.PUT());
r.body(r.fileAsBytes("request.pdf"));
r.headers(h -> {
h.contentType(h.applicationOctetStream());
});
});
c.response(r -> {
r.status(r.OK());
r.body(r.fileAsBytes("response.pdf"));
r.headers(h -> {
h.contentType(h.applicationOctetStream());
});
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2013-2019 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
*
* https://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.
*/
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_rest_with_tags implements Supplier<Collection<Contract>> {
static Object description = Collections.singletonList(
// tag::description[]
Contract.make(c -> {
c.description("Some description");
}));
// end::description[]
static Object name = Collections.singletonList(
// tag::name[]
Contract.make(c -> {
c.name("some name");
}));
// end::name[]
static Object ignored = Collections.singletonList(
// tag::ignored[]
Contract.make(c -> {
c.ignored();
}));
// end::ignored[]
static Object in_progress = Collections.singletonList(
// tag::in_progress[]
Contract.make(c -> {
c.inProgress();
}));
// end::in_progress[]
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.description("Some description");
c.name("some name");
c.priority(8);
c.ignored();
c.inProgress();
c.request(r -> {
r.url("/foo", u -> {
u.queryParameters(q -> {
q.parameter("a", "b");
q.parameter("b", "c");
});
});
r.method(r.PUT());
r.headers(h -> {
h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar")));
h.header("fooReq", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo", "bar"));
r.bodyMatchers(m -> {
m.jsonPath("$.foo", m.byRegex("bar"));
});
});
c.response(r -> {
r.fixedDelayMilliseconds(1000);
r.status(r.OK());
r.headers(h -> {
h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar")));
h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")),
r.client("foo33")));
h.header("fooRes", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo2", "bar")
.entry("foo3", "baz").entry("nullValue", null));
r.bodyMatchers(m -> {
m.jsonPath("$.foo2", m.byRegex("bar"));
m.jsonPath("$.foo3", m.byCommand("executeMe($it)"));
m.jsonPath("$.nullValue", m.byNull());
});
});
}));
}
}
// end::class[]

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2013-2019 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
*
* https://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.
*/
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract_xml implements Supplier<Contract> {
@Override
public Contract get() {
return Contract.make(c -> {
c.request(r -> {
r.method(r.GET());
r.urlPath("/get");
r.headers(h -> {
h.contentType(h.applicationXml());
});
});
c.response(r -> {
r.status(r.OK());
r.headers(h -> {
h.contentType(h.applicationXml());
});
r.body("<test>\n" + "<duck type='xtype'>123</duck>\n"
+ "<alpha>abc</alpha>\n" + "<list>\n" + "<elem>abc</elem>\n"
+ "<elem>def</elem>\n" + "<elem>ghi</elem>\n" + "</list>\n"
+ "<number>123</number>\n" + "<aBoolean>true</aBoolean>\n"
+ "<date>2017-01-01</date>\n"
+ "<dateTime>2017-01-01T01:23:45</dateTime>\n"
+ "<time>01:02:34</time>\n"
+ "<valueWithoutAMatcher>foo</valueWithoutAMatcher>\n"
+ "<key><complex>foo</complex></key>\n" + "</test>");
r.bodyMatchers(m -> {
m.xPath("/test/duck/text()", m.byRegex("[0-9]{3}"));
m.xPath("/test/duck/text()", m.byCommand("equals($it)"));
m.xPath("/test/duck/xxx", m.byNull());
m.xPath("/test/duck/text()", m.byEquality());
m.xPath("/test/alpha/text()", m.byRegex(r.onlyAlphaUnicode()));
m.xPath("/test/alpha/text()", m.byEquality());
m.xPath("/test/number/text()", m.byRegex(r.number()));
m.xPath("/test/date/text()", m.byDate());
m.xPath("/test/dateTime/text()", m.byTimestamp());
m.xPath("/test/time/text()", m.byTime());
m.xPath("/test/*/complex/text()", m.byEquality());
m.xPath("/test/duck/@type", m.byEquality());
});
});
});
};
}

View File

@@ -0,0 +1,3 @@
{
"hello": "request"
}

View File

@@ -0,0 +1,3 @@
{
"hello": "response"
}

View File

@@ -9,6 +9,8 @@
<logger name="com.github.jknack.handlebars.internal" level="INFO"/>
<logger name="org.springframework.cloud.function" level="INFO"/>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>