Consumer Contracts (#83)

With this functionality you can have one centralized repository containing all contracts. This repo will have to produce a JAR containing all contracts. The layout of the repository can be arbitrary but some sensible defaults are assumed. The producer will be able to then download that JAR and produce tests and stubs from it.

fixes #38
This commit is contained in:
Marcin Grzejszczak
2016-09-23 10:16:51 +02:00
committed by GitHub
parent 93782cb049
commit 01f4ad76be
50 changed files with 1995 additions and 168 deletions

View File

@@ -16,8 +16,10 @@
package org.springframework.cloud.contract.maven.verifier;
import java.io.File;
import javax.inject.Inject;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
@@ -27,6 +29,8 @@ import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.filtering.MavenResourcesFiltering;
import org.eclipse.aether.RepositorySystemSession;
import org.springframework.cloud.contract.maven.verifier.stubrunner.AetherStubDownloaderFactory;
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
import org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter;
import org.springframework.cloud.contract.verifier.wiremock.RecursiveFilesConverter;
@@ -40,6 +44,9 @@ import org.springframework.cloud.contract.verifier.wiremock.RecursiveFilesConver
defaultPhase = LifecyclePhase.PROCESS_TEST_RESOURCES)
public class ConvertMojo extends AbstractMojo {
@Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
private RepositorySystemSession repoSession;
/**
* Directory containing Spring Cloud Contract Verifier contracts written using the GroovyDSL
*/
@@ -72,9 +79,43 @@ public class ConvertMojo extends AbstractMojo {
@Parameter(defaultValue = "${project}", readonly = true) private MavenProject project;
/**
* The URL from which a JAR containing the contracts should get downloaded. If not provided
* but artifactid / coordinates notation was provided then the current Maven's build repositories will be
* taken into consideration
*/
@Parameter(property = "contractsRepositoryUrl")
private String contractsRepositoryUrl;
@Parameter(property = "contractDependency")
private Dependency contractDependency;
/**
* The path in the JAR with all the contracts where contracts for this particular service lay.
* If not provided will be resolved to {@code groupid/artifactid}. Example:
* </p>
* If {@code groupid} is {@code com.example} and {@code artifactid} is {@code service} then the resolved path will be
* {@code /com/example/artifactid}
*/
@Parameter(property = "contractsPath")
private String contractsPath;
/**
* If {@code true} then JAR with contracts will be taken from local maven repository
*/
@Parameter(property = "contractsWorkOffline", defaultValue = "false")
private boolean contractsWorkOffline;
@Component(role = MavenResourcesFiltering.class, hint = "default")
private MavenResourcesFiltering mavenResourcesFiltering;
private final AetherStubDownloaderFactory aetherStubDownloaderFactory;
@Inject
public ConvertMojo(AetherStubDownloaderFactory aetherStubDownloaderFactory) {
this.aetherStubDownloaderFactory = aetherStubDownloaderFactory;
}
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.skip) {
@@ -83,12 +124,18 @@ public class ConvertMojo extends AbstractMojo {
this.skip));
return;
}
// download contracts, unzip them and pass as output directory
ContractVerifierConfigProperties config = new ContractVerifierConfigProperties();
File contractsDirectory = new MavenContractsDownloader(this.project, this.contractDependency,
this.contractsPath, this.contractsRepositoryUrl, this.contractsWorkOffline, getLog(),
this.aetherStubDownloaderFactory, this.repoSession).downloadAndUnpackContractsIfRequired(config, this.contractsDirectory);
getLog().info("Directory with contract is present at [" + contractsDirectory + "]");
new CopyContracts(this.project, this.mavenSession, this.mavenResourcesFiltering)
.copy(this.contractsDirectory, this.outputDirectory);
.copy(contractsDirectory, this.outputDirectory);
final ContractVerifierConfigProperties config = new ContractVerifierConfigProperties();
config.setContractsDslDir(isInsideProject() ? this.contractsDirectory : this.source);
config.setContractsDslDir(isInsideProject() ? contractsDirectory : this.source);
config.setStubsOutputDir(
isInsideProject() ? new File(this.outputDirectory, "mappings") : this.destination);
@@ -100,6 +147,7 @@ public class ConvertMojo extends AbstractMojo {
getLog().info(String.format("WireMock stubs mappings directory: %s",
config.getStubsOutputDir()));
RecursiveFilesConverter converter = new RecursiveFilesConverter(
new DslToWireMockClientConverter(), config);
converter.processFiles();

View File

@@ -29,6 +29,9 @@ import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.jar.JarArchiver;
/**
* Picks the converted .json files and creates a jar. Requires convert to be executed first
*/
@Mojo(name = "generateStubs", defaultPhase = LifecyclePhase.PACKAGE,
requiresProject = true)
public class GenerateStubsMojo extends AbstractMojo {

View File

@@ -18,6 +18,9 @@ package org.springframework.cloud.contract.maven.verifier;
import java.io.File;
import java.util.List;
import javax.inject.Inject;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
@@ -26,6 +29,8 @@ import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystemSession;
import org.springframework.cloud.contract.maven.verifier.stubrunner.AetherStubDownloaderFactory;
import org.springframework.cloud.contract.spec.ContractVerifierException;
import org.springframework.cloud.contract.verifier.TestGenerator;
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
@@ -36,6 +41,9 @@ import org.springframework.cloud.contract.verifier.config.TestMode;
requiresDependencyResolution = ResolutionScope.TEST)
public class GenerateTestsMojo extends AbstractMojo {
@Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
private RepositorySystemSession repoSession;
@Parameter(property = "spring.cloud.contract.verifier.contractsDirectory",
defaultValue = "${project.basedir}/src/test/resources/contracts")
private File contractsDirectory;
@@ -105,6 +113,40 @@ public class GenerateTestsMojo extends AbstractMojo {
@Parameter(property = "skipTests", defaultValue = "false") private boolean skipTests;
/**
* The URL from which a JAR containing the contracts should get downloaded. If not provided
* but artifactid / coordinates notation was provided then the current Maven's build repositories will be
* taken into consideration
*/
@Parameter(property = "contractsRepositoryUrl")
private String contractsRepositoryUrl;
@Parameter(property = "contractDependency")
private Dependency contractDependency;
/**
* The path in the JAR with all the contracts where contracts for this particular service lay.
* If not provided will be resolved to {@code groupid/artifactid}. Example:
* </p>
* If {@code groupid} is {@code com.example} and {@code artifactid} is {@code service} then the resolved path will be
* {@code /com/example/artifactid}
*/
@Parameter(property = "contractsPath")
private String contractsPath;
/**
* If {@code true} then JAR with contracts will be taken from local maven repository
*/
@Parameter(property = "contractsWorkOffline", defaultValue = "false")
private boolean contractsWorkOffline;
private final AetherStubDownloaderFactory aetherStubDownloaderFactory;
@Inject
public GenerateTestsMojo(AetherStubDownloaderFactory aetherStubDownloaderFactory) {
this.aetherStubDownloaderFactory = aetherStubDownloaderFactory;
}
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.skip || this.mavenTestSkip || this.skipTests) {
if (this.skip) getLog().info("Skipping Spring Cloud Contract Verifier execution: spring.cloud.contract.verifier.skip=" + this.skip);
@@ -115,7 +157,12 @@ public class GenerateTestsMojo extends AbstractMojo {
getLog().info(
"Generating server tests source code for Spring Cloud Contract Verifier contract verification");
final ContractVerifierConfigProperties config = new ContractVerifierConfigProperties();
config.setContractsDslDir(this.contractsDirectory);
// download contracts, unzip them and pass as output directory
File contractsDirectory = new MavenContractsDownloader(this.project, this.contractDependency,
this.contractsPath, this.contractsRepositoryUrl, this.contractsWorkOffline, getLog(),
this.aetherStubDownloaderFactory, this.repoSession).downloadAndUnpackContractsIfRequired(config, this.contractsDirectory);
getLog().info("Directory with contract is present at [" + contractsDirectory + "]");
config.setContractsDslDir(contractsDirectory);
config.setGeneratedTestSourcesDir(this.generatedTestSourcesDir);
config.setTargetFramework(this.testFramework);
config.setTestMode(this.testMode);

View File

@@ -0,0 +1,102 @@
package org.springframework.cloud.contract.maven.verifier;
import java.io.File;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystemSession;
import org.springframework.cloud.contract.maven.verifier.stubrunner.AetherStubDownloaderFactory;
import org.springframework.cloud.contract.stubrunner.AetherStubDownloader;
import org.springframework.cloud.contract.stubrunner.ContractDownloader;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder;
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
import org.springframework.util.StringUtils;
/**
* Downloads JAR with contracts
*
* @author Marcin Grzejszczak
* @since 1.0.0
*/
class MavenContractsDownloader {
private static final String LATEST_VERSION = "+";
private static final String CONTRACTS_DIRECTORY_PROP = "CONTRACTS_DIRECTORY";
private final MavenProject project;
private final Dependency contractDependency;
private final String contractsPath;
private final String contractsRepositoryUrl;
private final boolean contractsWorkOffline;
private final Log log;
private final AetherStubDownloaderFactory aetherStubDownloaderFactory;
private final RepositorySystemSession repoSession;
MavenContractsDownloader(MavenProject project, Dependency contractDependency,
String contractsPath, String contractsRepositoryUrl,
boolean contractsWorkOffline, Log log,
AetherStubDownloaderFactory aetherStubDownloaderFactory,
RepositorySystemSession repoSession) {
this.project = project;
this.contractDependency = contractDependency;
this.contractsPath = contractsPath;
this.contractsRepositoryUrl = contractsRepositoryUrl;
this.contractsWorkOffline = contractsWorkOffline;
this.log = log;
this.aetherStubDownloaderFactory = aetherStubDownloaderFactory;
this.repoSession = repoSession;
}
File downloadAndUnpackContractsIfRequired(ContractVerifierConfigProperties config, File defaultContractsDir) {
String contractsDirFromProp = this.project.getProperties().getProperty(CONTRACTS_DIRECTORY_PROP);
File downloadedContractsDir = StringUtils.hasText(contractsDirFromProp) ?
new File(contractsDirFromProp) : null;
// reuse downloaded contracts from another mojo
if (downloadedContractsDir != null && downloadedContractsDir.exists()) {
this.log.info("Another mojo has downloaded the contracts - will reuse them from [" + downloadedContractsDir + "]");
contractDownloader().updatePropertiesWithInclusion(downloadedContractsDir, config);
return downloadedContractsDir;
} else if (shouldDownloadContracts()) {
this.log.info("Download dependency is provided - will download contract jars");
File downloadedContracts = contractDownloader().unpackedDownloadedContracts(config);
this.project.getProperties().setProperty(CONTRACTS_DIRECTORY_PROP, downloadedContracts.getAbsolutePath());
return downloadedContracts;
}
this.log.info("Will use contracts provided in the folder [" + defaultContractsDir + "]");
return defaultContractsDir;
}
private boolean shouldDownloadContracts() {
return this.contractDependency != null && StringUtils.hasText(this.contractDependency.getArtifactId());
}
private ContractDownloader contractDownloader() {
return new ContractDownloader(stubDownloader(), stubConfiguration(),
this.contractsPath, this.project.getGroupId(), this.project.getArtifactId());
}
private AetherStubDownloader stubDownloader() {
if (StringUtils.hasText(this.contractsRepositoryUrl) || this.contractsWorkOffline) {
this.log.info("Will download contracts from [" + this.contractsRepositoryUrl + "]. "
+ "Work offline switch equals to [" + this.contractsWorkOffline + "]");
return new AetherStubDownloader(
new StubRunnerOptionsBuilder()
.withStubRepositoryRoot(this.contractsRepositoryUrl)
.withWorkOffline(this.contractsWorkOffline)
.build());
}
this.log.info("Will download contracts using current build's Maven repository setup");
return this.aetherStubDownloaderFactory.build(this.repoSession);
}
private StubConfiguration stubConfiguration() {
String groupId = this.contractDependency.getGroupId();
String artifactId = this.contractDependency.getArtifactId();
String version = StringUtils.hasText(this.contractDependency.getVersion()) ?
this.contractDependency.getVersion() : LATEST_VERSION;
String classifier = this.contractDependency.getClassifier();
return new StubConfiguration(groupId, artifactId, version, classifier);
}
}

View File

@@ -134,5 +134,32 @@ public class PluginUnitTest {
assertFilesPresent(basedir, "target/sample-project-0.1-foo.jar");
}
@Test
public void shouldGenerateStubsByDownloadingContractsFromARepo() throws Exception {
File basedir = this.resources.getBasedir("basic-remote-contracts");
this.maven.executeMojo(basedir, "convert", newParameter("contractsRepositoryUrl", "file://" + PluginUnitTest.class.getClassLoader().getResource("m2repo/repository").getFile()));
assertFilesPresent(basedir, "target/stubs/mappings/com/example/server/client1/contracts/shouldMarkClientAsFraud.json");
}
@Test
public void shouldGenerateStubsByDownloadingContractsFromARepoWhenCustomPathIsProvided() throws Exception {
File basedir = this.resources.getBasedir("complex-remote-contracts");
this.maven.executeMojo(basedir, "convert", newParameter("contractsRepositoryUrl", "file://" + PluginUnitTest.class.getClassLoader().getResource("m2repo/repository").getFile()));
assertFilesPresent(basedir, "target/stubs/mappings/com/example/server/client1/contracts/shouldMarkClientAsFraud.json");
}
@Test
public void shouldGenerateTestsByDownloadingContractsFromARepo() throws Exception {
File basedir = this.resources.getBasedir("basic-remote-contracts");
this.maven.executeMojo(basedir, "generateTests", newParameter("contractsRepositoryUrl", "file://" + PluginUnitTest.class.getClassLoader().getResource("m2repo/repository").getFile()));
assertFilesPresent(basedir, "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/com/example/server/client1/ContractsTest.java");
}
@Test
public void shouldGenerateTestsByDownloadingContractsFromARepoWhenCustomPathIsProvided() throws Exception {
File basedir = this.resources.getBasedir("complex-remote-contracts");
this.maven.executeMojo(basedir, "generateTests", newParameter("contractsRepositoryUrl", "file://" + PluginUnitTest.class.getClassLoader().getResource("m2repo/repository").getFile()));
assertFilesPresent(basedir, "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/com/example/server/client1/ContractsTest.java");
}
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2013-2016 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>server</artifactId>
<version>0.1.BUILD-SNAPSHOT</version>
<build>
<plugins>
<!-- tag::remote_config[] -->
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<contractsRepositoryUrl>http://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
<contractDependency>
<groupId>com.example.standalone</groupId>
<artifactId>contracts</artifactId>
</contractDependency>
</configuration>
</plugin>
<!-- end::remote_config[] -->
</plugins>
</build>
</project>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2013-2016 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>server</artifactId>
<version>0.1.BUILD-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<contractDependency>
<groupId>com.example</groupId>
<artifactId>contracts</artifactId>
</contractDependency>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2013-2016 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>someartifact</artifactId>
<version>0.1.BUILD-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<configuration>
<contractsPath>com/example/server</contractsPath>
<contractDependency>
<groupId>com.example</groupId>
<artifactId>contracts</artifactId>
<version>+</version>
</contractDependency>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,25 @@
<!--
~ Copyright 2013-2016 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.
-->
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework.cloud" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2016 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.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>contracts</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
</project>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
<groupId>com.example</groupId>
<artifactId>contracts</artifactId>
<version>0.0.1-SNAPSHOT</version>
<versioning>
<snapshot>
<localCopy>true</localCopy>
</snapshot>
<lastUpdated>20160916125313</lastUpdated>
<snapshotVersions>
<snapshotVersion>
<extension>jar</extension>
<value>0.0.1-SNAPSHOT</value>
<updated>20160916125313</updated>
</snapshotVersion>
<snapshotVersion>
<extension>pom</extension>
<value>0.0.1-SNAPSHOT</value>
<updated>20160916125313</updated>
</snapshotVersion>
</snapshotVersions>
</versioning>
</metadata>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2016 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.
-->
<metadata>
<groupId>com.example</groupId>
<artifactId>contracts</artifactId>
<version>0.0.1-SNAPSHOT</version>
<versioning>
<versions>
<version>0.0.1-SNAPSHOT</version>
</versions>
<lastUpdated>20160409062112</lastUpdated>
</versioning>
</metadata>