diff --git a/README.adoc b/README.adoc
index 94d6ca3ec1..a88022179c 100644
--- a/README.adoc
+++ b/README.adoc
@@ -256,7 +256,7 @@ public class ApplicationTests {
public void contextLoads() throws Exception {
mockMvc.perform(post("/resource")
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
- .andExpect(status.isOk())
+ .andExpect(status().isOk())
.andDo(verify().jsonPath("$.id")
.stub("resource"));
}
@@ -279,7 +279,7 @@ created stub. Example:
public void contextLoads() throws Exception {
mockMvc.perform(post("/resource")
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
- .andExpect(status.isOk())
+ .andExpect(status().isOk())
.andDo(verify()
.wiremock(WireMock.post(
urlPathEquals("/resource"))
@@ -397,7 +397,7 @@ the generated document (example for Asciidoc) will contain a formatted contract
=== Spring Cloud Contract Verifier
-:introduction_url: https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/1.0.x
+:introduction_url: https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master
=== Introduction
@@ -947,16 +947,28 @@ That's because all the generated tests will extend that class. Over there you ca
----
package com.example.fraud;
-import com.example.fraud.FraudDetectionController;
-import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
-
import org.junit.Before;
+import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
+
public class FraudBase {
@Before
public void setup() {
- RestAssuredMockMvc.standaloneSetup(new FraudDetectionController());
+ RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
+ new FraudStatsController(stubbedStatsProvider()));
+ }
+
+ private StatsProvider stubbedStatsProvider() {
+ return fraudType -> {
+ switch (fraudType) {
+ case DRUNKS:
+ return 100;
+ case ALL:
+ return 200;
+ }
+ return 0;
+ };
}
public void assertThatRejectionReasonIsNull(Object rejectionReason) {
@@ -1379,15 +1391,15 @@ Example of a `pom.xml` inside the `server` folder.
org.springframework.boot
spring-boot-starter-parent
- 1.4.2.BUILD-SNAPSHOT
+ 1.5.0.BUILD-SNAPSHOT
UTF-8
1.8
- 1.0.4.BUILD-SNAPSHOT
- Camden.BUILD-SNAPSHOT
+ 1.1.0.BUILD-SNAPSHOT
+ Dalston.BUILD-SNAPSHOT
true
@@ -1601,6 +1613,11 @@ The rest of the flow looks the same.
Yes! Check out the https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_different_base_classes_for_contracts[Different base classes for contracts] sections
of either Gradle or Maven plugins.
+==== Can I reference the request from the response?
+
+Yes! With version 1.1.0 we've added such a possibility. On the HTTP stub server side we're providing support
+for this for WireMock. In case of other HTTP server stubs you'll have to implement the approach yourself.
+
=== Links
Here you can find interesting links related to Spring Cloud Contract Verifier:
diff --git a/circle.yml b/circle.yml
index 9651f9412f..522ab63b4d 100644
--- a/circle.yml
+++ b/circle.yml
@@ -6,8 +6,8 @@ machine:
java:
version: oraclejdk8
environment:
- _JAVA_OPTIONS: "-Xms512m -Xmx1024m"
- GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx1024m -XX:+HeapDumpOnOutOfMemoryError"'
+ _JAVA_OPTIONS: "-Xms512m -Xmx768m"
+ GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx768m -XX:+HeapDumpOnOutOfMemoryError"'
TERM: dumb
dependencies:
pre:
@@ -40,4 +40,4 @@ notify:
webhooks:
# A list of hook hashes, containing the url field
# gitter hook
- - url: https://webhooks.gitter.im/e/ece0ece9a76a5af2aa91
\ No newline at end of file
+ - url: https://webhooks.gitter.im/e/ece0ece9a76a5af2aa91
diff --git a/docs/src/main/asciidoc/verifier/contract.adoc b/docs/src/main/asciidoc/verifier/contract.adoc
index c7a456a633..ea6e1e0f2e 100644
--- a/docs/src/main/asciidoc/verifier/contract.adoc
+++ b/docs/src/main/asciidoc/verifier/contract.adoc
@@ -256,6 +256,120 @@ IMPORTANT: You can't use both a String and `execute` to perform concatenation. E
To make this work just call `header('Authorization', execute('authToken()'))` and ensure that
the `authToken()` method returns everything that you need.
+===== Referencing request from response
+
+The best situation is to provide fixed values but sometimes you need to reference a request in your response.
+In order to do this you can profit from the `fromRequest()` method that allows you to reference a bunch
+of elements from the HTTP request. You can use the following options:
+
+- `fromRequest().url()` - return the request URL
+- `fromRequest().query(String key)` - return the first query parameter with a given name
+- `fromRequest().query(String key, int index)` - return the nth query parameter with a given name
+- `fromRequest().header(String key)` - return the first header with a given name
+- `fromRequest().header(String key, int index)` - return the nth header with a given name
+- `fromRequest().body()` - return the full request body
+- `fromRequest().body(String jsonPath)` - return the element from the request that matches the JSON Path
+
+Let's take a look at the following contract
+
+[source,groovy,indent=0]
+----
+include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderSpec.groovy[tags=template_contract,indent=0]
+----
+
+Running a JUnit test generation will lead in creation of a test looking more or less like this
+
+[source,java,indent=0]
+----
+ // given:
+ MockMvcRequestSpecification request = given()
+ .header("Authorization", "secret")
+ .header("Authorization", "secret2")
+ .body("{\"foo\":\"bar\",\"baz\":5}");
+
+ // when:
+ ResponseOptions response = given().spec(request)
+ .queryParam("foo","bar")
+ .queryParam("foo","bar2")
+ .get("/api/v1/xxxx");
+
+ // then:
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.header("Authorization")).isEqualTo("foo secret bar");
+ // and:
+ DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
+ assertThatJson(parsedJson).field("url").isEqualTo("/api/v1/xxxx");
+ assertThatJson(parsedJson).field("fullBody").isEqualTo("{\"foo\":\"bar\",\"baz\":5}");
+ assertThatJson(parsedJson).field("paramIndex").isEqualTo("bar2");
+ assertThatJson(parsedJson).field("responseFoo").isEqualTo("bar");
+ assertThatJson(parsedJson).field("authorization2").isEqualTo("secret2");
+ assertThatJson(parsedJson).field("responseBaz").isEqualTo(5);
+ assertThatJson(parsedJson).field("responseBaz2").isEqualTo("Bla bla bar bla bla");
+ assertThatJson(parsedJson).field("param").isEqualTo("bar");
+ assertThatJson(parsedJson).field("authorization").isEqualTo("secret");
+----
+
+As you can see elements from the request have been properly referenced in the response.
+
+The generated WireMock stub will look more or less like this:
+
+[source,json,indent=0]
+----
+{
+ "request" : {
+ "urlPath" : "/api/v1/xxxx",
+ "method" : "POST",
+ "headers" : {
+ "Authorization" : {
+ "equalTo" : "secret2"
+ }
+ },
+ "queryParameters" : {
+ "foo" : {
+ "equalTo" : "bar2"
+ }
+ },
+ "bodyPatterns" : [ {
+ "matchesJsonPath" : "$[?(@.baz == 5)]"
+ }, {
+ "matchesJsonPath" : "$[?(@.foo == 'bar')]"
+ } ]
+ },
+ "response" : {
+ "status" : 200,
+ "body" : "{\"url\":\"{{{request.url}}}\",\"param\":\"{{{request.query.foo.[0]}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\",\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\"}",
+ "headers" : {
+ "Authorization" : "{{{request.headers.Authorization.[0]}}}"
+ },
+ "transformers" : [ "response-template" ]
+ }
+}
+----
+
+So sending a request as the one presented in the `request` part of the contract will lead in sending the following
+response body
+
+[source,json,indent=0]
+----
+{
+ "url" : "/api/v1/xxxx?foo=bar&foo=bar2",
+ "param" : "bar",
+ "paramIndex" : "bar2",
+ "authorization" : "secret",
+ "authorization2" : "secret2",
+ "fullBody" : "{\"foo\":\"bar\",\"baz\":5}",
+ "responseFoo" : "bar",
+ "responseBaz" : 5,
+ "responseBaz2" : "Bla bla bar bla bla"
+}
+----
+
+IMPORTANT: This feature will work only with WireMock having version greater or equal to 2.5.1. We're using WireMock's
+`response-template` response transformer. It's using Handlebars to convert the Mustache `{{{ }}}` templates into
+proper values. Additionally we're registering 2 helper functions. `escapejsonbody` - that escapes the request
+body in a format that can be embedded in a JSON. Another is `jsonpath` that for a given parameter knows how to
+find an object in the request body.
+
===== Dynamic properties in matchers sections
If you've been working with https://docs.pact.io/[Pact] this might seem familiar. Quite a few users
@@ -924,4 +1038,4 @@ include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/resources/
that way you'll be able to pick a folder with the source of your stubs.
IMPORTANT: If you don't provide any implementation then the default one - Aether based that will download stubs from a remote repo
-will be picked. If you provide more than one then the first one on the list will be picked.
+will be picked. If you provide more than one then the first one on the list will be picked.
\ No newline at end of file
diff --git a/docs/src/main/asciidoc/verifier/introduction.adoc b/docs/src/main/asciidoc/verifier/introduction.adoc
index f6871dee08..c57a6780fc 100644
--- a/docs/src/main/asciidoc/verifier/introduction.adoc
+++ b/docs/src/main/asciidoc/verifier/introduction.adoc
@@ -807,3 +807,8 @@ The rest of the flow looks the same.
Yes! Check out the https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_different_base_classes_for_contracts[Different base classes for contracts] sections
of either Gradle or Maven plugins.
+
+==== Can I reference the request from the response?
+
+Yes! With version 1.1.0 we've added such a possibility. On the HTTP stub server side we're providing support
+for this for WireMock. In case of other HTTP server stubs you'll have to implement the approach yourself.
diff --git a/pom.xml b/pom.xml
index 177eca8bec..c1fd514827 100644
--- a/pom.xml
+++ b/pom.xml
@@ -137,6 +137,11 @@
pact-jvm-model
2.4.18
+
+ com.github.jknack
+ handlebars
+ 4.0.6
+
org.springframework.cloud
spring-cloud-contract-dependencies
diff --git a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java
index 82ae6bee85..d172969868 100644
--- a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java
+++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java
@@ -1,5 +1,7 @@
package com.example.fraud;
+import java.math.BigDecimal;
+
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -8,8 +10,6 @@ import com.example.fraud.model.FraudCheck;
import com.example.fraud.model.FraudCheckResult;
import com.example.fraud.model.FraudCheckStatus;
-import java.math.BigDecimal;
-
import static org.springframework.web.bind.annotation.RequestMethod.PUT;
@RestController
diff --git a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java
new file mode 100644
index 0000000000..ea6ff8f2c0
--- /dev/null
+++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudNameController.java
@@ -0,0 +1,73 @@
+package com.example.fraud;
+
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Marcin Grzejszczak
+ */
+@RestController
+class FraudNameController {
+
+ private static final String FRAUD_SERVICE_JSON_VERSION_1 = "application/vnd.fraud.v1+json";
+
+ private final FraudVerifier fraudVerifier;
+
+ FraudNameController(FraudVerifier fraudVerifier) {
+ this.fraudVerifier = fraudVerifier;
+ }
+
+ @PutMapping(
+ value = "/frauds/name",
+ produces = FRAUD_SERVICE_JSON_VERSION_1)
+ public NameResponse checkByName(@RequestBody NameRequest request) {
+ boolean fraud = this.fraudVerifier.isFraudByName(request.getName());
+ if (fraud) {
+ return new NameResponse("Sorry " + request.getName() + " but you're a fraud");
+ }
+ return new NameResponse("Don't worry " + request.getName() + " you're not a fraud");
+ }
+}
+
+interface FraudVerifier {
+ boolean isFraudByName(String name);
+}
+
+class NameRequest {
+ private String name;
+
+ public NameRequest(String name) {
+ this.name = name;
+ }
+
+ public NameRequest() {
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
+
+class NameResponse {
+ private String result;
+
+ public NameResponse(String result) {
+ this.result = result;
+ }
+
+ public NameResponse() {
+ }
+
+ public String getResult() {
+ return this.result;
+ }
+
+ public void setResult(String result) {
+ this.result = result;
+ }
+}
diff --git a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudStatsController.java b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudStatsController.java
index 5b27668600..ab8d933814 100644
--- a/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudStatsController.java
+++ b/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudStatsController.java
@@ -55,4 +55,3 @@ class Response {
this.count = count;
}
}
-
diff --git a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java
index 639d686c2d..8b3ab5877b 100644
--- a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java
+++ b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java
@@ -5,7 +5,6 @@ import org.junit.Before;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
public class FraudBase {
-
@Before
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
diff --git a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudnameBase.java b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudnameBase.java
new file mode 100644
index 0000000000..2b8992819c
--- /dev/null
+++ b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudnameBase.java
@@ -0,0 +1,17 @@
+package com.example.fraud;
+
+import org.junit.Before;
+
+import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
+
+public class FraudnameBase {
+
+ private static final String FRAUD_NAME = "fraud";
+
+ FraudVerifier fraudVerifier = FRAUD_NAME::equals;
+
+ @Before
+ public void setup() {
+ RestAssuredMockMvc.standaloneSetup(new FraudNameController(this.fraudVerifier));
+ }
+}
\ No newline at end of file
diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy
new file mode 100644
index 0000000000..f2f96c2676
--- /dev/null
+++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnAFraudForTheName.groovy
@@ -0,0 +1,25 @@
+package contracts.fraudname
+
+org.springframework.cloud.contract.spec.Contract.make {
+ request {
+ // highest priority
+ priority(1)
+ method PUT()
+ url '/frauds/name'
+ body([
+ name: "fraud"
+ ])
+ headers {
+ contentType("application/vnd.fraud.v1+json")
+ }
+ }
+ response {
+ status 200
+ body([
+ result: "Sorry ${fromRequest().body('$.name')} but you're a fraud"
+ ])
+ headers {
+ header(contentType(), "${fromRequest().header(contentType())};charset=UTF-8")
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.groovy
new file mode 100644
index 0000000000..eddea317a7
--- /dev/null
+++ b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraudname/shouldReturnNonFraudForTheName.groovy
@@ -0,0 +1,23 @@
+package contracts.fraudname
+
+org.springframework.cloud.contract.spec.Contract.make {
+ request {
+ method PUT()
+ url '/frauds/name'
+ body([
+ name: $(anyAlphaUnicode())
+ ])
+ headers {
+ contentType("application/vnd.fraud.v1+json")
+ }
+ }
+ response {
+ status 200
+ body([
+ result: "Don't worry ${fromRequest().body('$.name')} you're not a fraud"
+ ])
+ headers {
+ header(contentType(), "${fromRequest().header(contentType())};charset=UTF-8")
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy
new file mode 100644
index 0000000000..5d7cd1270c
--- /dev/null
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractTemplate.groovy
@@ -0,0 +1,72 @@
+package org.springframework.cloud.contract.spec
+
+/**
+ * Contract for defining templated responses.
+ *
+ * If no implementation is provided then Handlebars will be picked as a default implementation.
+ *
+ * @author Marcin Grzejszczak
+ * @since 1.1.0
+ */
+interface ContractTemplate {
+
+ /**
+ * Handlebars is using the Mustache template thus it looks like this
+ * {{{ Mustache }}}. In this case the opening template would return {{{
+ */
+ String openingTemplate()
+
+ /**
+ * Handlebars is using the Mustache template thus it looks like this
+ * {{{ Mustache }}}. In this case the closing template would return }}}
+ */
+ String closingTemplate()
+
+ /**
+ * Returns the template for retrieving a URL from request
+ */
+ String url()
+
+ /**
+ * Returns the template for retrieving first value of a query parameter e.g. {{{ request.query.search }}}
+ * @param key
+ */
+ String query(String key)
+
+ /**
+ * Returns the template for retrieving nth value of a query parameter (zero indexed) e.g. {{{ request.query.search.[5] }}}
+ * @param key
+ * @param index
+ */
+ String query(String key, int index)
+
+ /**
+ * Returns the template for retrieving the first value of a request header e.g. {{{ request.headers.X-Request-Id }}}
+ * @param key
+ */
+ String header(String key)
+
+ /**
+ * Returns the template for retrieving the nth value of a request header (zero indexed) e.g. {{{ request.headers.X-Request-Id.[5] }}}
+ * @param key
+ * @param index
+ */
+ String header(String key, int index)
+
+ /**
+ * Request body text (avoid for non-text bodies) e.g. {{{ request.body }}} . The body will not be escaped
+ * so you won't be able to directly embed it in a JSON for example.
+ */
+ String body()
+
+ /**
+ * Request body text (avoid for non-text bodies) e.g. {{{ escapejsonbody }}} . The body will not be escaped
+ * so you will be able to embed it
+ */
+ String escapedBody()
+
+ /**
+ * Request body text for the given JsonPath. e.g. {{{ jsonpath this '$.a.b.c' }}}
+ */
+ String body(String jsonPath)
+}
\ No newline at end of file
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy
index e9213bef4a..7db6aca4b9 100644
--- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Common.groovy
@@ -205,4 +205,4 @@ class Common {
void assertThatSidesMatch(Object firstSide, Object secondSide) {
// do nothing
}
-}
+}
\ No newline at end of file
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy
new file mode 100644
index 0000000000..7c504f7388
--- /dev/null
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/FromRequest.groovy
@@ -0,0 +1,81 @@
+package org.springframework.cloud.contract.spec.internal
+
+import groovy.transform.CompileStatic
+import org.springframework.cloud.contract.spec.ContractTemplate
+/**
+ * Helper class to reference the request body parameters
+ *
+ * @author Marcin Grzejszczak
+ * @since 1.1.0
+ */
+
+@CompileStatic
+class FromRequest {
+
+ private final ContractTemplate template
+
+ FromRequest() {
+ this.template = template()
+ }
+
+ private ContractTemplate template() {
+ return new HandlebarsContractTemplate()
+ }
+
+ /**
+ * URL path and query
+ */
+ DslProperty url() {
+ return new DslProperty(template.url())
+ }
+
+ /**
+ * First value of a query parameter e.g. request.query.search
+ * @param key
+ */
+ DslProperty query(String key) {
+ return new DslProperty(template.query(key))
+ }
+
+ /**
+ * nth value of a query parameter (zero indexed) e.g. request.query.search.[5]
+ * @param key
+ * @param index
+ */
+ DslProperty query(String key, int index) {
+ return new DslProperty(template.query(key, index))
+ }
+
+ /**
+ * First value of a request header e.g. request.headers.X-Request-Id
+ * @param key
+ */
+ DslProperty header(String key) {
+ return new DslProperty(template.header(key))
+ }
+
+ /**
+ * nth value of a request header (zero indexed) e.g. request.headers.X-Request-Id
+ * @param key
+ */
+ DslProperty header(String key, int index) {
+ return new DslProperty(template.header(key, index))
+ }
+
+ /**
+ * Request body text (avoid for non-text bodies)
+ */
+ DslProperty body() {
+ return new DslProperty(template.body())
+ }
+
+ /**
+ * Request body text for the given JsonPath
+ */
+ DslProperty body(String jsonPath) {
+ return new DslProperty(template.body(jsonPath))
+ }
+
+}
+
+
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy
new file mode 100644
index 0000000000..33d49aa348
--- /dev/null
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/HandlebarsContractTemplate.groovy
@@ -0,0 +1,69 @@
+package org.springframework.cloud.contract.spec.internal
+
+import groovy.transform.CompileStatic
+import org.springframework.cloud.contract.spec.ContractTemplate
+
+/**
+ * Represents the structure of templates using Handlebars compatible with
+ * WireMock template model requirements.
+ *
+ * @author Marcin Grzejszczak
+ * @since 1.1.0
+ */
+@CompileStatic
+class HandlebarsContractTemplate implements ContractTemplate {
+
+ @Override
+ String openingTemplate() {
+ return "{{{"
+ }
+
+ @Override
+ String closingTemplate() {
+ return "}}}"
+ }
+
+ @Override
+ String url() {
+ return wrapped("request.url")
+ }
+
+ @Override
+ String query(String key) {
+ return query(key, 0)
+ }
+
+ @Override
+ String query(String key, int index) {
+ return wrapped("request.query.${key}.[${index}]")
+ }
+
+ @Override
+ String header(String key) {
+ return header(key, 0)
+ }
+
+ @Override
+ String header(String key, int index) {
+ return wrapped("request.headers.${key}.[${index}]")
+ }
+
+ @Override
+ String body() {
+ return wrapped("request.body")
+ }
+
+ @Override
+ String escapedBody() {
+ return wrapped("escapejsonbody")
+ }
+
+ @Override
+ String body(String jsonPath) {
+ return wrapped("jsonpath this '${jsonPath}'")
+ }
+
+ private String wrapped(String text) {
+ return openingTemplate() + text + closingTemplate()
+ }
+}
diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy
index 6112243b12..4f6c032f87 100644
--- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy
+++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy
@@ -118,6 +118,10 @@ class Response extends Common {
closure()
}
+ FromRequest fromRequest() {
+ return new FromRequest()
+ }
+
@Override
DslProperty value(ClientDslProperty client, ServerDslProperty server) {
if (client.clientValue instanceof Pattern) {
diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java
index 727de1c5d7..889713b19b 100644
--- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java
+++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java
@@ -5,18 +5,24 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.contract.stubrunner.HttpServerStub;
+import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsEscapeHelper;
+import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsJsonPathHelper;
import org.springframework.cloud.contract.wiremock.WireMockSpring;
import org.springframework.util.ClassUtils;
import org.springframework.util.SocketUtils;
import org.springframework.util.StreamUtils;
+import com.github.jknack.handlebars.Helper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
/**
@@ -34,9 +40,24 @@ public class WireMockHttpServerStub implements HttpServerStub {
private WireMockConfiguration config() {
if (ClassUtils.isPresent("org.springframework.cloud.contract.wiremock.WireMockSpring", null)) {
- return WireMockSpring.options();
+ return WireMockSpring.options()
+ .extensions(responseTemplateTransformer());
}
- return new WireMockConfiguration();
+ return new WireMockConfiguration().extensions(responseTemplateTransformer());
+ }
+
+ private ResponseTemplateTransformer responseTemplateTransformer() {
+ return new ResponseTemplateTransformer(false, helpers());
+ }
+
+ /**
+ * Override this if you want to register your own helpers
+ */
+ protected Map helpers() {
+ Map helpers = new HashMap<>();
+ helpers.put(HandlebarsJsonPathHelper.NAME, new HandlebarsJsonPathHelper());
+ helpers.put(HandlebarsEscapeHelper.NAME, new HandlebarsEscapeHelper());
+ return helpers;
}
@Override
diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml
index dd4909600b..ebdee3b938 100644
--- a/spring-cloud-contract-verifier/pom.xml
+++ b/spring-cloud-contract-verifier/pom.xml
@@ -77,6 +77,10 @@
org.skyscreamer
jsonassert
+
+ com.github.jknack
+ handlebars
+
cglib
cglib
@@ -122,6 +126,21 @@
1.2
test
+
+ jetty-server
+ org.eclipse.jetty
+ test
+
+
+ jetty-servlet
+ org.eclipse.jetty
+ test
+
+
+ jetty-servlets
+ org.eclipse.jetty
+ test
+
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy
index c19d25c558..de3f392561 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/BlockBuilder.groovy
@@ -114,6 +114,11 @@ class BlockBuilder {
return character == "{" || character == toAdd
}
+ BlockBuilder updateContents(String contents) {
+ this.builder.replace(0, this.builder.length(), contents)
+ return this
+ }
+
@Override
String toString() {
return builder.toString()
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy
index 77bf86d2c1..41caff1368 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientSpockMethodRequestProcessingBodyBuilder.groovy
@@ -160,6 +160,9 @@ class JaxRsClientSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequ
@Override
protected String postProcessJsonPathCall(String jsonPath) {
+ if (templateProcessor.containsTemplateEntry(jsonPath)) {
+ return jsonPath
+ }
return jsonPath.replace('$', '\\$')
}
}
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy
index af6d47ae19..73e250fe34 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilder.groovy
@@ -24,6 +24,7 @@ import org.springframework.cloud.contract.spec.internal.Input
import org.springframework.cloud.contract.spec.internal.OutputMessage
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import org.springframework.cloud.contract.verifier.util.ContentType
+import org.springframework.cloud.contract.verifier.util.ContentUtils
import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent
import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader
@@ -45,7 +46,7 @@ abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder {
protected final OutputMessage outputMessage
MessagingMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) {
- super(configProperties)
+ super(configProperties, stubDefinition)
this.inputMessage = stubDefinition.input
this.outputMessage = stubDefinition.outputMessage
}
@@ -93,6 +94,12 @@ abstract class MessagingMethodBodyBuilder extends MethodBodyBuilder {
}
}
+ @Override
+ protected void processHeaderElement(BlockBuilder blockBuilder, String property, GString value) {
+ String gstringValue = ContentUtils.extractValueForGString(value, ContentUtils.GET_TEST_SIDE).toString()
+ processHeaderElement(blockBuilder, property, gstringValue)
+ }
+
protected ContentType getResponseContentType() {
ContentType contentType = recognizeContentTypeFromHeader(outputMessage.headers)
if (contentType == ContentType.UNKNOWN) {
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy
index 6b896bfb0d..b103e710d0 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy
@@ -16,20 +16,37 @@
package org.springframework.cloud.contract.verifier.builder
+import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.PathNotFoundException
import groovy.json.JsonOutput
import groovy.transform.PackageScope
import groovy.transform.TypeChecked
import org.apache.commons.lang3.StringEscapeUtils
-import org.springframework.cloud.contract.spec.internal.*
-import org.springframework.cloud.contract.verifier.util.MapConverter;
+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.ContractTemplate
+import org.springframework.cloud.contract.spec.internal.BodyMatcher
+import org.springframework.cloud.contract.spec.internal.BodyMatchers
+import org.springframework.cloud.contract.spec.internal.DslProperty
+import org.springframework.cloud.contract.spec.internal.ExecutionProperty
+import org.springframework.cloud.contract.spec.internal.Header
+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.OptionalProperty
+import org.springframework.cloud.contract.spec.internal.QueryParameter
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
+import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor
+import org.springframework.cloud.contract.verifier.template.TemplateProcessor
import org.springframework.cloud.contract.verifier.util.ContentType
import org.springframework.cloud.contract.verifier.util.JsonPaths
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
+import org.springframework.cloud.contract.verifier.util.MapConverter
import org.springframework.util.SerializationUtils
+import java.lang.invoke.MethodHandles
import java.util.regex.Pattern
import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue
@@ -46,10 +63,26 @@ import static org.springframework.cloud.contract.verifier.util.ContentUtils.extr
@PackageScope
abstract class MethodBodyBuilder {
- protected final ContractVerifierConfigProperties configProperties
+ private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass())
- protected MethodBodyBuilder(ContractVerifierConfigProperties configProperties) {
+ protected final ContractVerifierConfigProperties configProperties
+ protected final TemplateProcessor templateProcessor
+ protected final ContractTemplate contractTemplate
+ protected final Contract contract
+
+ protected MethodBodyBuilder(ContractVerifierConfigProperties configProperties, Contract contract) {
this.configProperties = configProperties
+ this.templateProcessor = processor()
+ this.contractTemplate = template()
+ this.contract = contract
+ }
+
+ private TemplateProcessor processor() {
+ return new HandlebarsTemplateProcessor()
+ }
+
+ private ContractTemplate template() {
+ return new HandlebarsTemplateProcessor()
}
/**
@@ -125,6 +158,11 @@ abstract class MethodBodyBuilder {
* Appends to the {@link BlockBuilder} the assertion for the given header path
*/
protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, String value)
+
+ /**
+ * Appends to the {@link BlockBuilder} the assertion for the given header path
+ */
+ protected abstract void processHeaderElement(BlockBuilder blockBuilder, String property, GString value)
/**
* Appends to the {@link BlockBuilder} the code to retrieve a value for a property
@@ -301,9 +339,16 @@ abstract class MethodBodyBuilder {
Object copiedBody = cloneBody(convertedResponseBody)
convertedResponseBody = JsonToJsonPathsConverter.removeMatchingJsonPaths(convertedResponseBody, bodyMatchers)
JsonPaths jsonPaths = new JsonToJsonPathsConverter(configProperties).transformToJsonPathWithTestsSideValues(convertedResponseBody)
+ DocumentContext parsedRequestBody
+ if (contract.request?.body) {
+ def requestBody = MapConverter.getTestSideValues(contract.request.body)
+ parsedRequestBody = JsonPath.parse(requestBody)
+ }
jsonPaths.each {
String method = it.method()
- String postProcessedMethod = postProcessJsonPathCall(method)
+ method = processIfTemplateIsPresent(method, parsedRequestBody)
+ String postProcessedMethod = templateProcessor.containsJsonPathTemplateEntry(method) ?
+ method : postProcessJsonPathCall(method)
bb.addLine("assertThatJson(parsedJson)" + postProcessedMethod)
addColonIfRequired(bb)
}
@@ -325,6 +370,21 @@ abstract class MethodBodyBuilder {
processBodyElement(bb, "", convertedResponseBody)
}
+ protected String processIfTemplateIsPresent(String method, DocumentContext parsedRequestBody) {
+ if (templateProcessor.containsTemplateEntry(method) &&
+ templateProcessor.containsJsonPathTemplateEntry(method) && contract.request?.body) {
+ // Unquoting the values of non strings
+ String jsonPathEntry = templateProcessor.jsonPathFromTemplateEntry(method)
+ Object object = parsedRequestBody.read(jsonPathEntry)
+ if (!(object instanceof String)) {
+ return method
+ .replace('"' + contractTemplate.openingTemplate(), contractTemplate.openingTemplate())
+ .replace(contractTemplate.closingTemplate() + '"', contractTemplate.closingTemplate())
+ }
+ }
+ return method
+ }
+
protected void methodForEqualityCheck(BodyMatcher bodyMatcher, BlockBuilder bb, Object copiedBody) {
String path = quotedAndEscaped(bodyMatcher.path())
Object retrievedValue = value(copiedBody, bodyMatcher)
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy
index 446fa1305e..bbca85cfeb 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcSpockMethodRequestProcessingBodyBuilder.groovy
@@ -25,7 +25,6 @@ import org.springframework.cloud.contract.spec.internal.NotToEscapePattern
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import java.util.regex.Pattern
-
/**
* A {@link SpockMethodRequestProcessingBodyBuilder} implementation that uses MockMvc to send requests.
*
@@ -63,6 +62,7 @@ class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestP
"${patternComparison(value.serverValue.pattern().replace("\\", "\\\\"))}")
}
}
+
@Override
protected void processHeaderElement(BlockBuilder blockBuilder, String property, ExecutionProperty exec) {
blockBuilder.addLine("${exec.insertValue("response.header(\'$property\')")}")
@@ -81,6 +81,9 @@ class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestP
// #273 - should escape $ for Groovy since it will try to make it a GString
@Override
protected String postProcessJsonPathCall(String jsonPath) {
+ if (templateProcessor.containsTemplateEntry(jsonPath)) {
+ return jsonPath
+ }
return jsonPath.replace('$', '\\$')
}
}
\ No newline at end of file
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy
index 62a34d200a..0492aa4023 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy
@@ -21,21 +21,22 @@ import groovy.transform.PackageScope
import groovy.transform.TypeChecked
import groovy.transform.TypeCheckingMode
import org.springframework.cloud.contract.spec.Contract
+import org.springframework.cloud.contract.spec.internal.BodyMatchers
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
-import org.springframework.cloud.contract.spec.internal.Request
import org.springframework.cloud.contract.spec.internal.Header
import org.springframework.cloud.contract.spec.internal.MatchingStrategy
import org.springframework.cloud.contract.spec.internal.NamedProperty
import org.springframework.cloud.contract.spec.internal.QueryParameter
+import org.springframework.cloud.contract.spec.internal.Request
import org.springframework.cloud.contract.spec.internal.Response
import org.springframework.cloud.contract.spec.internal.Url
-import org.springframework.cloud.contract.verifier.util.MapConverter
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import org.springframework.cloud.contract.verifier.util.ContentType
+import org.springframework.cloud.contract.verifier.util.ContentUtils
+import org.springframework.cloud.contract.verifier.util.MapConverter
import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent
import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader
-
/**
* An abstraction for creating a test method that includes processing of an HTTP request
*
@@ -55,7 +56,7 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder {
private static final String QUERY_PARAM_METHOD = 'queryParam'
RequestProcessingMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) {
- super(configProperties)
+ super(configProperties, stubDefinition)
this.request = stubDefinition.request
this.response = stubDefinition.response
}
@@ -154,6 +155,19 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder {
}
}
+ @Override
+ protected void validateResponseBodyBlock(BlockBuilder bb, BodyMatchers bodyMatchers, Object responseBody) {
+ super.validateResponseBodyBlock(bb, bodyMatchers, responseBody)
+ String newBody = this.templateProcessor.transform(request, bb.toString())
+ bb.updateContents(newBody)
+ }
+
+ @Override
+ protected void processHeaderElement(BlockBuilder blockBuilder, String property, GString value) {
+ String gstringValue = ContentUtils.extractValueForGString(value, ContentUtils.GET_TEST_SIDE).toString()
+ processHeaderElement(blockBuilder, property, gstringValue)
+ }
+
@Override
protected ContentType getResponseContentType() {
ContentType contentType = recognizeContentTypeFromHeader(response.headers)
@@ -214,4 +228,4 @@ abstract class RequestProcessingMethodBodyBuilder extends MethodBodyBuilder {
private boolean hasQueryParams(Url url) {
return url.queryParameters
}
-}
+}
\ No newline at end of file
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy
index c64726d68d..3f265e9a08 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMessagingMethodBodyBuilder.groovy
@@ -194,6 +194,9 @@ class SpockMessagingMethodBodyBuilder extends MessagingMethodBodyBuilder {
// #273 - should escape $ for Groovy since it will try to make it a GString
@Override
protected String postProcessJsonPathCall(String method) {
+ if (templateProcessor.containsTemplateEntry(method)) {
+ return method
+ }
return method.replace('$', '\\$')
}
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy
index ea6a8b7130..07124834f2 100644
--- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SpockMethodRequestProcessingBodyBuilder.groovy
@@ -25,6 +25,7 @@ import org.springframework.cloud.contract.spec.internal.NamedProperty
import org.springframework.cloud.contract.spec.internal.Request
import org.springframework.cloud.contract.spec.internal.ExecutionProperty
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
+import org.springframework.cloud.contract.verifier.util.ContentUtils
import java.util.regex.Pattern
@@ -140,6 +141,12 @@ abstract class SpockMethodRequestProcessingBodyBuilder extends RequestProcessing
return ".param('$parameter.key', '$parameter.value')"
}
+ @Override
+ protected void processHeaderElement(BlockBuilder blockBuilder, String property, GString value) {
+ String gstringValue = ContentUtils.extractValueForGString(value, ContentUtils.GET_TEST_SIDE).toString()
+ processHeaderElement(blockBuilder, property, gstringValue)
+ }
+
protected String convertHeaderComparison(String headerValue) {
return " == '$headerValue'"
}
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy
new file mode 100644
index 0000000000..b66e795591
--- /dev/null
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.groovy
@@ -0,0 +1,83 @@
+package org.springframework.cloud.contract.verifier.builder
+
+import groovy.json.JsonOutput
+import groovy.transform.CompileStatic
+import groovy.transform.Immutable
+import org.apache.commons.lang3.StringEscapeUtils
+import org.springframework.cloud.contract.spec.internal.DslProperty
+import org.springframework.cloud.contract.spec.internal.Request
+import org.springframework.cloud.contract.verifier.util.ContentUtils
+import org.springframework.cloud.contract.verifier.util.MapConverter
+
+/**
+ * Representation of the request side to be used for response templating in
+ * the generated tests.
+ *
+ * @author Marcin Grzejszczak
+ * @since 1.1.0
+ */
+@Immutable
+@CompileStatic
+class TestSideRequestTemplateModel {
+ /**
+ * Request URL
+ */
+ final String url
+
+ /**
+ * Map containing query parameters
+ */
+ final Map> query
+
+ /**
+ * Map containing request headers
+ */
+ final Map> headers
+
+ /**
+ * Escaped request body that can be put into test
+ */
+ final String body
+
+ /**
+ * Request body as it would be sent to the controller
+ */
+ final String rawBody
+
+ static TestSideRequestTemplateModel from(final Request request) {
+ String url = MapConverter.getTestSideValues(request.url ?: request.urlPath)
+ Map> query = (Map>) (request.url ?: request.urlPath)
+ .queryParameters?.parameters?.groupBy { it.name }?.collectEntries {
+ [(it.key): it.value.collect { MapConverter.getTestSideValues(it) }]
+ }
+ Map> headers = (Map>) (request.headers?.entries?.groupBy {
+ it.name
+ }?.collectEntries {
+ [(it.key): it.value.collect { MapConverter.getTestSideValues(it) }]
+ })
+ String body = trimmedAndEscapedBody(request.body)
+ String rawBody = getBodyAsRawJson(request.body)
+ return new TestSideRequestTemplateModel(url, query, headers, body, rawBody)
+ }
+
+ private static String trimmedAndEscapedBody(Object body) {
+ String rawBody = getBodyAsRawJson(body)
+ return StringEscapeUtils.escapeJava(rawBody)
+ }
+
+ private static String getBodyAsRawJson(Object body) {
+ Object bodyValue = extractServerValueFromBody(body)
+ return new JsonOutput().toJson(bodyValue)
+ }
+
+ protected static Object extractServerValueFromBody(bodyValue) {
+ if (bodyValue instanceof GString) {
+ bodyValue = ContentUtils.extractValue(bodyValue, { DslProperty dslProperty -> dslProperty.serverValue })
+ } else {
+ bodyValue = MapConverter.transformValues(bodyValue, {
+ it instanceof DslProperty ? it.serverValue : it
+ })
+ }
+ return bodyValue
+ }
+}
diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy
new file mode 100644
index 0000000000..b4ca4933b1
--- /dev/null
+++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/handlebars/HandlebarsEscapeHelper.groovy
@@ -0,0 +1,40 @@
+package org.springframework.cloud.contract.verifier.builder.handlebars
+
+import com.github.jknack.handlebars.Helper
+import com.github.jknack.handlebars.Options
+import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel
+import groovy.transform.CompileStatic
+import org.apache.commons.lang3.StringEscapeUtils
+import org.springframework.cloud.contract.verifier.builder.TestSideRequestTemplateModel
+/**
+ * A Handlebars helper for the {@code escapejsonbody} helper function.
+ *
+ * @author Marcin Grzejszczak
+ * @since 1.1.0
+ */
+@CompileStatic
+class HandlebarsEscapeHelper implements Helper