diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractConverter.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractConverter.groovy index 5ce9775d57..e8ec69d280 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractConverter.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractConverter.groovy @@ -25,7 +25,7 @@ package org.springframework.cloud.contract.spec * @author Marcin Grzejszczak * @since 1.1.0 */ -interface ContractConverter { +interface ContractConverter extends ContractStorer { /** * Should this file be accepted by the converter. Can use the file extension diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractStorer.java b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractStorer.java new file mode 100644 index 0000000000..dc94ffb2d7 --- /dev/null +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractStorer.java @@ -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 { + /** + * 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 storeAsString(T contracts) { + Map map = new HashMap<>(); + map.put(String.valueOf(Math.abs(hashCode())), contracts.toString()); + return map; + } +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java index 1b1c660882..08a1fbdf82 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java @@ -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 contractDescriptors() { - return (this.path.exists() ? collectContractDescriptors(this.path) + return (this.path.exists() ? + ContractScanner.collectContractDescriptors(this.path, this::isStubPerConsumerPathMatching) : Collections.emptySet()); } - @SuppressWarnings("unchecked") - private Collection collectContractDescriptors( - final File descriptorsDirectory) { - final List contractDescriptors = new ArrayList<>(); - try { - Files.walkFileTree(Paths.get(descriptorsDirectory.toURI()), - new SimpleFileVisitor() { - @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"); - } - } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy index 49161f87d5..af0e3ea9e9 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy @@ -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> { } return pactContracts } + + @Override + Map storeAsString(Collection 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" + } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy index b94c30b2bd..39e87e7674 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy @@ -417,6 +417,23 @@ class PactContractConverterSpec extends Specification { convertedPactAsText, false) } + def "should convert pacts to strings"() { + given: + List contracts = ContractVerifierDslConverter.convertAsCollection(new File("/"), + new File("src/test/resources/contracts/grouped/shouldWorkWithBeer.groovy")) + and: + Collection pacts = converter.convertTo(contracts) + when: + Map 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 expectedContracts = [ diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy index f9633d6c96..23abecf05c 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy @@ -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> { 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 convertTo(Collection contracts) { return this.contractsToYaml.convertTo(contracts) } + + @Override + Map storeAsString(List 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" + } } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractScanner.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractScanner.groovy new file mode 100644 index 0000000000..6375c0d62b --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractScanner.groovy @@ -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 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 collectContractDescriptors( + final File rootDirectory, Predicate predicate) { + final List contractDescriptors = new ArrayList<>() + try { + Files.walkFileTree(Paths.get(rootDirectory.toURI()), + new SimpleFileVisitor() { + @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") + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ToFileContractsTransformer.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ToFileContractsTransformer.groovy new file mode 100644 index 0000000000..43c4cfdfcb --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ToFileContractsTransformer.groovy @@ -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 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 contracts = ContractScanner.collectContractDescriptors(new File(path)) + log.info("Found [" + contracts.size() + "] contract definition") + Class name = Class.forName(fqn) + ContractConverter contractConverter = (ContractConverter) name.newInstance() + Collection converted = contractConverter.convertTo(contracts) + log.info("Successfully converted contracts definitions") + Map stored = contractConverter.storeAsString(converted) + File outputFolder = new File(outputPath) + outputFolder.mkdirs() + int i = 1 + Set> entries = stored.entrySet() + log.info("Will convert [" + entries.size() + "] contracts") + List files = new ArrayList<>() + for (Map.Entry 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) + } + } +} + diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy index 8494538dbe..039bb82f53 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy @@ -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 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) diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ToFileContractsTransformerSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ToFileContractsTransformerSpec.groovy new file mode 100644 index 0000000000..fd2fff4c75 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ToFileContractsTransformerSpec.groovy @@ -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 files = new ToFileContractsTransformer().storeContractsAsFiles(input.absolutePath, fqn, folder.absolutePath) + then: + files.size() == 1 + files.get(0).name.endsWith(".yml") + } +}