Add an option to dump contracts into a common representation

Allow an option to dump contracts as files for the given ContractConverter. In other words, if you have contracts written in e,g, groovy and yml, by providing a FQN of org.springframework.cloud.contract.verifier.converter.YamlContractConverter, you will be able to dump all files in a folder in a yml format.

fixes gh-793
This commit is contained in:
Marcin Grzejszczak
2018-11-10 23:53:18 +01:00
parent 3109d5b610
commit 6210c42162
10 changed files with 428 additions and 60 deletions

View File

@@ -25,7 +25,7 @@ package org.springframework.cloud.contract.spec
* @author Marcin Grzejszczak
* @since 1.1.0
*/
interface ContractConverter<T> {
interface ContractConverter<T> extends ContractStorer<T> {
/**
* Should this file be accepted by the converter. Can use the file extension

View File

@@ -0,0 +1,25 @@
package org.springframework.cloud.contract.spec;
import java.util.HashMap;
import java.util.Map;
/**
* Defines how to store converted contracts to a String representation
* that can be stored to drive
*
* @author Marcin Grzejszczak
* @since 2.1.0
*/
public interface ContractStorer<T> {
/**
* Stores the contracts as a map of filename and String
*
* @param contracts - to convert
* @return mapping of filename to converted String representation of the contract
*/
default Map<String, String> storeAsString(T contracts) {
Map<String, String> map = new HashMap<>();
map.put(String.valueOf(Math.abs(hashCode())), contracts.toString());
return map;
}
}

View File

@@ -34,8 +34,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.ContractConverter;
import org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStub;
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter;
import org.springframework.cloud.contract.verifier.util.ContractScanner;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
@@ -157,50 +156,11 @@ class StubRepository {
}
private Collection<Contract> contractDescriptors() {
return (this.path.exists() ? collectContractDescriptors(this.path)
return (this.path.exists() ?
ContractScanner.collectContractDescriptors(this.path, this::isStubPerConsumerPathMatching)
: Collections.<Contract>emptySet());
}
@SuppressWarnings("unchecked")
private Collection<Contract> collectContractDescriptors(
final File descriptorsDirectory) {
final List<Contract> contractDescriptors = new ArrayList<>();
try {
Files.walkFileTree(Paths.get(descriptorsDirectory.toURI()),
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path path,
BasicFileAttributes attrs) throws IOException {
File file = path.toFile();
ContractConverter converter = contractConverter(file);
if (isStubPerConsumerPathMatching(file)) {
if (isContractDescriptor(file)) {
contractDescriptors
.addAll(ContractVerifierDslConverter
.convertAsCollection(
file.getParentFile(), file));
}
else if (converter != null
&& converter.isAccepted(file)) {
contractDescriptors
.addAll(converter.convertFrom(file));
}
else if (YamlContractConverter.INSTANCE
.isAccepted(file)) {
contractDescriptors
.addAll(YamlContractConverter.INSTANCE
.convertFrom(file));
}
}
return super.visitFile(path, attrs);
}
});
}
catch (IOException e) {
log.warn("Exception occurred while trying to parse file", e);
}
return contractDescriptors;
}
private boolean isStubPerConsumerPathMatching(File file) {
if (!this.options.isStubsPerConsumer()) {
@@ -218,9 +178,4 @@ class StubRepository {
return stubPerConsumerMatching;
}
private static boolean isContractDescriptor(File file) {
// TODO: Consider script injections implications...
return file.isFile() && file.getName().endsWith(".groovy");
}
}

View File

@@ -17,8 +17,10 @@ package org.springframework.cloud.contract.verifier.spec.pact
import au.com.dius.pact.model.Pact
import au.com.dius.pact.model.PactReader
import au.com.dius.pact.model.PactSpecVersion
import au.com.dius.pact.model.RequestResponsePact
import au.com.dius.pact.model.v3.messaging.MessagePact
import groovy.json.JsonOutput
import groovy.transform.CompileStatic
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
@@ -73,4 +75,15 @@ class PactContractConverter implements ContractConverter<Collection<Pact>> {
}
return pactContracts
}
@Override
Map<String, String> storeAsString(Collection<Pact> contracts) {
return contracts.collectEntries {
return [(name(it)) : JsonOutput.prettyPrint(JsonOutput.toJson(it.toMap(PactSpecVersion.V3)))]
}
}
protected String name(Pact contract) {
return contract.consumer.name + "_" + contract.provider.name + "_" + String.valueOf(Math.abs(contract.hashCode())) + ".json"
}
}

