Generate stubs at runtime (#1152)

* Generate stubs at runtime

without this change the producer side must publish the stubs for the consumer side to use them
with this change the consumer can toggle a switch so that the stubs get generated at runtime. This might of course lead to false positivies but we assume that the users know what they're doing

fixes gh-881
This commit is contained in:
Marcin Grzejszczak
2019-08-05 10:44:35 +02:00
committed by GitHub
parent d97db26001
commit dbb1af8d98
24 changed files with 501 additions and 97 deletions

View File

@@ -1820,7 +1820,7 @@ own implementation of the `StubGenerator` interface. The following code listing
[source,groovy]
----
include::{converters_path}/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.groovy[indent=0,lines=16..-1]
include::{converters_path}/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGenerator.java[indent=0,lines=16..-1]
----
Again, you must provide a `spring.factories` file, such as the one shown in the following

View File

@@ -0,0 +1,56 @@
/*
* 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 com.example.loan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerPort;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
// tag::autoconfigure_stubrunner[]
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {
"com.example:http-server-dsl"},
repositoryRoot = "stubs://classpath://contractsAtRuntime/",
stubsMode = StubRunnerProperties.StubsMode.LOCAL,
generateStubs = true)
public class GoodbyeWorldTests {
// end::autoconfigure_stubrunner[]
@StubRunnerPort("http-server-dsl")
int port;
@Test
public void shouldGenerateStubsAtRuntime() {
// when:
String response = new RestTemplate().getForObject("http://localhost:" + this.port + "/goodbye", String.class);
// then:
assertThat(response)
.isEqualTo("Goodbye World!");
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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 org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
url("/goodbye")
method(GET())
}
response {
status(OK())
body("Goodbye World!")
}
}

View File

@@ -22,6 +22,10 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-shade</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-converters</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>

View File

@@ -0,0 +1,82 @@
/*
* 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.stubrunner;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
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.util.StringUtils;
final class MappingGenerator {
private static final Log log = LogFactory.getLog(MappingGenerator.class);
private MappingGenerator() {
throw new IllegalStateException("Can't instantiate utility class");
}
static Collection<Path> toMappings(File contractFile, Collection<Contract> contracts,
File mappingsFolder) {
StubGeneratorProvider provider = new StubGeneratorProvider();
Collection<StubGenerator> stubGenerators = provider
.converterForName(contractFile.getName());
if (log.isDebugEnabled()) {
log.debug("Found following matching stub generators " + stubGenerators);
}
Collection<Path> mappings = new LinkedList<>();
for (StubGenerator stubGenerator : stubGenerators) {
Map<Contract, String> map = stubGenerator.convertContents(
contractFile.getName(), new ContractMetadata(contractFile.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(contractFile.getName()) + "_"
+ Math.abs(entry.getKey().hashCode())
+ stubGenerator.fileExtension());
mappings.add(storeFile(mapping.toPath(), value.getBytes()));
}
}
return mappings;
}
private static Path storeFile(Path path, byte[] contents) {
try {
Path storedPath = Files.write(path, contents);
if (log.isDebugEnabled()) {
log.debug("Stored file [" + path.toString() + "]");
}
return storedPath;
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -17,7 +17,13 @@
package org.springframework.cloud.contract.stubrunner;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.io.IOException;
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.ArrayList;
import java.util.Collection;
import java.util.Map;
@@ -25,7 +31,12 @@ import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter;
import org.springframework.cloud.contract.verifier.converter.StubGenerator;
import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.core.io.Resource;
/**
* Factory of StubRunners. Basing on the options and passed collaborators downloads the
@@ -33,8 +44,7 @@ import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
*/
class StubRunnerFactory {
private static final Log log = LogFactory
.getLog(MethodHandles.lookup().lookupClass());
private static final Log log = LogFactory.getLog(StubRunnerFactory.class);
private final StubRunnerOptions stubRunnerOptions;
@@ -69,13 +79,93 @@ class StubRunnerFactory {
+ "] the downloaded entry is [" + entry + "]");
}
if (entry != null) {
result.add(createStubRunner(entry.getKey(), entry.getValue()));
Path path = resolvePath(entry.getValue());
File unpackedLocation = path.toFile();
if (this.stubRunnerOptions.isGenerateStubs()) {
if (log.isDebugEnabled()) {
log.debug(
"Flag to generate stubs at runtime was switched on. Will remove the current mappings and will generate new ones.");
}
generateMappingsAtRuntime(path);
}
result.add(createStubRunner(entry.getKey(), unpackedLocation));
}
}
return result;
}
private void generateMappingsAtRuntime(Path path) {
removeCurrentMappings(path);
generateNewMappings(path);
}
private Path resolvePath(File unpackedLocation) {
Resource resource = ResourceResolver.resource(unpackedLocation.getPath());
Path path = unpackedLocation.toPath();
if (resource != null) {
try {
return Paths.get(resource.getURI());
}
catch (IOException ex) {
return unpackedLocation.toPath();
}
}
return path;
}
private void removeCurrentMappings(Path path) {
try {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
private final Log log = LogFactory.getLog(StubRunnerFactory.class);
private final StubGeneratorProvider provider = new StubGeneratorProvider();
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
Collection<StubGenerator> stubGenerators = this.provider
.converterForName(file.toString());
if (!stubGenerators.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Deleting file [" + file.toString()
+ "] since at least one stub generator would run it");
}
try {
Files.delete(file);
}
catch (IOException ex) {
log.warn("Failed to delete file [" + file.toString() + "]",
ex);
}
}
return FileVisitResult.CONTINUE;
}
});
}
catch (IOException ex) {
log.warn("Exception occurred while trying to delete mappings", ex);
}
}
private void generateNewMappings(Path path) {
File unpackedLocation = path.toFile();
ContractVerifierConfigProperties configProperties = new ContractVerifierConfigProperties();
configProperties
.setContractsDslDir(subfolderIfPresent(unpackedLocation, "contracts"));
configProperties
.setStubsOutputDir(subfolderIfPresent(unpackedLocation, "mappings"));
RecursiveFilesConverter converter = new RecursiveFilesConverter(configProperties);
converter.processFiles();
}
private File subfolderIfPresent(File unpackedLocation, String subfolder) {
File subfolderDir = new File(unpackedLocation, subfolder);
if (subfolderDir.exists()) {
return subfolderDir;
}
return unpackedLocation;
}
private StubRunner createStubRunner(StubConfiguration stubsConfiguration,
File unzipedStubDir) {
if (unzipedStubDir == null) {

View File

@@ -116,6 +116,12 @@ public class StubRunnerOptions {
*/
private boolean deleteStubsAfterTest;
/**
* When enabled, this flag will tell stub runner to not load the generated stubs, but
* convert the found contracts at runtime to a stub format and run those stubs.
*/
private boolean generateStubs;
/**
* Map of properties that can be passed to custom
* {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}.
@@ -128,7 +134,8 @@ public class StubRunnerOptions {
Map<StubConfiguration, Integer> stubIdsToPortMapping, String username,
String password, final StubRunnerProxyOptions stubRunnerProxyOptions,
boolean stubsPerConsumer, String consumerName, String mappingsOutputFolder,
boolean deleteStubsAfterTest, Map<String, String> properties,
boolean deleteStubsAfterTest, boolean generateStubs,
Map<String, String> properties,
Class<? extends HttpServerStubConfigurer> httpServerStubConfigurer) {
this.minPortValue = minPortValue;
this.maxPortValue = maxPortValue;
@@ -145,6 +152,7 @@ public class StubRunnerOptions {
this.consumerName = consumerName;
this.mappingsOutputFolder = mappingsOutputFolder;
this.deleteStubsAfterTest = deleteStubsAfterTest;
this.generateStubs = generateStubs;
this.properties = properties;
this.httpServerStubConfigurer = httpServerStubConfigurer;
}
@@ -169,6 +177,8 @@ public class StubRunnerOptions {
System.getProperty("stubrunner.mappings-output-folder"))
.withDeleteStubsAfterTest(Boolean.parseBoolean(
System.getProperty("stubrunner.delete-stubs-after-test", "true")))
.withGenerateStubs(Boolean.parseBoolean(
System.getProperty("stubrunner.generate-stubs", "false")))
.withProperties(stubRunnerProps());
builder = httpStubConfigurer(builder);
String proxyHost = System.getProperty("stubrunner.proxy.host");
@@ -318,6 +328,10 @@ public class StubRunnerOptions {
this.deleteStubsAfterTest = deleteStubsAfterTest;
}
public boolean isGenerateStubs() {
return this.generateStubs;
}
public Map<String, String> getProperties() {
return this.properties;
}

View File

@@ -69,6 +69,8 @@ public class StubRunnerOptionsBuilder {
private boolean deleteStubsAfterTest = true;
private boolean generateStubs;
private Map<String, String> properties = new HashMap<>();
private Class httpServerStubConfigurer = HttpServerStubConfigurer.NoOpHttpServerStubConfigurer.class;
@@ -194,6 +196,7 @@ public class StubRunnerOptionsBuilder {
this.stubIdsToPortMapping = options.stubIdsToPortMapping != null
? options.stubIdsToPortMapping : new LinkedHashMap<>();
this.deleteStubsAfterTest = options.isDeleteStubsAfterTest();
this.generateStubs = options.isGenerateStubs();
this.properties = options.getProperties();
this.httpServerStubConfigurer = options.getHttpServerStubConfigurer();
return this;
@@ -211,6 +214,11 @@ public class StubRunnerOptionsBuilder {
return this;
}
public StubRunnerOptionsBuilder withGenerateStubs(boolean generateStubs) {
this.generateStubs = generateStubs;
return this;
}
public StubRunnerOptionsBuilder withProperties(Map<String, String> properties) {
this.properties = properties;
return this;
@@ -228,7 +236,7 @@ public class StubRunnerOptionsBuilder {
buildDependencies(), this.stubIdsToPortMapping, this.username,
this.password, this.stubRunnerProxyOptions, this.stubsPerConsumer,
this.consumerName, this.mappingsOutputFolder, this.deleteStubsAfterTest,
this.properties, this.httpServerStubConfigurer);
this.generateStubs, this.properties, this.httpServerStubConfigurer);
}
private Collection<StubConfiguration> buildDependencies() {

View File

@@ -252,6 +252,12 @@ public class StubRunnerExtension implements BeforeAllCallback, AfterAllCallback,
return new PortStubRunnerExtension(this.delegate);
}
@Override
public StubRunnerExtension withGenerateStubs(boolean generateStubs) {
builder().withGenerateStubs(generateStubs);
return new PortStubRunnerExtension(this.delegate);
}
@Override
public StubRunnerExtension withProperties(Map<String, String> properties) {
builder().withProperties(properties);

View File

@@ -149,6 +149,13 @@ interface StubRunnerExtensionOptions {
*/
StubRunnerExtension withDeleteStubsAfterTest(boolean deleteStubsAfterTest);
/**
* @param generateStubs If set to {@code true} will NOT load generated stubs but will
* generate stubs from contract definitions at runtime.
* @return the rule
*/
StubRunnerExtension withGenerateStubs(boolean generateStubs);
/**
* @param properties Map of properties that can be passed to custom
* {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}

View File

@@ -190,6 +190,12 @@ public class StubRunnerRule implements TestRule, StubFinder, StubRunnerRuleOptio
return this.delegate;
}
@Override
public StubRunnerRule withGenerateStubs(boolean generateStubs) {
builder().withGenerateStubs(true);
return this.delegate;
}
@Override
public StubRunnerRule withProperties(Map<String, String> properties) {
builder().withProperties(properties);

View File

@@ -145,6 +145,13 @@ interface StubRunnerRuleOptions {
*/
StubRunnerRule withDeleteStubsAfterTest(boolean deleteStubsAfterTest);
/**
* @param generateStubs If set to {@code true} will NOT load generated stubs but will
* generate stubs from contract definitions at runtime.
* @return the rule
*/
StubRunnerRule withGenerateStubs(boolean generateStubs);
/**
* @param properties Map of properties that can be passed to custom
* {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}

View File

@@ -137,6 +137,13 @@ public @interface AutoConfigureStubRunner {
*/
boolean deleteStubsAfterTest() default true;
/**
* @return When enabled, this flag will tell stub runner to not load the generated
* stubs, but convert the found contracts at runtime to a stub format and run those
* stubs.
*/
boolean generateStubs() default false;
/**
* Configuration for an HTTP server stub.
* @return class that allows to perform additional HTTP server stub configuration

View File

@@ -101,6 +101,7 @@ public class StubRunnerConfiguration {
.withConsumerName(consumerName())
.withMappingsOutputFolder(this.props.getMappingsOutputFolder())
.withDeleteStubsAfterTest(this.props.isDeleteStubsAfterTest())
.withGenerateStubs(this.props.isGenerateStubs())
.withProperties(this.props.getProperties())
.withHttpServerStubConfigurer(this.props.getHttpServerStubConfigurer());
}

View File

@@ -30,7 +30,6 @@ import org.springframework.util.StringUtils;
/**
* @author Dave Syer
*
*/
@ConfigurationProperties("stubrunner")
public class StubRunnerProperties {
@@ -109,6 +108,12 @@ public class StubRunnerProperties {
*/
private boolean deleteStubsAfterTest = true;
/**
* When enabled, this flag will tell stub runner to not load the generated stubs, but
* convert the found contracts at runtime to a stub format and run those stubs.
*/
private boolean generateStubs;
/**
* Map of properties that can be passed to custom
* {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}.
@@ -247,6 +252,14 @@ public class StubRunnerProperties {
}
}
public boolean isGenerateStubs() {
return this.generateStubs;
}
public void setGenerateStubs(boolean generateStubs) {
this.generateStubs = generateStubs;
}
public Class getHttpServerStubConfigurer() {
return this.httpServerStubConfigurer;
}

View File

@@ -231,7 +231,7 @@ class StubRunnerOptionsBuilderSpec extends Specification {
given:
StubRunnerOptionsBuilder builder = builder.withOptions(new StubRunnerOptions(1, 2, new FileSystemResource("root"), StubRunnerProperties.StubsMode.LOCAL,
"classifier", [new StubConfiguration("a:b:c")], [(new StubConfiguration("a:b:c")): 3], "foo", "bar",
new StubRunnerOptions.StubRunnerProxyOptions("host", 4), true, "consumer", "folder", false, [foo: "bar"], Foo))
new StubRunnerOptions.StubRunnerProxyOptions("host", 4), true, "consumer", "folder", false, true, [foo: "bar"], Foo))
builder.withStubs("foo:bar:baz")
when:
StubRunnerOptions options = builder.build()
@@ -251,6 +251,7 @@ class StubRunnerOptionsBuilderSpec extends Specification {
options.consumerName == "consumer"
options.mappingsOutputFolder == "folder"
options.deleteStubsAfterTest == false
options.generateStubs == true
options.properties == [foo: "bar"]
options.httpServerStubConfigurer == Foo
}
@@ -260,7 +261,7 @@ class StubRunnerOptionsBuilderSpec extends Specification {
StubRunnerOptionsBuilder builder = builder.withOptions(new StubRunnerOptions(1, 2, new FileSystemResource("root"),
StubRunnerProperties.StubsMode.CLASSPATH, "classifier",
[new StubConfiguration("a:b:c")], [(new StubConfiguration("a:b:c")): 3], "username123", "password123",
new StubRunnerOptions.StubRunnerProxyOptions("host", 4), true, "consumer", "folder", false, [:], Foo))
new StubRunnerOptions.StubRunnerProxyOptions("host", 4), true, "consumer", "folder", false, true, [:], Foo))
builder.withStubs("foo:bar:baz")
when:
String options = builder.build().toString()
@@ -290,6 +291,8 @@ class StubRunnerOptionsBuilderSpec extends Specification {
System.setProperty("stubrunner.properties.foo-bar", "bar")
System.setProperty("stubrunner.properties.foo-baz", "baz")
System.setProperty("stubrunner.properties.bar.bar", "foo")
System.setProperty("stubrunner.delete-stubs-after-test", "false")
System.setProperty("stubrunner.generate-stubs", "true")
System.setProperty("stubrunner.http-server-stub-configurer", "org.springframework.cloud.contract.stubrunner.Foo")
when:
StubRunnerOptions options = StubRunnerOptions.fromSystemProps()
@@ -305,6 +308,8 @@ class StubRunnerOptionsBuilderSpec extends Specification {
options.proxyOptions.proxyHost == "host"
options.proxyOptions.proxyPort == 4
options.stubsPerConsumer == true
options.deleteStubsAfterTest == false
options.generateStubs == true
options.consumerName == "consumer"
options.mappingsOutputFolder == "folder"
options.properties == ["foo-bar": "bar", "foo-baz": "baz", "bar.bar": "foo"]

View File

@@ -16,14 +16,21 @@
package org.springframework.cloud.contract.stubrunner
import spock.lang.Specification
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages
import org.springframework.util.SocketUtils
class StubRunnerSpec extends Specification {
private static final int MIN_PORT = 8111
private static final int MAX_PORT = 8111
private static final int MIN_PORT = SocketUtils.findAvailableTcpPort()
private static final int MAX_PORT = MIN_PORT
private static final URL EXPECTED_STUB_URL = new URL("http://localhost:$MIN_PORT")
private static final URL generateStubs = StubRunnerSpec.getResource('/generateStubs/')
def 'should provide stub URL for provided groupid and artifactId'() {
given:
Arguments args = argumentsWithProjectDefinition()
@@ -50,10 +57,42 @@ class StubRunnerSpec extends Specification {
runner.close()
}
def 'should generate stubs at runtime'() {
given:
Arguments args = argumentsWithGenerateStubs()
StubDownloader downloader = new FileStubDownloader().build(args.stubRunnerOptions);
StubRunner runner = new StubRunnerFactory(args.stubRunnerOptions,
downloader, new NoOpStubMessages()).createStubsFromServiceConfiguration().first()
when:
runner.runStubs()
then:
URL url = runner.findStubUrl("groupId2", "artifactId2")
new URL(url.toString() + "/goodbye").text
when:
new URL(url.toString() + "/hello").text
then:
thrown(FileNotFoundException)
cleanup:
runner.close()
}
Arguments argumentsWithProjectDefinition() {
StubConfiguration stubConfiguration = new StubConfiguration("groupId", "artifactId", "classifier")
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder().withMinMaxPort(MIN_PORT, MAX_PORT).build()
return new Arguments(stubRunnerOptions, 'src/test/resources/repository', stubConfiguration)
}
Arguments argumentsWithGenerateStubs() {
StubConfiguration stubConfiguration = new StubConfiguration("groupId2", "artifactId2", "classifier2")
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
.withMinMaxPort(MIN_PORT, MAX_PORT)
.withGenerateStubs(true)
.withStubs(stubConfiguration.toString())
.withStubsMode(StubRunnerProperties.StubsMode.REMOTE)
.withStubRepositoryRoot("stubs://file://" + generateStubs.path)
.build()
return new Arguments(stubRunnerOptions, 'src/test/resources/generateStubs', stubConfiguration)
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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 org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
url("/goodbye")
method(GET())
}
response {
status(OK())
body("Goodbye World!")
}
}

View File

@@ -0,0 +1,13 @@
{
"request": {
"method": "GET",
"url": "/hello"
},
"response": {
"status": 200,
"body": "Hello world!",
"headers": {
"Content-Type": "text/plain"
}
}
}

View File

@@ -1,54 +0,0 @@
/*
* 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.transform.CompileStatic
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.file.ContractMetadata
/**
* Converts contracts into their stub representation.
*
* @since 1.1.0
*/
@CompileStatic
interface StubGenerator {
/**
* @return {@code true} if the converter can handle the file to convert it into a stub.
*/
boolean canHandleFileName(String fileName)
/**
* @return the collection of converted contracts into stubs. One contract can
* result in multiple stubs.
*/
Map<Contract, String> convertContents(String rootName, ContractMetadata content)
/**
* @return the name of the converted stub file. If you have multiple contracts
* in a single file then a prefix will be added to the generated file. If you
* provide the {@link Contract#name} field then that field will override the
* generated file name.
*
* Example: name of file with 2 contracts is {@code foo.groovy}, it will be
* converted by the implementation to {@code foo.json}. The recursive file
* converter will create two files {@code 0_foo.json} and {@code 1_foo.json}
*/
String generateOutputFileNameForInput(String inputFileName)
}

View File

@@ -0,0 +1,68 @@
/*
* 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 java.util.Map;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.file.ContractMetadata;
/**
* Converts contracts into their stub representation.
*
* @since 1.1.0
*/
public interface StubGenerator {
/**
* @param fileName - file name
* @return {@code true} if the converter can handle the file to convert it into a
* stub.
*/
default boolean canHandleFileName(String fileName) {
return fileName.endsWith(fileExtension());
}
/**
* @param rootName - root name of the contract
* @param content - metadata of the contract
* @return the collection of converted contracts into stubs. One contract can result
* in multiple stubs.
*/
Map<Contract, String> convertContents(String rootName, ContractMetadata content);
/**
* @param inputFileName - name of the input file
* @return the name of the converted stub file. If you have multiple contracts in a
* single file then a prefix will be added to the generated file. If you provide the
* {@link Contract#name} field then that field will override the generated file name.
*
* Example: name of file with 2 contracts is {@code foo.groovy}, it will be converted
* by the implementation to {@code foo.json}. The recursive file converter will create
* two files {@code 0_foo.json} and {@code 1_foo.json}
*/
String generateOutputFileNameForInput(String inputFileName);
/**
* Describes the file extension that this stub generator can handle.
* @return string describing the file extension
*/
default String fileExtension() {
return ".json";
}
}

View File

@@ -28,11 +28,6 @@ import org.springframework.cloud.contract.verifier.converter.StubGenerator
@CompileStatic
abstract class DslToWireMockConverter implements StubGenerator {
@Override
boolean canHandleFileName(String fileName) {
return fileName.endsWith('.groovy')
}
@Override
String generateOutputFileNameForInput(String inputFileName) {
return inputFileName.replaceAll(extension(inputFileName), 'json')

View File

@@ -38,10 +38,10 @@ import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConve
import org.springframework.cloud.contract.verifier.converter.ToYamlConverter;
/**
* Convert Spring Cloud Contract Verifier contracts into WireMock stubs mappings.
* Convert Spring Cloud Contract Verifier contracts into stubs mappings.
* <p>
* This goal allows you to generate `stubs-jar` or execute `spring-cloud-contract:run`
* with generated WireMock mappings.
* with generated mappings.
*
* @author Mariusz Smykula
*/

View File

@@ -47,9 +47,6 @@ 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;
@@ -216,32 +213,16 @@ class PactStubDownloader implements StubDownloader {
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());
}
}
MappingGenerator.toMappings(file, contracts, mappingsFolder);
}
private void storeFile(Path path, byte[] contents) {
private Path storeFile(Path path, byte[] contents) {
try {
Files.write(path, contents);
Path storedPath = Files.write(path, contents);
if (log.isDebugEnabled()) {
log.debug("Stored file [" + path.toString() + "]");
}
return storedPath;
}
catch (IOException e) {
throw new IllegalStateException(e);