diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy deleted file mode 100644 index c22c2d18a9..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2013-2020 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.wiremock - -import java.nio.charset.StandardCharsets -import java.util.regex.Pattern - -import groovy.io.FileType -import groovy.json.JsonOutput -import groovy.json.JsonParserType -import groovy.json.JsonSlurper -import groovy.transform.CompileDynamic -import groovy.xml.XmlUtil -import repackaged.nl.flotsam.xeger.Xeger - -import org.springframework.cloud.contract.spec.internal.RegexProperty -import org.springframework.cloud.contract.verifier.util.ContentUtils - -import org.springframework.cloud.contract.spec.Contract - -import static org.apache.commons.text.StringEscapeUtils.escapeJava - -/** - * Converts WireMock stubs into the DSL format - * - * @author Marcin Grzejszczak - * @author Konstantin Shevchuk - * - * @since 1.0.0 - */ -@CompileDynamic -class WireMockToDslConverter { - - /** - * @return the string content of the contract - * - * @param wireMockStringStub - string content of the WireMock JSON stub - */ - static String fromWireMockStub(String wireMockStringStub) { - return new WireMockToDslConverter().convertFromWireMockStub(wireMockStringStub) - } - - private String convertFromWireMockStub(String wireMockStringStub) { - Object wireMockStub = parseStubDefinition(wireMockStringStub) - Integer priority = wireMockStub.priority - def request = wireMockStub.request - def response = wireMockStub.response - def bodyPatterns = request.bodyPatterns - String urlPattern = request.urlPattern - String urlPathPattern = request.urlPathPattern - return """\ - ${priority ? "priority ${priority}" : ''} - request { - ${request.method ? "method \"\"\"$request.method\"\"\"" : ""} - ${request.url ? "url \"\"\"$request.url\"\"\"" : ""} - ${ - urlPattern ? "url \$(consumer(regex('${escapeJava(urlPattern)}')), producer('${new Xeger(escapeJava(urlPattern)).generate()}'))" : "" - } - ${ - urlPathPattern ? "urlPath \$(consumer(regex('${escapeJava(urlPattern)}')), producer('${new Xeger(escapeJava(urlPattern)).generate()}'))" : "" - } - ${request.urlPath ? "url \"\"\"$request.urlPath\"\"\"" : ""} - ${ - request.headers ? """headers { - ${ - request.headers.collect { - def assertion = it.value - String headerName = it.key as String - def entry = assertion.entrySet().first() - """header(\"\"\"$headerName\"\"\", ${ - buildRequestHeader(entry.key, entry.value) - })\n""" - }.join('') - } - } - """ : "" - } - ${ - bodyPatterns?.equalTo?. - every { it } ? "body('''${bodyPatterns.equalTo[0]}''')" : '' - } - ${ - bodyPatterns?.equalToJson?. - every { it } ? "body('''${bodyPatterns.equalToJson[0]}''')" : '' - } - ${ - bodyPatterns?.matches?.every { - it - } ? "body \$(consumer(regex('${escapeJava(bodyPatterns.matches[0])}')), producer('${new Xeger(escapeJava(bodyPatterns.matches[0])).generate()}'))" : "" - } - } - response { - ${response.status ? "status $response.status" : ""} - ${response.body ? "body( ${buildBody(response.body)})" : ""} - ${ - response.headers ? """headers { - ${ - response.headers.collect { "header('$it.key': '${it.value}')\n" }.join('') - } - } - """ : "" - } - } - """ - } - - private Object parseStubDefinition(String wireMockStringStub) { - return new JsonSlurper().setType(JsonParserType.LAX).parseText(wireMockStringStub) - } - - private String buildRequestHeader(String method, Object value) { - switch (method) { - case 'equalTo': - return wrapWithMultilineGString(value) - case 'contains': - String regex = '^.*' + value.toString() + '.*$' - String escapedRegex = escapeJava(regex) - String wrappedRegex = wrapWithMultilineGString(escapedRegex) - return 'c(regex(' + wrappedRegex + '))' - default: - String regex = value.toString() - String escapedRegex = escapeJava(regex) - String wrappedRegex = wrapWithMultilineGString(escapedRegex) - return 'c(regex(' + wrappedRegex + '))' - } - } - - private Object buildBody(Map responseBody) { - return responseBody.entrySet().collectAll(withQuotedMapStringElements()). - inject([:], appendToIterable()) - } - - private Object buildBody(List responseBody) { - return responseBody.collectAll(withQuotedStringElements()). - inject([], appendToIterable()) - } - - private Object buildBody(Integer responseBody) { - return responseBody - } - - private Object buildBody(String responseBody) { - try { - def json = new JsonSlurper().parseText(responseBody) - return wrapWithMultilineGString(JsonOutput.prettyPrint(responseBody)) - } - catch (Exception jsonException) { - try { - def xml = ContentUtils.getXmlSlurperWithDefaultErrorHandler().parseText(responseBody) - return wrapWithMultilineGString(XmlUtil.serialize(responseBody)) - } - catch (Exception xmlException) { - return wrapWithMultilineGString(responseBody) - } - } - } - - private String wrapWithMultilineGString(String string) { - return """'''$string'''""" - } - - private Closure withQuotedMapStringElements() { - return { - [(it.key): convert(it.value)] - } - } - - private Closure withQuotedStringElements() { - return { - convert(it) - } - } - - private Closure appendToIterable() { - return { - acc, el -> acc << el - } - } - - private Object convert(Object element) { - return element - } - - private Object convert(String element) { - return quoteString(element) - } - - private String quoteString(String element) { - if (element =~ /^".*"$/) { - return element - } - return """\"\"\"${escapeJava(element)}\"\"\"""" - } - - private Object convert(List element) { - return element.collect { - convert(it) - } - } - - private Object convert(Map element) { - return element.collectEntries { - [(it.key): convert(it.value)] - } - } - - static void main(String[] args) { - String rootOfFolderWithStubs = args[0] - new File(rootOfFolderWithStubs).eachFileRecurse(FileType.FILES) { - try { - if (!it.name.endsWith('json')) { - return - } - String dslFromWireMockStub = - fromWireMockStub(it.getText(StandardCharsets.UTF_8.toString())) - String dslWrappedWithFactoryMethod = - wrapWithFactoryMethod(dslFromWireMockStub) - File newGroovyFile = new File(it.parent, it.name. - replaceAll('json', 'groovy')) - println("Creating new groovy file [$newGroovyFile.path]") - newGroovyFile.setText(dslWrappedWithFactoryMethod, StandardCharsets.UTF_8. - toString()) - } - catch (Exception e) { - System.err.println(e) - } - - } - } - - static String wrapWithFactoryMethod(String dslFromWireMockStub) { - return """\ -${Contract.name}.make { - $dslFromWireMockStub -} -""" - } -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.java new file mode 100644 index 0000000000..1bf342ef37 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.java @@ -0,0 +1,321 @@ +/* + * Copyright 2013-2020 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.wiremock; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.core.util.Separators; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import repackaged.nl.flotsam.xeger.Xeger; + +import static org.apache.commons.text.StringEscapeUtils.escapeJava; + +/** + * Converts WireMock stubs into the DSL format. + * + * @author Marcin Grzejszczak + * @author Konstantin Shevchuk + * @since 1.0.0 + */ +public class WireMockToDslConverter { + + private static final JsonPointer PRIORITY_POINTER = JsonPointer.compile("/priority"); + + private static final JsonPointer REQUEST_METHOD_POINTER = JsonPointer.compile("/request/method"); + + private static final JsonPointer REQUEST_URL_POINTER = JsonPointer.compile("/request/url"); + + private static final JsonPointer REQUEST_URL_PATH_POINTER = JsonPointer.compile("/request/urlPath"); + + private static final JsonPointer REQUEST_URL_PATTERN_POINTER = JsonPointer.compile("/request/urlPattern"); + + private static final JsonPointer REQUEST_URL_PATH_PATTERN_POINTER = JsonPointer.compile("/request/urlPathPattern"); + + private static final JsonPointer REQUEST_HEADERS_POINTER = JsonPointer.compile("/request/headers"); + + private static final JsonPointer REQUEST_BODY_POINTER = JsonPointer.compile("/request/bodyPatterns"); + + private static final JsonPointer RESPONSE_STATUS_POINTER = JsonPointer.compile("/response/status"); + + private static final JsonPointer RESPONSE_BODY_POINTER = JsonPointer.compile("/response/body"); + + private static final JsonPointer RESPONSE_HEADERS_POINTER = JsonPointer.compile("/response/headers"); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_COMMENTS, true); + OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_YAML_COMMENTS, true); + OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + } + + /** + * Returns the string content of the contract. + * + * @param wireMockStringStub - string content of the WireMock JSON stub + */ + public static String fromWireMockStub(String wireMockStringStub) { + return new WireMockToDslConverter().convertFromWireMockStub(wireMockStringStub); + } + + private String convertFromWireMockStub(String wireMockStringStub) { + JsonNode wireMockStub = parseStubDefinition(wireMockStringStub); + return buildPriority(wireMockStub) + + "request {\n" + + buildRequestMethod(wireMockStub) + + buildRequestUrl(wireMockStub) + + buildRequestUrlPattern(wireMockStub) + + buildRequestUrlPathPattern(wireMockStub) + + buildRequestUrlPath(wireMockStub) + + buildRequestHeaders(wireMockStub) + + buildRequestBody(wireMockStub) + + "}\n" + + "response {\n" + + buildResponseStatus(wireMockStub) + + buildResponseBody(wireMockStub) + + buildResponseHeaders(wireMockStub) + + "}" + + "\n"; + } + + private JsonNode parseStubDefinition(String wireMockStringStub) { + try { + return OBJECT_MAPPER.reader().readTree(wireMockStringStub); + } + catch (IOException e) { + throw new RuntimeException("WireMock string stub could not be read", e); + } + } + + private String buildPriority(JsonNode wireMockStub) { + String priority = ""; + JsonNode priorityNode = wireMockStub.at(PRIORITY_POINTER); + if (priorityNode != null && priorityNode.asInt() > 0) { + priority = "priority " + priorityNode.asInt() + "\n"; + } + return priority; + } + + private String buildRequestMethod(JsonNode wireMockStub) { + String requestMethod = ""; + JsonNode requestMethodNode = wireMockStub.at(REQUEST_METHOD_POINTER); + if (requestMethodNode != null) { + requestMethod = "method '" + requestMethodNode.asText() + "'\n"; + } + return requestMethod; + } + + private String buildRequestUrl(JsonNode wireMockStub) { + String requestUrl = ""; + JsonNode requestUrlNode = wireMockStub.at(REQUEST_URL_POINTER); + if (!requestUrlNode.isMissingNode()) { + requestUrl = "url '" + requestUrlNode.asText() + "'\n"; + } + return requestUrl; + } + + private String buildRequestUrlPath(JsonNode wireMockStub) { + String requestUrlPath = ""; + JsonNode requestUrlPathNode = wireMockStub.at(REQUEST_URL_PATH_POINTER); + if (!requestUrlPathNode.isMissingNode()) { + requestUrlPath = "url '" + requestUrlPathNode.asText() + "'\n"; + } + return requestUrlPath; + } + + private String buildRequestUrlPattern(JsonNode wireMockStub) { + String requestUrlPattern = ""; + JsonNode requestUrlPatternNode = wireMockStub.at(REQUEST_URL_PATTERN_POINTER); + if (!requestUrlPatternNode.isMissingNode()) { + String escapedRequestUrlPatternValue = escapeJava(requestUrlPatternNode.asText()); + requestUrlPattern = "url $(consumer(regex('" + escapedRequestUrlPatternValue + + "')), producer('" + + new Xeger(escapedRequestUrlPatternValue).generate() + "'))\n"; + } + return requestUrlPattern; + } + + private String buildRequestUrlPathPattern(JsonNode wireMockStub) { + String requestUrlPathPattern = ""; + JsonNode requestUrlPathPatternNode = wireMockStub.at(REQUEST_URL_PATH_PATTERN_POINTER); + if (!requestUrlPathPatternNode.isMissingNode()) { + String escapedRequestUrlPathPatternValue = escapeJava(requestUrlPathPatternNode.asText()); + requestUrlPathPattern = "urlPath $(consumer(regex('" + + escapedRequestUrlPathPatternValue + "')), producer('" + + new Xeger(escapedRequestUrlPathPatternValue).generate() + "'))'\n"; + } + return requestUrlPathPattern; + } + + private String buildRequestHeaders(JsonNode wireMockStub) { + final StringBuilder requestHeadersBuilder = new StringBuilder(); + JsonNode requestHeadersNode = wireMockStub.at(REQUEST_HEADERS_POINTER); + + if (requestHeadersNode.isObject()) { + requestHeadersBuilder.append("headers {\n"); + ObjectNode requestHeadersObjectNode = requestHeadersNode.deepCopy(); + Iterator> fields = requestHeadersObjectNode.fields(); + fields.forEachRemaining(c -> { + requestHeadersBuilder.append("header('").append(c.getKey()).append("',"); + Map.Entry headerValue = c.getValue().deepCopy().fields().next(); + String header = buildHeader(headerValue.getKey(), headerValue.getValue().asText()); + requestHeadersBuilder.append(header).append(")").append("\n"); + }); + requestHeadersBuilder.append("}"); + } + return requestHeadersBuilder.toString(); + } + + private String buildHeader(String method, String value) { + switch (method) { + case "equalTo": + return "'" + value + "'"; + case "contains": + String regex = "^.*" + value + ".*$"; + return "c(regex('" + escapeJava(regex) + "'))"; + default: + return "c(regex('" + escapeJava(value) + "'))"; + } + } + + private String buildRequestBody(JsonNode wireMockStub) { + final StringBuilder requestBody = new StringBuilder(); + JsonNode requestBodyNode = wireMockStub.at(REQUEST_BODY_POINTER); + if (requestBodyNode.isArray()) { + ArrayNode requestBodyArrayNode = requestBodyNode.deepCopy(); + Iterator elements = requestBodyArrayNode.elements(); + Iterable iterableFields = () -> elements; + List> requestBodyObjectNodes = new ArrayList<>(); + StreamSupport.stream(iterableFields.spliterator(), false) + .filter(f -> f instanceof ObjectNode) + .map(f -> (ObjectNode) f) + .map(ObjectNode::fields) + .forEachOrdered(i -> i.forEachRemaining(requestBodyObjectNodes::add)); + requestBodyObjectNodes.stream() + .filter(b -> b.getKey().equals("equalTo")) + .findFirst() + .ifPresent(b -> requestBody.append("body ('").append(b.getValue().asText()).append("')")); + requestBodyObjectNodes.stream() + .filter(b -> b.getKey().equals("equalToJson")) + .findFirst() + .ifPresent(b -> requestBody.append("body ('").append(b.getValue().asText()).append("')")); + requestBodyObjectNodes.stream().filter(b -> b.getKey().equals("matches")) + .findFirst() + .ifPresent(b -> requestBody.append("body $(consumer(regex('") + .append(escapeJava(b.getValue().asText())) + .append("')), producer('") + .append(new Xeger(escapeJava(b.getValue().asText())).generate()) + .append("'))")); + } + return requestBody.toString(); + } + + private String buildResponseStatus(JsonNode wireMockStub) { + String responseStatus = ""; + JsonNode responseStatusNode = wireMockStub.at(RESPONSE_STATUS_POINTER); + if (!responseStatusNode.isMissingNode()) { + int responseStatusValue = responseStatusNode.asInt(); + responseStatus += "status " + responseStatusValue + "\n"; + } + return responseStatus; + } + + private String buildResponseBody(JsonNode wireMockStub) { + String responseBody = ""; + JsonNode responseBodyNode = wireMockStub.at(RESPONSE_BODY_POINTER); + if (responseBodyNode.isInt()) { + responseBody += "body( " + escapeJava(buildPrettyPrintResponseBody((IntNode) responseBodyNode)) + ")\n"; + } + if (responseBodyNode.isTextual()) { + responseBody += "body( \"" + escapeJava(buildPrettyPrintResponseBody((TextNode) responseBodyNode)) + "\")\n"; + } + return responseBody; + } + + private String buildPrettyPrintResponseBody(IntNode node) { + return node.asText(); + } + + private String buildPrettyPrintResponseBody(TextNode node) { + try { + String textNode = node.asText(); + Object intermediateObjectForPrettyPrinting = OBJECT_MAPPER.reader() + .readValue(textNode, Object.class); + DefaultIndenter customIndenter = new DefaultIndenter(" ", "\n"); + return OBJECT_MAPPER + .writer(new PrivatePrettyPrinter() + .withArrayIndenter(customIndenter) + .withObjectIndenter(customIndenter)) + .writeValueAsString(intermediateObjectForPrettyPrinting); + } + catch (IOException e) { + throw new RuntimeException("WireMock response body could not be pretty printed"); + } + } + + private String buildResponseHeaders(JsonNode wireMockStub) { + final StringBuilder responseHeadersBuilder = new StringBuilder(); + JsonNode requestHeadersNode = wireMockStub.at(RESPONSE_HEADERS_POINTER); + + if (requestHeadersNode.isObject()) { + responseHeadersBuilder.append("headers {\n"); + ObjectNode responseHeadersObjectNode = requestHeadersNode.deepCopy(); + Iterator> fields = responseHeadersObjectNode.fields(); + fields.forEachRemaining(c -> responseHeadersBuilder + .append("header('") + .append(c.getKey()) + .append("',") + .append("'") + .append(c.getValue().asText()) + .append("')\n")); + responseHeadersBuilder.append("}"); + } + return responseHeadersBuilder.toString(); + } + + private static class PrivatePrettyPrinter extends DefaultPrettyPrinter { + + @Override + public DefaultPrettyPrinter createInstance() { + return new PrivatePrettyPrinter(); + } + + @Override + public DefaultPrettyPrinter withSeparators(Separators separators) { + _separators = separators; + _objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " "; + return this; + } + + } + +}