View File

@@ -417,6 +417,23 @@ class PactContractConverterSpec extends Specification {
convertedPactAsText, false)
}
def "should convert pacts to strings"() {
given:
List<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"),
new File("src/test/resources/contracts/grouped/shouldWorkWithBeer.groovy"))
and:
Collection<Pact> pacts = converter.convertTo(contracts)
when:
Map<String, String> strings = converter.storeAsString(pacts)
then:
strings.size() == 1
strings.keySet().first().startsWith("10-04-pact-consumer_10-05-pact-producer_")
strings.keySet().first().endsWith(".json")
JSONAssert.assertEquals(
new File("src/test/resources/contracts/grouped/shouldWorkWithBeer.json").text,
strings.values().first(), false)
}
def "should convert from pact v2 to two SC contracts"() {
given:
Collection<Contract> expectedContracts = [

View File

@@ -16,9 +16,8 @@
package org.springframework.cloud.contract.verifier.converter
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
import groovy.transform.CompileStatic
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
/**
@@ -32,6 +31,7 @@ import org.springframework.cloud.contract.spec.ContractConverter
class YamlContractConverter implements ContractConverter<List<YamlContract>> {
public static final YamlContractConverter INSTANCE = new YamlContractConverter()
private final YAMLMapper mapper = new YAMLMapper()
private final YamlToContracts yamlToContracts = new YamlToContracts()
private final ContractsToYaml contractsToYaml = new ContractsToYaml()
@@ -50,4 +50,17 @@ class YamlContractConverter implements ContractConverter<List<YamlContract>> {
List<YamlContract> convertTo(Collection<Contract> contracts) {
return this.contractsToYaml.convertTo(contracts)
}
@Override
Map<String, String> storeAsString(List<YamlContract> contracts) {
return contracts.collectEntries {
return [(name(it)) :
this.mapper.writeValueAsString(it)]
}
}
protected String name(YamlContract contract) {
return (contract.name ?:
String.valueOf(Math.abs(contract.hashCode()))) + ".yml"
}
}

View File

@@ -0,0 +1,123 @@
/*
* 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.verifier.util
import groovy.transform.CompileStatic
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter
import org.springframework.core.io.support.SpringFactoriesLoader
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import java.util.function.Predicate
/**
* Scans through the given directory and converts all files for
* contract definitions.
*
* @author Marcin Grzejszczak
* @since 2.1.0
*/
@CompileStatic
final class ContractScanner {
private static final Log log = LogFactory.getLog(ContractScanner.class);
/**
* Traverses through the directories, applies converters
* to files that match them and converts the files to {@link Contract}.
* No additional file filtering takes place.
*
* @param rootDirectory - directory to traverse through
* @return collection of converted contracts
*/
@SuppressWarnings("unchecked")
static Collection<Contract> collectContractDescriptors(
final File rootDirectory) {
return collectContractDescriptors(rootDirectory, { true })
}
/**
* Traverses through the directories, applies converters
* to files that match them and converts the files to {@link Contract}.
* Filters out files not matching a predicate.
*
* @param rootDirectory - directory to traverse through
* @param predicate - test applied against a file
* @return collection of converted contracts
*/
@SuppressWarnings("unchecked")
static Collection<Contract> collectContractDescriptors(
final File rootDirectory, Predicate<File> predicate) {
final List<Contract> contractDescriptors = new ArrayList<>()
try {
Files.walkFileTree(Paths.get(rootDirectory.toURI()),
new SimpleFileVisitor<Path>() {
@Override
FileVisitResult visitFile(Path path,
BasicFileAttributes attrs) throws IOException {
File file = path.toFile()
ContractConverter converter = contractConverter(file)
if (predicate.test(file)) {
if (isContractDescriptor(file)) {
contractDescriptors
.addAll(ContractVerifierDslConverter
.convertAsCollection(
file.getParentFile(), file))
}
else if (converter != null
&& converter.isAccepted(file)) {
contractDescriptors
.addAll(converter.convertFrom(file))
}
else if (YamlContractConverter.INSTANCE
.isAccepted(file)) {
contractDescriptors
.addAll(YamlContractConverter.INSTANCE
.convertFrom(file))
}
}
return super.visitFile(path, attrs)
}
})
}
catch (IOException e) {
log.warn("Exception occurred while trying to parse file", e)
}
return contractDescriptors
}
private static ContractConverter contractConverter(File file) {
for (ContractConverter converter : SpringFactoriesLoader
.loadFactories(ContractConverter.class, null)) {
if (converter.isAccepted(file)) {
return converter
}
}
return null
}
private static boolean isContractDescriptor(File file) {
return file.isFile() && file.getName().endsWith(".groovy")
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.verifier.util
import groovy.transform.CompileStatic
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
import java.nio.file.Files
/**
* Allows conversion of Contract files to files.
*
* WARNING: This class is incubating and experimental. It might change in the future.
*
* @author Marcin Grzejszczak
* @since 2.1.0
*/
@CompileStatic
final class ToFileContractsTransformer {
private static final Log log = LogFactory.getLog(ToFileContractsTransformer.class)
/**
* Dumps contracts as files for the given {@link ContractConverter}
*
* - argument 1 : FQN - fully qualified name of the {@link ContractConverter} [REQUIRED]
* - argument 2 : path - path where the dumped files should be stored [OPTIONAL - defaults to target/converted-contracts]
* - argument 3 : path - path were the contracts should be searched for [OPTIONAL - defaults to src/test/resources/contracts]
*/
static void main(String[] args) {
if (args.length == 0) {
throw new IllegalStateException(exceptionMessage())
}
String fqn = args[0]
String outputPath = args.length >= 2 ? args[1] : "target/converted-contracts"
String path = args.length >= 3 ? args[2] : "src/test/resources/contracts"
new ToFileContractsTransformer().storeContractsAsFiles(path, fqn, outputPath)
}
private static String exceptionMessage() {
return "Please provide the FQN of the ContractConverter. E.g. [org.springframework.cloud.contract.verifier.converter.YamlContractConverter]"
}
/**
*
* @param path - path were the contracts should be searched for
* @param fqn - fully qualified name of the {@link ContractConverter}
* @param outputPath - path where the dumped files should be stored
* @return list of dumped files
*/
final List<File> storeContractsAsFiles(String path, String fqn, String outputPath) {
try {
log.info("Input path [" + path + "]")
log.info("FQN of the converter [" + fqn + "]")
log.info("Output path [" + outputPath + "]")
Collection<Contract> contracts = ContractScanner.collectContractDescriptors(new File(path))
log.info("Found [" + contracts.size() + "] contract definition")
Class<?> name = Class.forName(fqn)
ContractConverter<Collection> contractConverter = (ContractConverter) name.newInstance()
Collection converted = contractConverter.convertTo(contracts)
log.info("Successfully converted contracts definitions")
Map<String, String> stored = contractConverter.storeAsString(converted)
File outputFolder = new File(outputPath)
outputFolder.mkdirs()
int i = 1
Set<Map.Entry<String, String>> entries = stored.entrySet()
log.info("Will convert [" + entries.size() + "] contracts")
List<File> files = new ArrayList<>()
for (Map.Entry<String, String> entry : entries) {
File outputFile = new File(outputFolder, entry.getKey())
Files.write(outputFile.toPath(), entry.getValue().getBytes())
log.info("[" + i + "/" + entries.size() + "] Successfully stored [" + outputFile.getName() + "]")
files.add(outputFile)
}
return files
} catch (Exception ex) {
throw new IllegalStateException(ex)
}
}
}

View File

@@ -16,23 +16,20 @@
package org.springframework.cloud.contract.verifier.converter
import org.springframework.cloud.contract.spec.internal.MatchingStrategy
import org.springframework.cloud.contract.spec.internal.QueryParameters
import spock.lang.Issue
import java.util.regex.Pattern
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.MatchingStrategy
import org.springframework.cloud.contract.spec.internal.MatchingType
import org.springframework.cloud.contract.spec.internal.NamedProperty
import org.springframework.cloud.contract.spec.internal.QueryParameters
import org.springframework.cloud.contract.spec.internal.RegexPatterns
import org.springframework.cloud.contract.spec.internal.Url
import org.springframework.cloud.contract.verifier.util.MapConverter
import spock.lang.Issue
import spock.lang.Shared
import spock.lang.Specification
import java.util.regex.Pattern
/**
* @author Marcin Grzejszczak
* @author Tim Ysewyn
@@ -538,6 +535,104 @@ class YamlContractConverterSpec extends Specification {
contracts.last().request.url.clientValue == "/users/2"
}
def "should dump yml as string"() {
given:
String expectedYaml1 = '''\
---
request:
method: "POST"
url: "/users/1"
urlPath: null
queryParameters: {}
headers: {}
cookies: {}
body: null
bodyFromFile: null
matchers:
url: null
body: []
headers: []
queryParameters: []
cookies: []
multipart: null
multipart: null
response:
status: 200
headers: {}
cookies: {}
body: null
bodyFromFile: null
matchers:
body: []
headers: []
cookies: []
async: null
fixedDelayMilliseconds: null
input: null
outputMessage: null
description: null
label: null
name: "post1"
priority: null
ignored: false
'''
String expectedYaml2 = '''\
---
request:
method: "POST"
url: "/users/2"
urlPath: null
queryParameters: {}
headers: {}
cookies: {}
body: null
bodyFromFile: null
matchers:
url: null
body: []
headers: []
queryParameters: []
cookies: []
multipart: null
multipart: null
response:
status: 200
headers: {}
cookies: {}
body: null
bodyFromFile: null
matchers:
body: []
headers: []
cookies: []
async: null
fixedDelayMilliseconds: null
input: null
outputMessage: null
description: null
label: null
name: "post2"
priority: null
ignored: false
'''
when:
Map<String, String> strings = converter.storeAsString([
new YamlContract(
name: "post1",
request: new YamlContract.Request(method: "POST", url: "/users/1"),
response: new YamlContract.Response(status: 200)
),new YamlContract(
name: "post2",
request: new YamlContract.Request(method: "POST", url: "/users/2"),
response: new YamlContract.Response(status: 200)
),
])
then:
strings.size() == 2
strings["post1.yml"].trim() == expectedYaml1.trim()
strings["post2.yml"].trim() == expectedYaml2.trim()
}
def "should parse messaging contract for [#file]"() {
given:
assert converter.isAccepted(file)

View File

@@ -0,0 +1,31 @@
package org.springframework.cloud.contract.verifier.util
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter
import spock.lang.Specification
/**
* @author Marcin Grzejszczak
* @since
*/
class ToFileContractsTransformerSpec extends Specification {
@Rule TemporaryFolder tmp = new TemporaryFolder()
File folder
def setup() {
folder = tmp.newFolder()
}
def "should store contracts as files"() {
given:
File input = new File("src/test/resources/dsl")
String fqn = YamlContractConverter.name
when:
List<File> files = new ToFileContractsTransformer().storeContractsAsFiles(input.absolutePath, fqn, folder.absolutePath)
then:
files.size() == 1
files.get(0).name.endsWith(".yml")
}
}