diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.groovy deleted file mode 100644 index 9e2774af33..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.groovy +++ /dev/null @@ -1,222 +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.spec.pact - -import java.util.regex.Pattern - -import au.com.dius.pact.consumer.dsl.DslPart -import au.com.dius.pact.consumer.dsl.PactDslJsonArray -import au.com.dius.pact.consumer.dsl.PactDslJsonBody -import au.com.dius.pact.core.model.OptionalBody -import au.com.dius.pact.core.model.Request -import au.com.dius.pact.core.model.Response -import au.com.dius.pact.core.model.generators.Generator -import au.com.dius.pact.core.model.messaging.Message -import com.jayway.jsonpath.Configuration -import com.jayway.jsonpath.internal.EvaluationContext -import com.jayway.jsonpath.internal.Path -import com.jayway.jsonpath.internal.PathRef -import com.jayway.jsonpath.internal.path.PathCompiler -import groovy.json.JsonException -import groovy.json.JsonSlurper -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.internal.Body -import org.springframework.cloud.contract.spec.internal.ClientDslProperty -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.internal.ServerDslProperty -import org.springframework.cloud.contract.verifier.util.ContentUtils - -/** - * @author Tim Ysewyn - * @Since 2.0.0 - */ -@CompileStatic -@PackageScope -class BodyConverter { - - private static final JsonSlurper jsonSlurper = new JsonSlurper() - - static DslPart toPactBody(Body body, Closure dslPropertyValueExtractor) { - return traverse(body, null, dslPropertyValueExtractor) - } - - static DslPart toPactBody(DslProperty dslProperty, Closure dslPropertyValueExtractor) { - return traverse(dslProperty, null, dslPropertyValueExtractor) - } - - private static DslPart traverse(Object value, DslPart parent, Closure dslPropertyValueExtractor) { - boolean isRoot = parent == null - Object v = value - if (v instanceof DslProperty) { - v = dslPropertyValueExtractor(v) - } - if (v instanceof GString) { - v = ContentUtils.extractValue(v, dslPropertyValueExtractor) - } - if (v instanceof String) { - v = v.trim() - if (v.startsWith("{") && v.endsWith("}")) { - try { - v = jsonSlurper.parseText(v as String) - } - catch (JsonException ex) { /*it wasn't a JSON string after all...*/ - } - } - } - DslPart p = isRoot ? createRootDslPart(v) : parent - if (v instanceof Map) { - processMap(v as Map, p as PactDslJsonBody, dslPropertyValueExtractor) - } - else if (v instanceof Collection) { - processCollection(v as Collection, p as PactDslJsonArray, dslPropertyValueExtractor) - } - return p - } - - private static DslPart createRootDslPart(Object value) { - return value instanceof Collection ? new PactDslJsonArray() : new PactDslJsonBody() - } - - private static void processCollection(Collection values, PactDslJsonArray jsonArray, Closure dslPropertyValueExtractor) { - values.forEach({ - Object v = it - if (v instanceof DslProperty) { - v = dslPropertyValueExtractor(v) - } - if (v instanceof GString) { - v = ContentUtils.extractValue(v, dslPropertyValueExtractor) - } - if (v == null) { - jsonArray.nullValue() - } - else if (v instanceof String) { - jsonArray.string(v) - } - else if (v instanceof Number) { - jsonArray.number(v) - } - else if (v instanceof Map) { - PactDslJsonBody current = jsonArray.object() - traverse(v, current, dslPropertyValueExtractor) - current.closeObject() - } - else if (v instanceof Collection) { - PactDslJsonArray current = jsonArray.array() - traverse(v, current, dslPropertyValueExtractor) - current.closeArray() - } - }) - } - - private static void processMap(Map values, PactDslJsonBody jsonObject, Closure dslPropertyValueExtractor) { - values.forEach({ String k, Object v -> - if (v instanceof DslProperty) { - v = dslPropertyValueExtractor(v) - } - if (v instanceof GString) { - v = ContentUtils.extractValue(v, dslPropertyValueExtractor) - } - if (v == null) { - jsonObject.nullValue(k) - } - else if (v instanceof String) { - jsonObject.stringType(k, v) - } - else if (v instanceof Number) { - jsonObject.numberValue(k, v) - } - else if (v instanceof Map) { - PactDslJsonBody current = jsonObject.object(k) - traverse(v, current, dslPropertyValueExtractor) - current.closeObject() - } - else if (v instanceof Collection) { - PactDslJsonArray current = jsonObject.array(k) - traverse(v, current, dslPropertyValueExtractor) - current.closeArray() - } - }) - } - - static def toSCCBody(Request request) { - def body = parseBody(request.body) - if (request.generators.isNotEmpty() - && request.generators.categories. - containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) { - applyGenerators(body, request.generators.categories. - get(au.com.dius.pact.core.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue -> - return new DslProperty(new ClientDslProperty(pattern, generatedValue), currentValue) - } - } - return body - } - - static def toSCCBody(Response response) { - def body = parseBody(response.body) - if (response.generators.isNotEmpty() - && response.generators.categories. - containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) { - applyGenerators(body, response.generators.categories. - get(au.com.dius.pact.core.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue -> - return new DslProperty(currentValue, new ServerDslProperty(pattern, generatedValue)) - } - } - return body - } - - static def toSCCBody(Message message) { - def body = parseBody(message.contents) - if (message.generators.isNotEmpty() - && message.generators.categories. - containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) { - applyGenerators(body, message.generators.categories. - get(au.com.dius.pact.core.model.generators.Category.BODY)) { Object currentValue, Pattern pattern, Object generatedValue -> - return new DslProperty(new ClientDslProperty(pattern, generatedValue), currentValue) - } - } - return body - } - - private static def parseBody(OptionalBody optionalBody) { - if (optionalBody.present) { - return new JsonSlurper().parse(optionalBody.value) - } - else { - return optionalBody.value - } - } - - private static void applyGenerators(def body, Map generatorsPerPath, Closure dslPropertyProvider) { - Configuration configuration = Configuration.defaultConfiguration() - generatorsPerPath.each { String path, Generator generator -> - Path compiledPath = PathCompiler.compile(path) - EvaluationContext evaluationContext = compiledPath. - evaluate(body, body, configuration, true) - evaluationContext.updateOperations().each { PathRef pathRef -> - pathRef.convert({ Object currentValue, Configuration config -> - return ValueGeneratorConverter. - convert(generator) { Pattern pattern, Object generatedValue -> - return dslPropertyProvider(currentValue, pattern, generatedValue) - } - }, configuration) - } - } - } - -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy deleted file mode 100644 index c11728171b..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.groovy +++ /dev/null @@ -1,117 +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.spec.pact - -import au.com.dius.pact.core.model.matchingrules.Category -import au.com.dius.pact.core.model.matchingrules.DateMatcher -import au.com.dius.pact.core.model.matchingrules.EqualsMatcher -import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher -import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher -import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher -import au.com.dius.pact.core.model.matchingrules.NullMatcher -import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher -import au.com.dius.pact.core.model.matchingrules.RegexMatcher -import au.com.dius.pact.core.model.matchingrules.TimeMatcher -import au.com.dius.pact.core.model.matchingrules.TimestampMatcher -import au.com.dius.pact.core.model.matchingrules.TypeMatcher -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.internal.BodyMatcher -import org.springframework.cloud.contract.spec.internal.BodyMatchers -import org.springframework.cloud.contract.spec.internal.MatchingType -import org.springframework.cloud.contract.spec.internal.RegexPatterns - -/** - * @author Tim Ysewyn - * @Since 2.0.0 - */ -@CompileStatic -@PackageScope -class MatchingRulesConverter { - - static Category matchingRulesForBody(BodyMatchers bodyMatchers) { - return matchingRulesFor("body", bodyMatchers) - } - - private static Category matchingRulesFor(String categoryName, BodyMatchers bodyMatchers) { - Category category = new Category(categoryName) - bodyMatchers.matchers().forEach({ BodyMatcher it -> - String key = getMatcherKey(it.path()) - MatchingType matchingType = it.matchingType() - switch (matchingType) { - case MatchingType.NULL: - category.addRule(key, NullMatcher.INSTANCE) - break - case MatchingType.EQUALITY: - category.addRule(key, EqualsMatcher.INSTANCE) - break - case MatchingType.TYPE: - if (it.minTypeOccurrence() && it.maxTypeOccurrence()) { - category. - addRule(key, new MinMaxTypeMatcher(it.minTypeOccurrence(), it. - maxTypeOccurrence())) - } - else if (it.minTypeOccurrence()) { - category.addRule(key, new MinTypeMatcher(it.minTypeOccurrence())) - } - else if (it.maxTypeOccurrence()) { - category.addRule(key, new MaxTypeMatcher(it.maxTypeOccurrence())) - } - else { - category.addRule(key, TypeMatcher.INSTANCE) - } - break - case MatchingType.DATE: - category.addRule(key, new DateMatcher()) - break - case MatchingType.TIME: - category.addRule(key, new TimeMatcher()) - break - case MatchingType.TIMESTAMP: - category.addRule(key, new TimestampMatcher()) - break - case MatchingType.REGEX: - String pattern = it.value().toString() - if (pattern == RegexPatterns.number().pattern()) { - category. - addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER)) - } - else if (pattern == RegexPatterns.anInteger().pattern()) { - category. - addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER)) - } - else if (pattern == RegexPatterns.aDouble().pattern()) { - category. - addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL)) - } - else { - category.addRule(key, new RegexMatcher(pattern)) - } - break - default: - break - } - }) - return category - } - - private static String getMatcherKey(String path) { - return "${path.startsWith('$') ? path.substring(1) : path}" - } - -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy deleted file mode 100644 index 41421ec082..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.groovy +++ /dev/null @@ -1,120 +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.spec.pact - -import au.com.dius.pact.consumer.MessagePactBuilder -import au.com.dius.pact.consumer.dsl.DslPart -import au.com.dius.pact.core.model.messaging.MessagePact -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.internal.Header -import org.springframework.cloud.contract.spec.internal.Headers -import org.springframework.cloud.contract.spec.internal.Input -import org.springframework.cloud.contract.spec.internal.OutputMessage -import org.springframework.cloud.contract.verifier.util.ContentUtils - -/** - * Creator of {@link MessagePact} instances - * - * @author Tim Ysewyn - * @since 2.0.0 - */ -@CompileStatic -@PackageScope -class MessagePactCreator { - - private static final Closure clientValueExtractor = { DslProperty property -> property.clientValue } - - MessagePact createFromContract(List contracts) { - if (contracts.empty) { - return null - } - Names names = NamingUtil.name(contracts.get(0)) - MessagePactBuilder pactBuilder = MessagePactBuilder.consumer(names.consumer) - .hasPactWith(names.producer) - contracts.each { Contract contract -> - pactBuilder = pactBuilder - .given(getGiven(contract.input)) - .expectsToReceive(getOutcome(contract)) - if (contract.outputMessage) { - OutputMessage message = contract.outputMessage - if (message.body) { - DslPart pactResponseBody = BodyConverter. - toPactBody(message.body, clientValueExtractor) - if (message.bodyMatchers) { - pactResponseBody.setMatchers(MatchingRulesConverter. - matchingRulesForBody(message.bodyMatchers)) - } - pactResponseBody.setGenerators(ValueGeneratorConverter. - extract(message, { DslProperty dslProperty -> dslProperty.serverValue })) - pactBuilder = pactBuilder.withContent(pactResponseBody) - } - if (message.headers) { - pactBuilder = pactBuilder.withMetadata(getMetadata(message.headers)) - } - } - } - return pactBuilder.toPact() - } - - private String getGiven(Input input) { - if (input.triggeredBy) { - return input.triggeredBy.executionCommand - } - else if (input.messageFrom) { - return "received message from " + clientValueExtractor.call(input.messageFrom) - } - else { - return "" - } - } - - private String getOutcome(Contract contract) { - if (contract.outputMessage) { - OutputMessage message = contract.outputMessage - return "message sent to " + clientValueExtractor.call(message.sentTo) - } - else { - return "assert that " + contract.input.assertThat.executionCommand - } - } - - private Map getMetadata(Headers headers) { - return headers.entries.collectEntries({ Header header -> - return [(header.name): extractValue(header)] - }) - } - - private String extractValue(Object value) { - Object v = value - if (v instanceof DslProperty) { - v = clientValueExtractor.call(v) - } - if (v instanceof GString) { - v = ContentUtils.extractValue(v, clientValueExtractor) - } - if (v instanceof String) { - return v - } - else { - return v.toString() - } - } -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.groovy deleted file mode 100644 index 188d328e03..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.groovy +++ /dev/null @@ -1,180 +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.spec.pact - -import au.com.dius.pact.core.model.matchingrules.Category -import au.com.dius.pact.core.model.matchingrules.DateMatcher -import au.com.dius.pact.core.model.matchingrules.MatchingRule -import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup -import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher -import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher -import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher -import au.com.dius.pact.core.model.matchingrules.NullMatcher -import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher -import au.com.dius.pact.core.model.matchingrules.RegexMatcher -import au.com.dius.pact.core.model.matchingrules.RuleLogic -import au.com.dius.pact.core.model.matchingrules.TimeMatcher -import au.com.dius.pact.core.model.matchingrules.TimestampMatcher -import au.com.dius.pact.core.model.matchingrules.TypeMatcher -import au.com.dius.pact.core.model.messaging.Message -import au.com.dius.pact.core.model.messaging.MessagePact -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.internal.RegexPatterns -import org.springframework.cloud.contract.verifier.util.JsonPaths -import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter - -/** - * Creator of {@link Contract} instances - * - * @author Tim Ysewyn - * @since 2.0.0 - */ -@CompileStatic -@PackageScope -class MessagingSCContractCreator { - - private static final String FULL_BODY = '$' - private static final String DESTINATION_KEY = "sentTo" - private static final List NON_HEADER_META_DATA = [DESTINATION_KEY] - - Collection convertFrom(MessagePact pact) { - return pact.messages.collect({ Message message -> - Contract.make { - label("$message.description") - if (!message.providerStates.isEmpty()) { - input { - triggeredBy(getTriggeredBy(message)) - } - } - outputMessage { - if (message.contents.present) { - body(BodyConverter.toSCCBody(message)) - Category bodyRules = message.matchingRules. - rulesForCategory('body') - if (bodyRules && !bodyRules.matchingRules.isEmpty()) { - bodyMatchers { - bodyRules.matchingRules. - each { String key, MatchingRuleGroup ruleGroup -> - if (ruleGroup.ruleLogic != RuleLogic.AND) { - throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported") - } - if (FULL_BODY == key) { - JsonPaths jsonPaths = JsonToJsonPathsConverter. - transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(message.contents.value) - jsonPaths.each { - jsonPath(it.keyBeforeChecking(), byType()) - } - } - else { - ruleGroup.rules.each { MatchingRule rule -> - if (rule instanceof NullMatcher) { - jsonPath(key, byNull()) - } - else if (rule instanceof RegexMatcher) { - jsonPath(key, byRegex(rule.regex)) - } - else if (rule instanceof DateMatcher) { - jsonPath(key, byDate()) - } - else if (rule instanceof TimeMatcher) { - jsonPath(key, byTime()) - } - else if (rule instanceof TimestampMatcher) { - jsonPath(key, byTimestamp()) - } - else if (rule instanceof MinTypeMatcher) { - jsonPath(key, byType() { - minOccurrence((rule as MinTypeMatcher).min) - }) - } - else if (rule instanceof MinMaxTypeMatcher) { - jsonPath(key, byType() { - minOccurrence((rule as MinMaxTypeMatcher).min) - maxOccurrence((rule as MinMaxTypeMatcher).max) - }) - } - else if (rule instanceof MaxTypeMatcher) { - jsonPath(key, byType() { - maxOccurrence((rule as MaxTypeMatcher).max) - }) - } - else if (rule instanceof TypeMatcher) { - jsonPath(key, byType()) - } - else if (rule instanceof NumberTypeMatcher) { - switch (rule.numberType) { - case NumberTypeMatcher.NumberType.NUMBER: - jsonPath(key, - byRegex(RegexPatterns.number())) - break - case NumberTypeMatcher.NumberType.INTEGER: - jsonPath(key, - byRegex(RegexPatterns.anInteger())) - break - case NumberTypeMatcher.NumberType.DECIMAL: - jsonPath(key, - byRegex(RegexPatterns.aDouble())) - break - default: - throw new RuntimeException("Unsupported number type!") - } - } - } - } - } - } - } - } - if (!message.metaData.isEmpty()) { - headers { - message.metaData.each { String k, Object val -> - String v = val.toString() - if (k.equalsIgnoreCase("contentType")) { - messagingContentType(v) - } - else if (!NON_HEADER_META_DATA.contains(k)) { - header(k, v) - } - } - } - } - String dest = findDestination(message) - if (dest) { - sentTo(dest) - } - } - } - }) - } - - private String getTriggeredBy(Message message) { - return message.providerStates.first().name - .replace(':', ' ') - .replace(' ', '_') - .replace('(', '') - .replace(')', '') - .uncapitalize() + "()" - } - - private String findDestination(Message message) { - return message.metaData.get(DESTINATION_KEY) ?: "" - } - -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.groovy deleted file mode 100644 index eb15af45c8..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.groovy +++ /dev/null @@ -1,62 +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.spec.pact - -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.Contract - -/** - * @author Marcin Grzejszczak - * @since - */ -@PackageScope -@CompileStatic -final class NamingUtil { - - // consumer___producer___testname - private static final String SEPARATOR = "___" - - protected static Names name(Contract contract) { - String contractName = contract.name - if (!contractName || !contractName.contains(SEPARATOR)) { - return new Names(["Consumer", "Provider", ""] as String[]) - } - return new Names(contractName.split(SEPARATOR)) - } -} - -@PackageScope -@CompileStatic -class Names { - final String consumer - final String producer - final String test - - Names(String[] strings) { - this.consumer = strings[0] - this.producer = strings[1] - this.test = strings.length >= 2 ? strings[2] : "" - } - - - @Override - String toString() { - return this.consumer + "_" + this.producer - } -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy deleted file mode 100644 index 2273db548b..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy +++ /dev/null @@ -1,101 +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.spec.pact - -import au.com.dius.pact.core.model.DefaultPactReader -import au.com.dius.pact.core.model.Pact -import au.com.dius.pact.core.model.PactSpecVersion -import au.com.dius.pact.core.model.RequestResponsePact -import au.com.dius.pact.core.model.messaging.MessagePact -import groovy.json.JsonOutput -import groovy.transform.CompileStatic - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.ContractConverter -/** - * Converter of JSON PACT file - * - * @author Marcin Grzejszczak - * @author Tim Ysewyn - * @since 1.1.0 - */ -@CompileStatic -class PactContractConverter implements ContractConverter> { - - private final RequestResponseSCContractCreator requestResponseSCContractCreator = new RequestResponseSCContractCreator() - private final MessagingSCContractCreator messagingSCContractCreator = new MessagingSCContractCreator() - private final RequestResponsePactCreator requestResponsePactCreator = new RequestResponsePactCreator() - private final MessagePactCreator messagePactCreator = new MessagePactCreator() - - @Override - boolean isAccepted(File file) { - try { - DefaultPactReader.INSTANCE.loadPact(file) - return true - } - catch (Exception e) { - return false - } - } - - @Override - Collection convertFrom(File file) { - Pact pact = DefaultPactReader.INSTANCE.loadPact(file) - if (pact instanceof RequestResponsePact) { - return requestResponseSCContractCreator. - convertFrom(pact as RequestResponsePact) - } - if (pact instanceof MessagePact) { - return messagingSCContractCreator.convertFrom(pact as MessagePact) - } - throw new UnsupportedOperationException("We currently don't support pact contracts of type" + pact.class.simpleName) - } - - @Override - Collection convertTo(Collection contracts) { - List pactContracts = new ArrayList<>() - Map> groupedContracts = contracts. - groupBy { NamingUtil.name(it).toString() } - for (List list : groupedContracts.values()) { - List httpOnly = list.findAll { it.request } - List messagingOnly = list.findAll { it.input } - RequestResponsePact responsePact = requestResponsePactCreator. - createFromContract(httpOnly) - if (responsePact) { - pactContracts.add(responsePact) - } - MessagePact messagePact = messagePactCreator.createFromContract(messagingOnly) - if (messagePact) { - pactContracts.add(messagePact) - } - } - return pactContracts - } - - @Override - Map store(Collection contracts) { - return contracts.collectEntries { - return [(name(it)): JsonOutput. - prettyPrint(JsonOutput.toJson(it.toMap(PactSpecVersion.V3))).bytes] - } - } - - protected String name(Pact contract) { - return contract.consumer.name + "_" + contract.provider.name + "_" + String. - valueOf(Math.abs(contract.hashCode())) + ".json" - } -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy deleted file mode 100644 index 36910e6d30..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.groovy +++ /dev/null @@ -1,295 +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.spec.pact - -import java.util.regex.Pattern - -import au.com.dius.pact.consumer.ConsumerPactBuilder -import au.com.dius.pact.consumer.dsl.DslPart -import au.com.dius.pact.consumer.dsl.PactDslRequestWithPath -import au.com.dius.pact.consumer.dsl.PactDslResponse -import au.com.dius.pact.consumer.dsl.PactDslWithProvider -import au.com.dius.pact.core.model.RequestResponsePact -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.internal.Body -import org.springframework.cloud.contract.spec.internal.Cookies -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.QueryParameters -import org.springframework.cloud.contract.spec.internal.RegexProperty -import org.springframework.cloud.contract.spec.internal.Request -import org.springframework.cloud.contract.spec.internal.Response - -/** - * Creator of {@link RequestResponsePact} instances - * - * @author Tim Ysewyn - * @since 2.0.0 - */ -@CompileStatic -@PackageScope -class RequestResponsePactCreator { - - RequestResponsePact createFromContract(List contracts) { - if (contracts.empty) { - return null - } - Names names = NamingUtil.name(contracts.get(0)) - PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder - .consumer(names.consumer).hasPactWith(names.producer) - PactDslResponse pactDslResponse = null - contracts.each { Contract contract -> - assertNoExecutionProperty(contract) - PactDslRequestWithPath pactDslRequest = pactDslResponse ? - createPactDslRequestWithPath(contract, pactDslResponse) : - createPactDslRequestWithPath(contract, pactDslWithProvider) - pactDslResponse = createPactDslResponse(contract, pactDslRequest) - } - return pactDslResponse.toPact() - } - - private void assertNoExecutionProperty(Contract contract) { - assertNoExecutionPropertyInBody(contract.request.body, - { DslProperty dslProperty -> dslProperty.serverValue }) - assertNoExecutionPropertyInBody(contract.response.body, - { DslProperty dslProperty -> dslProperty.clientValue }) - } - - private void assertNoExecutionPropertyInBody(Body body, Closure dslPropertyValueExtractor) { - traverseValues(body, dslPropertyValueExtractor, { - if (it instanceof ExecutionProperty) { - throw new UnsupportedOperationException("We can't convert a contract that has execution property") - } - }) - } - - private void traverseValues(def value, Closure dslPropertyValueExtractor, Closure closure) { - if (value instanceof DslProperty) { - traverseValues( - dslPropertyValueExtractor(value), dslPropertyValueExtractor, closure) - } - else if (value instanceof Map) { - value.values(). - forEach({ traverseValues(it, dslPropertyValueExtractor, closure) }) - } - else if (value instanceof Collection) { - value.forEach({ traverseValues(it, dslPropertyValueExtractor, closure) }) - } - else { - closure(value) - } - } - - private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslResponse pactDslResponse) { - Request request = contract.request - PactDslRequestWithPath pactDslRequest = pactDslResponse - .uponReceiving(contract.description ?: "") - .path(url(request)) - .method(request.method.serverValue.toString()) - String query = query(request) - if (query) { - pactDslRequest = pactDslRequest.encodedQuery(query) - } - if (request.headers) { - request.headers.entries.each { Header header -> - pactDslRequest = processHeader(pactDslRequest, header) - } - } - if (request.cookies) { - pactDslRequest = processCookies(pactDslRequest, request.cookies) - } - if (request.body) { - DslPart pactRequestBody = BodyConverter. - toPactBody(request.body, { DslProperty property -> property.serverValue }) - if (request.bodyMatchers) { - pactRequestBody.setMatchers(MatchingRulesConverter. - matchingRulesForBody(request.bodyMatchers)) - } - pactRequestBody.setGenerators(ValueGeneratorConverter. - extract(request.body, { DslProperty dslProperty -> dslProperty.clientValue })) - pactDslRequest = pactDslRequest.body(pactRequestBody) - } - return pactDslRequest - } - - private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslWithProvider pactDslWithProvider) { - Request request = contract.request - PactDslRequestWithPath pactDslRequest = pactDslWithProvider - .uponReceiving(contract.description ?: "") - .path(url(request)) - .method(request.method.serverValue.toString()) - String query = query(request) - if (query) { - pactDslRequest = pactDslRequest.encodedQuery(query) - } - if (request.headers) { - request.headers.entries.each { Header header -> - pactDslRequest = processHeader(pactDslRequest, header) - } - } - if (request.body) { - DslPart pactRequestBody = BodyConverter. - toPactBody(request.body, { DslProperty property -> property.serverValue }) - if (request.bodyMatchers) { - pactRequestBody.setMatchers(MatchingRulesConverter. - matchingRulesForBody(request.bodyMatchers)) - } - pactRequestBody.setGenerators(ValueGeneratorConverter. - extract(request.body, { DslProperty dslProperty -> dslProperty.clientValue })) - pactDslRequest = pactDslRequest.body(pactRequestBody) - } - return pactDslRequest - } - - private PactDslResponse createPactDslResponse(Contract contract, PactDslRequestWithPath pactDslRequest) { - Response response = contract.response - PactDslResponse pactDslResponse = pactDslRequest.willRespondWith() - .status(response.status.clientValue as Integer) - if (response.headers) { - response.headers.entries.each { Header header -> - pactDslResponse = processHeader(pactDslResponse, header) - } - } - if (response.cookies) { - pactDslResponse = processCookies(pactDslResponse, response.cookies) - } - if (response.body) { - DslPart pactResponseBody = BodyConverter. - toPactBody(response.body, { DslProperty property -> property.clientValue }) - if (response.bodyMatchers) { - pactResponseBody.setMatchers(MatchingRulesConverter. - matchingRulesForBody(response.bodyMatchers)) - } - pactResponseBody.setGenerators(ValueGeneratorConverter. - extract(response.body, { DslProperty dslProperty -> dslProperty.serverValue })) - pactDslResponse = pactDslResponse.body(pactResponseBody) - } - return pactDslResponse - } - - private String url(Request request) { - if (request.urlPath) { - return request.urlPath.serverValue.toString() - } - else if (request.url) { - return request.url.serverValue.toString() - } - throw new IllegalStateException("No url provided") - } - - private String query(Request request) { - String query = null - QueryParameters params = queryParams(request) - if (params) { - query = "" - params.parameters.eachWithIndex { param, index -> - query += param.name + '=' + param.serverValue - if (index + 1 < params.parameters.size()) { - query += '&' - } - } - } - return query - } - - private QueryParameters queryParams(Request request) { - if (request.urlPath) { - return request.urlPath.queryParameters - } - else if (request.url) { - return request.url.queryParameters - } - throw new IllegalStateException("No url provided") - - } - - private PactDslRequestWithPath processHeader(PactDslRequestWithPath pactDslRequest, Header header) { - if (header.isSingleValue()) { - String value = getDslPropertyServerValue(header).toString() - return pactDslRequest.headers(header.name, value) - } - else { - String regex = getDslPropertyClientValue(header).toString() - String example = getDslPropertyServerValue(header).toString() - return pactDslRequest.matchHeader(header.name, regex, example) - } - } - - private PactDslRequestWithPath processCookies(PactDslRequestWithPath pactDslRequest, Cookies cookies) { - Map stubSideCookies = cookies.asStubSideMap() - Collection regexProperties = stubSideCookies.values().findAll { it instanceof Pattern || it instanceof RegexProperty }.collect { new RegexProperty(it)} - if (!regexProperties.empty) { - String regex = regexProperties.collect { it.pattern() }.join("|") - return pactDslRequest.matchHeader("Cookie", regex, testSideCookieExample(cookies)) - } - else { - return pactDslRequest.headers("Cookie", testSideCookieExample(cookies)) - } - } - - private String testSideCookieExample(Cookies cookies) { - return cookies.asTestSideMap().collect { it.key + "=" + it.value.toString() }.join(";") - } - - private PactDslResponse processHeader(PactDslResponse pactDslResponse, Header header) { - if (header.isSingleValue()) { - String value = getDslPropertyClientValue(header).toString() - return pactDslResponse.headers([(header.name): value]) - } - else { - String regex = getDslPropertyServerValue(header).toString() - String example = getDslPropertyClientValue(header).toString() - return pactDslResponse.matchHeader(header.name, regex, example) - } - } - - private PactDslResponse processCookies(PactDslResponse pactDslResponse, Cookies cookies) { - Map testSideCookies = cookies.asTestSideMap() - Collection regexProperties = testSideCookies.values().findAll { it instanceof Pattern || it instanceof RegexProperty }.collect { new RegexProperty(it)} - if (!regexProperties.empty) { - String regex = regexProperties.collect { it.pattern() }.join("|") - return pactDslResponse.matchHeader("Cookie", regex, stubSideCookieExample(cookies)) - } - else { - return pactDslResponse.headers(["Cookie": stubSideCookieExample(cookies)]) - } - } - - private String stubSideCookieExample(Cookies cookies) { - return cookies.asStubSideMap().collect { it.key + "=" + it.value.toString() }.join(";") - } - - private Object getDslPropertyClientValue(Object o) { - Object value = o - if (value instanceof DslProperty) { - value = getDslPropertyClientValue(value.getClientValue()) - } - return value - } - - private Object getDslPropertyServerValue(Object o) { - Object value = o - if (value instanceof DslProperty) { - value = getDslPropertyServerValue(value.getServerValue()) - } - return value - } -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.groovy deleted file mode 100644 index d3d26c0bfb..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.groovy +++ /dev/null @@ -1,391 +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.spec.pact - -import java.util.regex.Pattern - -import au.com.dius.pact.core.model.OptionalBody -import au.com.dius.pact.core.model.ProviderState -import au.com.dius.pact.core.model.Request -import au.com.dius.pact.core.model.RequestResponseInteraction -import au.com.dius.pact.core.model.RequestResponsePact -import au.com.dius.pact.core.model.Response -import au.com.dius.pact.core.model.matchingrules.Category -import au.com.dius.pact.core.model.matchingrules.DateMatcher -import au.com.dius.pact.core.model.matchingrules.MatchingRule -import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup -import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher -import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher -import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher -import au.com.dius.pact.core.model.matchingrules.NullMatcher -import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher -import au.com.dius.pact.core.model.matchingrules.RegexMatcher -import au.com.dius.pact.core.model.matchingrules.RuleLogic -import au.com.dius.pact.core.model.matchingrules.TimeMatcher -import au.com.dius.pact.core.model.matchingrules.TimestampMatcher -import au.com.dius.pact.core.model.matchingrules.TypeMatcher -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.internal.RegexPatterns -import org.springframework.cloud.contract.verifier.util.JsonPaths -import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter -/** - * Creator of {@link Contract} instances - * - * @author Tim Ysewyn - * @since 2.0.0 - */ -@CompileStatic -@PackageScope -class RequestResponseSCContractCreator { - - private static final String FULL_BODY = '$' - - Collection convertFrom(RequestResponsePact pact) { - return pact.interactions.collect { RequestResponseInteraction interaction -> - Contract.make { - description(buildDescription(interaction)) - request { - Request request = interaction.request - method(request.method) - if (request.query) { - url(request.path) { - queryParameters { - request.query.each { String key, List value -> - value.each { String singleValue -> - parameter(key, singleValue) - } - } - } - } - } - else { - url(request.path) - } - if (request.headers) { - Category headerRules = request.matchingRules. - rulesForCategory('header') - headers { - request.headers.each { String k, List v -> - if (k.equalsIgnoreCase("Cookie")) { - return - } - if (headerRules.matchingRules.containsKey(k)) { - MatchingRuleGroup ruleGroup = headerRules.matchingRules. - get(k) - if (ruleGroup.rules.size() > 1) { - throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported") - } - MatchingRule rule = ruleGroup.rules[0] - if (rule instanceof RegexMatcher) { - v.each({ - header(k, $(c(regex(((RegexMatcher) rule).getRegex())), - p(it))) - }) - } - else { - throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported") - } - } - else { - v.each({ - header(k, it) - }) - } - } - } - } - if (request.headers.containsKey("Cookie")) { - Category headerRules = request.matchingRules. - rulesForCategory('header') - String[] splitHeader = request.headers.get("Cookie").first().split(";") - Map foundCookies = splitHeader.collectEntries { - String[] keyValue = it.split("=") - return [(keyValue[0]): keyValue[1]] - } - cookies { - foundCookies.each { k, v -> - if (headerRules.matchingRules.containsKey("Cookie")) { - MatchingRuleGroup ruleGroup = headerRules.matchingRules. - get("Cookie") - if (ruleGroup.rules.size() > 1) { - throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported") - } - MatchingRule rule = ruleGroup.rules[0] - if (rule instanceof RegexMatcher) { - v.each({ - cookie(k, $(c(regex(((RegexMatcher) rule).getRegex())), - p(it))) - }) - } - else { - throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported") - } - } - else { - cookie(k, v) - } - } - } - } - if (request.body.state == OptionalBody.State.PRESENT) { - def parsedBody = BodyConverter.toSCCBody(request) - if (parsedBody instanceof Map) { - body(parsedBody as Map) - } - else if (parsedBody instanceof List) { - body(parsedBody as List) - } - else { - body(parsedBody.toString()) - } - } - Category bodyRules = request.matchingRules.rulesForCategory('body') - if (bodyRules && !bodyRules.matchingRules.isEmpty()) { - bodyMatchers { - bodyRules.matchingRules. - each { String key, MatchingRuleGroup ruleGroup -> - if (ruleGroup.ruleLogic != RuleLogic.AND) { - throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported") - } - - ruleGroup.rules.each { MatchingRule rule -> - if (rule instanceof RegexMatcher) { - jsonPath(key, byRegex(rule.regex)) - } - else if (rule instanceof DateMatcher) { - jsonPath(key, byDate()) - } - else if (rule instanceof TimeMatcher) { - jsonPath(key, byTime()) - } - else if (rule instanceof TimestampMatcher) { - jsonPath(key, byTimestamp()) - } - else if (rule instanceof NumberTypeMatcher) { - switch (rule.numberType) { - case NumberTypeMatcher.NumberType.NUMBER: - jsonPath(key, - byRegex(RegexPatterns.number())) - break - case NumberTypeMatcher.NumberType.INTEGER: - jsonPath(key, byRegex(RegexPatterns. - anInteger())) - break - case NumberTypeMatcher.NumberType.DECIMAL: - jsonPath(key, - byRegex(RegexPatterns.aDouble())) - break - default: - throw new RuntimeException("Unsupported number type!") - } - } - } - } - } - } - } - response { - Response response = interaction.response - status(response.status) - if (response.body.present) { - def parsedBody = BodyConverter.toSCCBody(response) - if (parsedBody instanceof Map) { - body(parsedBody as Map) - } - else if (parsedBody instanceof List) { - body(parsedBody as List) - } - else { - body(parsedBody.toString()) - } - } - Category bodyRules = response.matchingRules.rulesForCategory('body') - if (bodyRules && !bodyRules.matchingRules.isEmpty()) { - bodyMatchers { - bodyRules.matchingRules. - each { String key, MatchingRuleGroup ruleGroup -> - if (ruleGroup.ruleLogic != RuleLogic.AND) { - throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported") - } - - if (FULL_BODY == key) { - JsonPaths jsonPaths = JsonToJsonPathsConverter. - transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(response.body.value instanceof byte[] ? new String(response.body.value) : response.body.value) - jsonPaths.each { - jsonPath(it.keyBeforeChecking(), byType()) - } - } - else { - ruleGroup.rules.each { MatchingRule rule -> - if (rule instanceof NullMatcher) { - jsonPath(key, byNull()) - } - else if (rule instanceof RegexMatcher) { - jsonPath(key, byRegex(rule.regex)) - } - else if (rule instanceof DateMatcher) { - jsonPath(key, byDate()) - } - else if (rule instanceof TimeMatcher) { - jsonPath(key, byTime()) - } - else if (rule instanceof TimestampMatcher) { - jsonPath(key, byTimestamp()) - } - else if (rule instanceof MinTypeMatcher) { - jsonPath(key, byType() { - minOccurrence((rule as MinTypeMatcher).min) - }) - } - else if (rule instanceof MinMaxTypeMatcher) { - jsonPath(key, byType() { - minOccurrence((rule as MinMaxTypeMatcher).min) - maxOccurrence((rule as MinMaxTypeMatcher).max) - }) - } - else if (rule instanceof MaxTypeMatcher) { - jsonPath(key, byType() { - maxOccurrence((rule as MaxTypeMatcher).max) - }) - } - else if (rule instanceof TypeMatcher) { - jsonPath(key, byType()) - } - else if (rule instanceof NumberTypeMatcher) { - switch (rule.numberType) { - case NumberTypeMatcher.NumberType.NUMBER: - jsonPath(key, - byRegex(RegexPatterns. - number())) - break - case NumberTypeMatcher.NumberType.INTEGER: - jsonPath(key, - byRegex(RegexPatterns. - anInteger())) - break - case NumberTypeMatcher.NumberType.DECIMAL: - jsonPath(key, - byRegex(RegexPatterns. - aDouble())) - break - default: - throw new UnsupportedOperationException("Unsupported number type!") - } - } - } - } - } - } - } - if (response.headers) { - Category headerRules = response.matchingRules. - rulesForCategory('header') - headers { - response.headers.forEach({ String k, List v -> - if (k.equalsIgnoreCase("Cookie")) { - return - } - if (headerRules.matchingRules.containsKey(k)) { - MatchingRuleGroup ruleGroup = headerRules.matchingRules. - get(k) - if (ruleGroup.rules.size() > 1) { - throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported") - } - MatchingRule rule = ruleGroup.rules[0] - if (rule instanceof RegexMatcher) { - v.each({ - header(k, $(p(regex(Pattern.compile( - ((RegexMatcher) rule).getRegex()))), - c(it))) - }) - - } - else { - throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported") - } - } - else { - v.each({ - header(k, it) - }) - } - }) - } - } - if (response.headers.containsKey("Cookie")) { - Category headerRules = response.matchingRules. - rulesForCategory('header') - String[] splitHeader = response.headers.get("Cookie").first().split(";") - Map foundCookies = splitHeader.collectEntries { - String[] keyValue = it.split("=") - return [(keyValue[0]): keyValue[1]] - } - cookies { - foundCookies.each { k, v -> - if (headerRules.matchingRules.containsKey("Cookie")) { - MatchingRuleGroup ruleGroup = headerRules.matchingRules. - get("Cookie") - if (ruleGroup.rules.size() > 1) { - throw new UnsupportedOperationException("Currently only 1 rule at a time for a header is supported") - } - MatchingRule rule = ruleGroup.rules[0] - if (rule instanceof RegexMatcher) { - v.each({ - cookie(k, $(p(regex(Pattern.compile( - ((RegexMatcher) rule).getRegex()))), - c(it))) - }) - - } - else { - throw new UnsupportedOperationException("Currently only the header matcher of type regex is supported") - } - } - else { - cookie(k, v) - } - } - } - } - } - } - } - } - - private String buildDescription(RequestResponseInteraction interaction) { - String description = "$interaction.description" - interaction.providerStates.forEach({ ProviderState it -> - description += " $it.name" - if (!it.params.isEmpty()) { - Map params = it.params - description += "(" - params.forEach({ String k, Object v -> - description += k + ": " + v.toString() - if (params.keySet().last() != k) { - description += ", " - } - }) - description += ")" - } - }) - return description - } - -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.groovy deleted file mode 100644 index 022b04bc95..0000000000 --- a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.groovy +++ /dev/null @@ -1,191 +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.spec.pact - -import java.util.regex.Pattern - -import au.com.dius.pact.core.model.generators.Category -import au.com.dius.pact.core.model.generators.DateGenerator -import au.com.dius.pact.core.model.generators.DateTimeGenerator -import au.com.dius.pact.core.model.generators.Generator -import au.com.dius.pact.core.model.generators.Generators -import au.com.dius.pact.core.model.generators.RandomBooleanGenerator -import au.com.dius.pact.core.model.generators.RandomDecimalGenerator -import au.com.dius.pact.core.model.generators.RandomHexadecimalGenerator -import au.com.dius.pact.core.model.generators.RandomIntGenerator -import au.com.dius.pact.core.model.generators.RandomStringGenerator -import au.com.dius.pact.core.model.generators.RegexGenerator -import au.com.dius.pact.core.model.generators.TimeGenerator -import au.com.dius.pact.core.model.generators.UuidGenerator -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.internal.Body -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.internal.OutputMessage -import org.springframework.cloud.contract.spec.internal.RegexProperty -import org.springframework.cloud.contract.verifier.util.ContentUtils - -/** - * @author Tim Ysewyn - * @Since 2.0.0 - */ -@CompileStatic -@PackageScope -class ValueGeneratorConverter { - - private static final Pattern INTEGER = Pattern.compile(INTEGER_PATTERN) - private static final String INTEGER_PATTERN = '-?(\\d+)' - private static final Pattern DECIMAL = Pattern.compile(DECIMAL_PATTERN) - private static final String DECIMAL_PATTERN = '-?(\\d*\\.\\d+)' - private static final Pattern HEX = Pattern.compile(HEX_PATTERN) - private static final String HEX_PATTERN = '[a-fA-F0-9]+' - private static final Pattern ALPHA_NUMERIC = Pattern.compile(ALPHA_NUMERIC_PATTERN) - private static final String ALPHA_NUMERIC_PATTERN = '[a-zA-Z0-9]+' - private static final Pattern UUID = Pattern.compile(UUID_PATTERN) - private static final String UUID_PATTERN = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' - private static final Pattern ANY_DATE = Pattern.compile(ANY_DATE_PATTERN) - private static final String ANY_DATE_PATTERN = '(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])' - private static final Pattern ANY_TIME = Pattern.compile(ANY_TIME_PATTERN) - private static final String ANY_TIME_PATTERN = '(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])' - private static final Pattern ANY_DATE_TIME = Pattern.compile(ANY_DATE_TIME_PATTERN) - private static final String ANY_DATE_TIME_PATTERN = '([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])' - private static final Pattern TRUE_OR_FALSE = Pattern.compile(TRUE_OR_FALSE_PATTERN) - private static final String TRUE_OR_FALSE_PATTERN = /(true|false)/ - - static DslProperty convert(Generator generator, Closure dslPropertyProvider) { - Pattern pattern - if (generator instanceof RandomIntGenerator) { - pattern = INTEGER - } - else if (generator instanceof RandomDecimalGenerator) { - pattern = DECIMAL - } - else if (generator instanceof RandomHexadecimalGenerator) { - pattern = HEX - } - else if (generator instanceof RandomStringGenerator) { - pattern = ALPHA_NUMERIC - } - else if (generator instanceof RegexGenerator) { - pattern = Pattern.compile(generator.regex) - } - else if (generator instanceof UuidGenerator) { - pattern = UUID - } - else if (generator instanceof DateGenerator) { - pattern = getDateTimePattern(generator.format, ANY_DATE) - } - else if (generator instanceof TimeGenerator) { - pattern = getDateTimePattern(generator.format, ANY_TIME) - } - else if (generator instanceof DateTimeGenerator) { - pattern = getDateTimePattern(generator.format, ANY_DATE_TIME) - } - else if (generator instanceof RandomBooleanGenerator) { - pattern = TRUE_OR_FALSE - } - if (pattern == null) { - throw new UnsupportedOperationException("We currently don't support a generator of type " + generator.class.simpleName) - } - else { - Object generatedValue = generator.generate([:]) - return dslPropertyProvider(pattern, generatedValue) - } - } - - private static Pattern getDateTimePattern(String format, Pattern defaultPattern) { - return format ? Pattern.compile(format) : defaultPattern - } - - static Generators extract(Body body, Closure dslPropertyValueProvider) { - Generators generators = new Generators() - traverse(body, dslPropertyValueProvider, '', generators, Category.BODY) - return generators - } - - static Generators extract(OutputMessage message, Closure dslPropertyValueProvider) { - Generators generators = new Generators() - traverse(message.body, dslPropertyValueProvider, '', generators, Category.BODY) - return generators - } - - private static void traverse(Object value, Closure dslPropertyValueProvider, String path, Generators generators, Category category) { - Object v = value - if (v instanceof DslProperty) { - v = dslPropertyValueProvider(v) - } - if (v instanceof GString) { - v = ContentUtils.extractValue(v, dslPropertyValueProvider) - } - if (v instanceof Map) { - v.each { Map.Entry entry -> - traverse(entry.value, dslPropertyValueProvider, path + "." + entry.key, generators, category) - } - } - else if (v instanceof Collection) { - v.eachWithIndex { def entry, int index -> - traverse(entry, dslPropertyValueProvider, path + "[" + index + "]", generators, category) - } - } - else if (v instanceof DslProperty) { - traverse(v, dslPropertyValueProvider, path, generators, category) - } - else if (v instanceof RegexProperty || v instanceof Pattern) { - RegexProperty regexProperty = new RegexProperty(v) - switch (regexProperty.pattern()) { - case INTEGER_PATTERN: - generators. - addGenerator(category, path, new RandomIntGenerator(0, Integer.MAX_VALUE)) - break - case DECIMAL_PATTERN: - generators. - addGenerator(category, path, new RandomDecimalGenerator(10)) - break - case HEX_PATTERN: - generators. - addGenerator(category, path, new RandomHexadecimalGenerator(10)) - break - case ALPHA_NUMERIC_PATTERN: - generators.addGenerator(category, path, new RandomStringGenerator(10)) - break - case UUID_PATTERN: - generators.addGenerator(category, path, UuidGenerator.INSTANCE) - break - case ANY_DATE_PATTERN: - generators.addGenerator(category, path, new DateGenerator()) - break - case ANY_TIME_PATTERN: - generators.addGenerator(category, path, new TimeGenerator()) - break - case ANY_DATE_TIME_PATTERN: - generators.addGenerator(category, path, new DateTimeGenerator()) - break - case TRUE_OR_FALSE_PATTERN: - generators. - addGenerator(category, path, RandomBooleanGenerator.INSTANCE) - break - default: - generators. - addGenerator(category, path, new RegexGenerator(regexProperty. - pattern())) - break - } - } - } - -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/stubrunner/PactStubDownloaderBuilder.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/stubrunner/PactStubDownloaderBuilder.java similarity index 100% rename from spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/groovy/org/springframework/cloud/contract/stubrunner/PactStubDownloaderBuilder.java rename to spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/stubrunner/PactStubDownloaderBuilder.java diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.java new file mode 100644 index 0000000000..0f4e419f7f --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/BodyConverter.java @@ -0,0 +1,231 @@ +/* + * 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.spec.pact; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.regex.Pattern; + +import au.com.dius.pact.consumer.dsl.DslPart; +import au.com.dius.pact.consumer.dsl.PactDslJsonArray; +import au.com.dius.pact.consumer.dsl.PactDslJsonBody; +import au.com.dius.pact.core.model.OptionalBody; +import au.com.dius.pact.core.model.Request; +import au.com.dius.pact.core.model.Response; +import au.com.dius.pact.core.model.generators.Generator; +import au.com.dius.pact.core.model.messaging.Message; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.Path; +import com.jayway.jsonpath.internal.path.PathCompiler; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import groovy.lang.GString; +import org.apache.commons.lang3.StringUtils; + +import org.springframework.cloud.contract.spec.internal.ClientDslProperty; +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.ServerDslProperty; +import org.springframework.cloud.contract.verifier.util.ContentUtils; + +/** + * @author Tim Ysewyn + * @author Stessy Delcroix + * @since 2.0.0 + */ +final class BodyConverter { + + private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.INSTANCE.getMapper(); + + private BodyConverter() { + + } + + static DslPart toPactBody(DslProperty dslProperty, Function, Object> dslPropertyValueExtractor) { + return traverse(dslProperty, null, dslPropertyValueExtractor); + } + + private static DslPart traverse(Object value, DslPart parent, + Function, Object> dslPropertyValueExtractor) { + boolean isRoot = parent == null; + Object v = value; + if (v instanceof DslProperty) { + v = dslPropertyValueExtractor.apply((DslProperty) v); + } + if (v instanceof GString) { + v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor); + } + if (v instanceof String) { + String stringValue = ((String) v).trim(); + if (StringUtils.startsWith(stringValue, "{") && StringUtils.endsWith(stringValue, "}")) { + try { + v = OBJECT_MAPPER.readValue(stringValue, Object.class); + } + catch (JsonProcessingException ex) { /* + * it wasn't a JSON string after + * all... + */ + } + } + } + DslPart p = isRoot ? createRootDslPart(v) : parent; + if (v instanceof Map) { + processMap((Map) v, (PactDslJsonBody) p, dslPropertyValueExtractor); + } + else if (v instanceof Collection) { + processCollection((Collection) v, (PactDslJsonArray) p, dslPropertyValueExtractor); + } + return p; + } + + private static DslPart createRootDslPart(Object value) { + return value instanceof Collection ? new PactDslJsonArray() : new PactDslJsonBody(); + } + + private static void processCollection(Collection values, PactDslJsonArray jsonArray, + Function, Object> dslPropertyValueExtractor) { + values.forEach(v -> { + + if (v instanceof DslProperty) { + v = dslPropertyValueExtractor.apply((DslProperty) v); + } + if (v instanceof GString) { + v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor); + } + if (v == null) { + jsonArray.nullValue(); + } + else if (v instanceof String) { + jsonArray.string((String) v); + } + else if (v instanceof Number) { + jsonArray.number((Number) v); + } + else if (v instanceof Map) { + PactDslJsonBody current = jsonArray.object(); + traverse(v, current, dslPropertyValueExtractor); + current.closeObject(); + } + else if (v instanceof Collection) { + PactDslJsonArray current = jsonArray.array(); + traverse(v, current, dslPropertyValueExtractor); + current.closeArray(); + } + }); + } + + private static void processMap(Map values, PactDslJsonBody jsonObject, + Function, Object> dslPropertyValueExtractor) { + values.forEach((k, v) -> { + if (v instanceof DslProperty) { + v = dslPropertyValueExtractor.apply((DslProperty) v); + } + if (v instanceof GString) { + v = ContentUtils.extractValue((GString) v, dslPropertyValueExtractor); + } + if (v == null) { + jsonObject.nullValue(k); + } + else if (v instanceof String) { + jsonObject.stringType(k, (String) v); + } + else if (v instanceof Number) { + jsonObject.numberValue(k, (Number) v); + } + else if (v instanceof Map) { + PactDslJsonBody current = jsonObject.object(k); + traverse(v, current, dslPropertyValueExtractor); + current.closeObject(); + } + else if (v instanceof Collection) { + PactDslJsonArray current = jsonObject.array(k); + traverse(v, current, dslPropertyValueExtractor); + current.closeArray(); + } + }); + } + + static Object toSCCBody(Request request) { + Object body = parseBody(request.getBody()); + if (request.getGenerators().isNotEmpty() && request.getGenerators().getCategories() + .containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) { + applyGenerators(body, + request.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY), + currentValue -> pattern -> generatedValue -> new DslProperty<>( + new ClientDslProperty(pattern, generatedValue), currentValue)); + } + return body; + } + + static Object toSCCBody(Response response) { + Object body = parseBody(response.getBody()); + if (response.getGenerators().isNotEmpty() && response.getGenerators().getCategories() + .containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) { + applyGenerators(body, + response.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY), + currentValue -> pattern -> generatedValue -> new DslProperty<>(currentValue, + new ServerDslProperty(pattern, generatedValue))); + } + return body; + } + + static Object toSCCBody(Message message) { + Object body = parseBody(message.getContents()); + if (message.getGenerators().isNotEmpty() && message.getGenerators().getCategories() + .containsKey(au.com.dius.pact.core.model.generators.Category.BODY)) { + applyGenerators(body, + message.getGenerators().getCategories().get(au.com.dius.pact.core.model.generators.Category.BODY), + currentValue -> pattern -> generatedValue -> new DslProperty<>( + new ClientDslProperty(pattern, generatedValue), currentValue)); + } + return body; + } + + private static Object parseBody(OptionalBody optionalBody) { + if (optionalBody.isPresent()) { + try { + return OBJECT_MAPPER.readValue(optionalBody.getValue(), Object.class); + } + catch (IOException e) { + throw new RuntimeException("Body could not be read", e); + } + } + else { + return optionalBody.getValue(); + } + } + + private static void applyGenerators(Object body, Map generatorsPerPath, + Function>>> dslPropertyProvider) { + Configuration configuration = Configuration.builder().jsonProvider(new JacksonJsonProvider(OBJECT_MAPPER)) + .build(); + generatorsPerPath.forEach((path, generator) -> { + Path compiledPath = PathCompiler.compile(path); + EvaluationContext evaluationContext = compiledPath.evaluate(body, body, configuration, true); + evaluationContext.updateOperations().forEach(pathRef -> { + pathRef.convert(((currentValue, config) -> ValueGeneratorConverter.convert(generator, + (pattern, generatedValue) -> dslPropertyProvider.apply(pattern).apply(generatedValue) + .apply(currentValue))), + configuration); + }); + }); + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.java new file mode 100644 index 0000000000..21d7b4d8c8 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MatchingRulesConverter.java @@ -0,0 +1,111 @@ +/* + * 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.spec.pact; + +import au.com.dius.pact.core.model.matchingrules.Category; +import au.com.dius.pact.core.model.matchingrules.DateMatcher; +import au.com.dius.pact.core.model.matchingrules.EqualsMatcher; +import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.NullMatcher; +import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.RegexMatcher; +import au.com.dius.pact.core.model.matchingrules.TimeMatcher; +import au.com.dius.pact.core.model.matchingrules.TimestampMatcher; +import au.com.dius.pact.core.model.matchingrules.TypeMatcher; + +import org.springframework.cloud.contract.spec.internal.BodyMatchers; +import org.springframework.cloud.contract.spec.internal.MatchingType; +import org.springframework.cloud.contract.spec.internal.RegexPatterns; + +/** + * @author Tim Ysewyn + * @author Stessy Delcroix + * @since 2.0.0 + */ +final class MatchingRulesConverter { + + private MatchingRulesConverter() { + } + + static Category matchingRulesForBody(BodyMatchers bodyMatchers) { + return matchingRulesFor("body", bodyMatchers); + } + + private static Category matchingRulesFor(String categoryName, BodyMatchers bodyMatchers) { + Category category = new Category(categoryName); + bodyMatchers.matchers().forEach((b) -> { + String key = getMatcherKey(b.path()); + MatchingType matchingType = b.matchingType(); + switch (matchingType) { + case NULL: + category.addRule(key, NullMatcher.INSTANCE); + break; + case EQUALITY: + category.addRule(key, EqualsMatcher.INSTANCE); + break; + case TYPE: + if (b.minTypeOccurrence() != null && b.maxTypeOccurrence() != null) { + category.addRule(key, new MinMaxTypeMatcher(b.minTypeOccurrence(), b.maxTypeOccurrence())); + } + else if (b.minTypeOccurrence() != null) { + category.addRule(key, new MinTypeMatcher(b.minTypeOccurrence())); + } + else if (b.maxTypeOccurrence() != null) { + category.addRule(key, new MaxTypeMatcher(b.maxTypeOccurrence())); + } + else { + category.addRule(key, TypeMatcher.INSTANCE); + } + break; + case DATE: + category.addRule(key, new DateMatcher()); + break; + case TIME: + category.addRule(key, new TimeMatcher()); + break; + case TIMESTAMP: + category.addRule(key, new TimestampMatcher()); + break; + case REGEX: + String pattern = b.value().toString(); + if (pattern.equals(RegexPatterns.number().pattern())) { + category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER)); + } + else if (pattern.equals(RegexPatterns.anInteger().pattern())) { + category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER)); + } + else if (pattern.equals(RegexPatterns.aDouble().pattern())) { + category.addRule(key, new NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL)); + } + else { + category.addRule(key, new RegexMatcher(pattern)); + } + break; + default: + break; + } + }); + return category; + } + + private static String getMatcherKey(String path) { + return path.startsWith("$") ? path.substring(1) : path; + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.java new file mode 100644 index 0000000000..10e3aae3a5 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MessagePactCreator.java @@ -0,0 +1,121 @@ +/* + * 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.spec.pact; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import au.com.dius.pact.consumer.MessagePactBuilder; +import au.com.dius.pact.consumer.dsl.DslPart; +import au.com.dius.pact.core.model.messaging.MessagePact; +import groovy.lang.GString; +import org.apache.commons.collections.CollectionUtils; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.Header; +import org.springframework.cloud.contract.spec.internal.Headers; +import org.springframework.cloud.contract.spec.internal.Input; +import org.springframework.cloud.contract.spec.internal.OutputMessage; +import org.springframework.cloud.contract.verifier.util.ContentUtils; + +/** + * Creator of {@link MessagePact} instances. + * + * @author Tim Ysewyn + * @author Stessy Delcroix + * @since 2.0.0 + */ +class MessagePactCreator { + + private static final Function, Object> clientValueExtractor = DslProperty::getClientValue; + + MessagePact createFromContract(List contracts) { + if (CollectionUtils.isEmpty(contracts)) { + return null; + } + Names names = NamingUtil.name(contracts.get(0)); + MessagePactBuilder pactBuilder = MessagePactBuilder.consumer(names.getConsumer()) + .hasPactWith(names.getProducer()); + + for (Contract contract : contracts) { + pactBuilder = pactBuilder.given(getGiven(contract.getInput())).expectsToReceive(getOutcome(contract)); + if (contract.getOutputMessage() != null) { + OutputMessage message = contract.getOutputMessage(); + if (message.getBody() != null) { + DslPart pactResponseBody = BodyConverter.toPactBody(message.getBody(), clientValueExtractor); + if (message.getBodyMatchers() != null) { + pactResponseBody + .setMatchers(MatchingRulesConverter.matchingRulesForBody(message.getBodyMatchers())); + } + pactResponseBody + .setGenerators(ValueGeneratorConverter.extract(message, DslProperty::getServerValue)); + pactBuilder = pactBuilder.withContent(pactResponseBody); + } + if (message.getHeaders() != null) { + pactBuilder = pactBuilder.withMetadata(getMetadata(message.getHeaders())); + } + } + } + return pactBuilder.toPact(); + } + + private String getGiven(Input input) { + if (input.getTriggeredBy() != null) { + return input.getTriggeredBy().getExecutionCommand(); + } + else if (input.getMessageFrom() != null) { + return "received message from " + clientValueExtractor.apply(input.getMessageFrom()); + } + else { + return ""; + } + } + + private String getOutcome(Contract contract) { + if (contract.getOutputMessage() != null) { + OutputMessage message = contract.getOutputMessage(); + return "message sent to " + clientValueExtractor.apply(message.getSentTo()); + } + else { + return "assert that " + contract.getInput().getAssertThat().getExecutionCommand(); + } + } + + private Map getMetadata(Headers headers) { + return headers.getEntries().stream().collect(Collectors.toMap(Header::getName, this::extractValue)); + } + + private String extractValue(Object value) { + Object v = value; + if (v instanceof DslProperty) { + v = clientValueExtractor.apply((DslProperty) v); + } + if (v instanceof GString) { + v = ContentUtils.extractValue((GString) v, clientValueExtractor); + } + if (v instanceof String) { + return (String) v; + } + else { + return v.toString(); + } + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.java new file mode 100644 index 0000000000..d0c7e3d3fb --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/MessagingSCContractCreator.java @@ -0,0 +1,188 @@ +/* + * 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.spec.pact; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import au.com.dius.pact.core.model.matchingrules.Category; +import au.com.dius.pact.core.model.matchingrules.DateMatcher; +import au.com.dius.pact.core.model.matchingrules.MatchingRule; +import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.NullMatcher; +import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.RegexMatcher; +import au.com.dius.pact.core.model.matchingrules.RuleLogic; +import au.com.dius.pact.core.model.matchingrules.TimeMatcher; +import au.com.dius.pact.core.model.matchingrules.TimestampMatcher; +import au.com.dius.pact.core.model.matchingrules.TypeMatcher; +import au.com.dius.pact.core.model.messaging.Message; +import au.com.dius.pact.core.model.messaging.MessagePact; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.Headers; +import org.springframework.cloud.contract.spec.internal.RegexPatterns; +import org.springframework.cloud.contract.spec.internal.ResponseBodyMatchers; +import org.springframework.cloud.contract.verifier.util.JsonPaths; +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; + +/** + * Creator of {@link Contract} instances. + * + * @author Tim Ysewyn + * @author Stessy Delcroix + * @since 2.0.0 + */ +class MessagingSCContractCreator { + + private static final String FULL_BODY = "$"; + + private static final String DESTINATION_KEY = "sentTo"; + + private static final List NON_HEADER_META_DATA = Collections.singletonList(DESTINATION_KEY); + + Collection convertFrom(MessagePact pact) { + return pact.getMessages().stream().map(message -> Contract.make(contract -> { + contract.label(message.getDescription()); + if (CollectionUtils.isNotEmpty(message.getProviderStates())) { + contract.input(i -> i.triggeredBy(this.getTriggeredBy(message))); + } + + contract.outputMessage((outputMessage) -> { + if (message.getContents().isPresent()) { + outputMessage.body(BodyConverter.toSCCBody(message)); + Category bodyRules = message.getMatchingRules().rulesForCategory("body"); + if (bodyRules != null && MapUtils.isNotEmpty(bodyRules.getMatchingRules())) { + outputMessage.bodyMatchers((responseBodyMatchers) -> outputMessageBodyMatchers(message, + bodyRules, responseBodyMatchers)); + } + } + if (MapUtils.isNotEmpty(message.getMetaData())) { + outputMessage.headers((headers) -> outputMessageHeaders(message, headers)); + } + String dest = findDestination(message); + if (StringUtils.isNotBlank(dest)) { + outputMessage.sentTo(dest); + } + }); + })).collect(Collectors.toList()); + } + + private void outputMessageHeaders(Message message, Headers headers) { + message.getMetaData().forEach((key, value) -> { + String matchingRuleGroup = value.toString(); + if (key.equalsIgnoreCase("contentType")) { + headers.messagingContentType(matchingRuleGroup); + } + else if (!NON_HEADER_META_DATA.contains(key)) { + headers.header(key, matchingRuleGroup); + } + }); + } + + private void outputMessageBodyMatchers(Message message, Category bodyRules, + ResponseBodyMatchers responseBodyMatchers) { + bodyRules.getMatchingRules().forEach((matchingRuleKey, matchingRuleGroup) -> { + if (matchingRuleGroup.getRuleLogic() != RuleLogic.AND) { + throw new UnsupportedOperationException("Currently only the AND combination rule logic is supported"); + } + if (FULL_BODY.equals(matchingRuleKey)) { + JsonPaths jsonPaths = JsonToJsonPathsConverter + .transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(message.getContents().getValue()); + jsonPaths.forEach((j) -> { + responseBodyMatchers.jsonPath(j.keyBeforeChecking(), responseBodyMatchers.byType()); + }); + } + else { + matchingRuleGroup.getRules().forEach((rule) -> { + applyJsonPathToResponseBodyMatchers(rule, matchingRuleKey, responseBodyMatchers); + }); + } + }); + } + + private String getTriggeredBy(Message message) { + String triggeredBy = message.getProviderStates().get(0).getName().replaceAll(":", " ").replaceAll(" ", "_") + .replaceAll("\\(", "").replaceAll("\\)", ""); + return StringUtils.uncapitalize(triggeredBy) + "()"; + } + + private String findDestination(Message message) { + return message.getMetaData().get(DESTINATION_KEY) != null + ? message.getMetaData().get(DESTINATION_KEY).toString() : ""; + } + + void applyJsonPathToResponseBodyMatchers(MatchingRule matchingRule, String key, + ResponseBodyMatchers responseBodyMatchers) { + if (matchingRule instanceof NullMatcher) { + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byNull()); + } + else if (matchingRule instanceof RegexMatcher) { + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex())); + } + else if (matchingRule instanceof DateMatcher) { + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byDate()); + } + else if (matchingRule instanceof TimeMatcher) { + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byTime()); + } + else if (matchingRule instanceof TimestampMatcher) { + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byTimestamp()); + } + else if (matchingRule instanceof MinTypeMatcher) { + responseBodyMatchers.jsonPath(key, + responseBodyMatchers.byType((b) -> b.minOccurrence(((MinTypeMatcher) matchingRule).getMin()))); + } + else if (matchingRule instanceof MinMaxTypeMatcher) { + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType((c) -> { + c.minOccurrence(((MinMaxTypeMatcher) matchingRule).getMin()); + c.maxOccurrence(((MinMaxTypeMatcher) matchingRule).getMax()); + })); + } + else if (matchingRule instanceof MaxTypeMatcher) { + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType((c) -> { + c.maxOccurrence(((MaxTypeMatcher) matchingRule).getMax()); + })); + } + else if (matchingRule instanceof TypeMatcher) { + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byType()); + } + else if (matchingRule instanceof NumberTypeMatcher) { + switch (((NumberTypeMatcher) matchingRule).getNumberType()) { + case NUMBER: + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.number())); + break; + case INTEGER: + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.anInteger())); + break; + case DECIMAL: + responseBodyMatchers.jsonPath(key, responseBodyMatchers.byRegex(RegexPatterns.aDouble())); + break; + default: + throw new RuntimeException("Unsupported number type!"); + } + } + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/Names.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/Names.java new file mode 100644 index 0000000000..c8d67a58e8 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/Names.java @@ -0,0 +1,51 @@ +/* + * 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.spec.pact; + +/** + * @author Marcin Grzejszczak + * @author Stessy Delcroix + * @since + */ +class Names { + + private final String consumer; + + private final String producer; + + private final String test; + + Names(String[] strings) { + this.consumer = strings[0]; + this.producer = strings[1]; + this.test = strings.length >= 2 ? strings[2] : ""; + } + + @Override + public String toString() { + return this.consumer + "_" + this.producer; + } + + String getConsumer() { + return consumer; + } + + String getProducer() { + return producer; + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.java new file mode 100644 index 0000000000..ec61951e65 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/NamingUtil.java @@ -0,0 +1,42 @@ +/* + * 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.spec.pact; + +import org.springframework.cloud.contract.spec.Contract; + +/** + * @author Marcin Grzejszczak + * @author Stessy Delcroix + * @since + */ +final class NamingUtil { + + // consumer___producer___testname + private static final String SEPARATOR = "___"; + + private NamingUtil() { + } + + static Names name(Contract contract) { + String contractName = contract.getName(); + if (contractName == null || !contractName.contains(SEPARATOR)) { + return new Names(new String[] { "Consumer", "Provider", "" }); + } + return new Names(contractName.split(SEPARATOR)); + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/ObjectMapperFactory.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/ObjectMapperFactory.java new file mode 100644 index 0000000000..53a472376e --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/ObjectMapperFactory.java @@ -0,0 +1,49 @@ +/* + * 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.spec.pact; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link ObjectMapper} singleton instance creation. + * @author Stessy Delcroix + * @since 2.0.0 + */ +public enum ObjectMapperFactory { + + /** + * Singleton instance constant. + */ + INSTANCE; + + 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); + OBJECT_MAPPER.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true); + } + + public ObjectMapper getMapper() { + return OBJECT_MAPPER; + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.java new file mode 100644 index 0000000000..45d0585441 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.java @@ -0,0 +1,152 @@ +/* + * 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.spec.pact; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import au.com.dius.pact.core.model.DefaultPactReader; +import au.com.dius.pact.core.model.Pact; +import au.com.dius.pact.core.model.PactSpecVersion; +import au.com.dius.pact.core.model.RequestResponsePact; +import au.com.dius.pact.core.model.messaging.MessagePact; +import com.fasterxml.jackson.core.JsonProcessingException; +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.ObjectMapper; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.ContractConverter; + +/** + * Converter of JSON PACT file. + * + * @author Marcin Grzejszczak + * @author Tim Ysewyn + * @author Stessy Delcroix + * @since 1.1.0 + */ +public class PactContractConverter implements ContractConverter>> { + + private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.INSTANCE.getMapper(); + + private final RequestResponseSCContractCreator requestResponseSCContractCreator = new RequestResponseSCContractCreator(); + + private final MessagingSCContractCreator messagingSCContractCreator = new MessagingSCContractCreator(); + + private final RequestResponsePactCreator requestResponsePactCreator = new RequestResponsePactCreator(); + + private final MessagePactCreator messagePactCreator = new MessagePactCreator(); + + @Override + public boolean isAccepted(File file) { + try { + DefaultPactReader.INSTANCE.loadPact(file); + return true; + } + catch (Exception e) { + return false; + } + } + + @Override + public Collection convertFrom(File file) { + Pact pact = DefaultPactReader.INSTANCE.loadPact(file); + if (pact instanceof RequestResponsePact) { + return requestResponseSCContractCreator.convertFrom((RequestResponsePact) pact); + } + if (pact instanceof MessagePact) { + return messagingSCContractCreator.convertFrom((MessagePact) pact); + } + throw new UnsupportedOperationException( + "We currently don't support pact contracts of type" + pact.getClass().getSimpleName()); + } + + @Override + public Collection> convertTo(Collection contracts) { + List> pactContracts = new ArrayList<>(); + Map> groupedContracts = contracts.stream() + .collect(Collectors.groupingBy(c -> NamingUtil.name(c).toString())); + for (List list : groupedContracts.values()) { + List httpOnly = list.stream().filter(c -> c.getRequest() != null).collect(Collectors.toList()); + List messagingOnly = list.stream().filter(c -> c.getInput() != null).collect(Collectors.toList()); + RequestResponsePact responsePact = requestResponsePactCreator.createFromContract(httpOnly); + if (responsePact != null) { + pactContracts.add(responsePact); + } + MessagePact messagePact = messagePactCreator.createFromContract(messagingOnly); + if (messagePact != null) { + pactContracts.add(messagePact); + } + } + return pactContracts; + } + + @Override + public Map store(Collection> contracts) { + return contracts.stream().collect(Collectors.toMap(this::name, c -> { + try { + return this.buildPrettyPrint(OBJECT_MAPPER.writeValueAsString(c.toMap(PactSpecVersion.V3))).getBytes(); + } + catch (JsonProcessingException e) { + throw new IllegalArgumentException("The pact contract is not a valid map", e); + } + })); + } + + protected String name(Pact contract) { + return contract.getConsumer().getName() + "_" + contract.getProvider().getName() + "_" + + Math.abs(contract.hashCode()) + ".json"; + } + + private String buildPrettyPrint(String contract) { + try { + Object intermediateObjectForPrettyPrinting = OBJECT_MAPPER.reader().readValue(contract, Object.class); + DefaultIndenter customIndenter = new DefaultIndenter(" ", "\n"); + return OBJECT_MAPPER + .writer(new CustomPrettyPrinter().withArrayIndenter(customIndenter) + .withObjectIndenter(customIndenter)) + .writeValueAsString(intermediateObjectForPrettyPrinting); + } + catch (IOException e) { + throw new RuntimeException("WireMock response body could not be pretty printed"); + } + } + + private static class CustomPrettyPrinter extends DefaultPrettyPrinter { + + @Override + public CustomPrettyPrinter createInstance() { + return new CustomPrettyPrinter(); + } + + @Override + public DefaultPrettyPrinter withSeparators(Separators separators) { + _separators = separators; + _objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " "; + return this; + } + + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.java new file mode 100644 index 0000000000..b77a157c70 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/RequestResponsePactCreator.java @@ -0,0 +1,304 @@ +/* + * 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.spec.pact; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import au.com.dius.pact.consumer.ConsumerPactBuilder; +import au.com.dius.pact.consumer.dsl.DslPart; +import au.com.dius.pact.consumer.dsl.PactDslRequestWithPath; +import au.com.dius.pact.consumer.dsl.PactDslResponse; +import au.com.dius.pact.consumer.dsl.PactDslWithProvider; +import au.com.dius.pact.core.model.RequestResponsePact; +import org.apache.commons.lang3.StringUtils; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.Body; +import org.springframework.cloud.contract.spec.internal.Cookies; +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.QueryParameters; +import org.springframework.cloud.contract.spec.internal.RegexProperty; +import org.springframework.cloud.contract.spec.internal.Request; +import org.springframework.cloud.contract.spec.internal.Response; +import org.springframework.util.CollectionUtils; + +import static java.util.stream.Collectors.joining; + +/** + * Creator of {@link RequestResponsePact} instances. + * + * @author Tim Ysewyn + * @author Stessy Delcroix + * @since 2.0.0 + */ +class RequestResponsePactCreator { + + RequestResponsePact createFromContract(List contracts) { + if (CollectionUtils.isEmpty(contracts)) { + return null; + } + Names names = NamingUtil.name(contracts.get(0)); + PactDslWithProvider pactDslWithProvider = ConsumerPactBuilder.consumer(names.getConsumer()) + .hasPactWith(names.getProducer()); + PactDslResponse pactDslResponse = null; + for (Contract contract : contracts) { + assertNoExecutionProperty(contract); + PactDslRequestWithPath pactDslRequest = pactDslResponse != null + ? createPactDslRequestWithPath(contract, pactDslResponse) + : createPactDslRequestWithPath(contract, pactDslWithProvider); + pactDslResponse = createPactDslResponse(contract, pactDslRequest); + } + return pactDslResponse.toPact(); + } + + private void assertNoExecutionProperty(Contract contract) { + assertNoExecutionPropertyInBody(contract.getRequest().getBody(), DslProperty::getServerValue); + assertNoExecutionPropertyInBody(contract.getResponse().getBody(), DslProperty::getClientValue); + } + + private void assertNoExecutionPropertyInBody(Body body, + Function, Object> dslPropertyValueExtractor) { + traverseValues(body, dslPropertyValueExtractor, (Object object) -> { + if (object instanceof ExecutionProperty) { + throw new UnsupportedOperationException("We can't convert a contract that has execution property"); + } + return object; + }); + } + + private void traverseValues(Object value, Function, Object> dslPropertyValueExtractor, + Function function) { + if (value instanceof DslProperty) { + traverseValues(dslPropertyValueExtractor.apply((DslProperty) value), dslPropertyValueExtractor, + function); + } + else if (value instanceof Map) { + ((Map) value).values().forEach(v -> traverseValues(v, dslPropertyValueExtractor, function)); + } + else if (value instanceof Collection) { + ((Collection) value).forEach(v -> traverseValues(v, dslPropertyValueExtractor, function)); + } + else { + function.apply(value); + } + } + + private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, PactDslResponse pactDslResponse) { + Request request = contract.getRequest(); + PactDslRequestWithPath pactDslRequest = pactDslResponse + .uponReceiving(StringUtils.isNotBlank(contract.getDescription()) ? contract.getDescription() : "") + .path(url(request)).method(request.getMethod().getServerValue().toString()); + String query = query(request); + if (StringUtils.isNotBlank(query)) { + pactDslRequest = pactDslRequest.encodedQuery(query); + } + final PactDslRequestWithPath finalPactDslRequest = pactDslRequest; + if (request.getHeaders() != null) { + request.getHeaders().getEntries().forEach(h -> processHeader(finalPactDslRequest, h)); + } + if (request.getCookies() != null) { + pactDslRequest = processCookies(finalPactDslRequest, request.getCookies()); + } + if (request.getBody() != null) { + DslPart pactRequestBody = BodyConverter.toPactBody(request.getBody(), DslProperty::getClientValue); + if (request.getBodyMatchers() != null) { + pactRequestBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(request.getBodyMatchers())); + } + pactRequestBody + .setGenerators(ValueGeneratorConverter.extract(request.getBody(), DslProperty::getClientValue)); + pactDslRequest = pactDslRequest.body(pactRequestBody); + } + return pactDslRequest; + } + + private PactDslRequestWithPath createPactDslRequestWithPath(Contract contract, + PactDslWithProvider pactDslWithProvider) { + Request request = contract.getRequest(); + PactDslRequestWithPath pactDslRequest = pactDslWithProvider + .uponReceiving(StringUtils.isNotBlank(contract.getDescription()) ? contract.getDescription() : "") + .path(url(request)).method(request.getMethod().getServerValue().toString()); + String query = query(request); + if (StringUtils.isNotBlank(query)) { + pactDslRequest = pactDslRequest.encodedQuery(query); + } + final PactDslRequestWithPath finalPactDslRequest = pactDslRequest; + if (request.getHeaders() != null) { + request.getHeaders().getEntries().forEach(h -> { + processHeader(finalPactDslRequest, h); + }); + } + + if (request.getBody() != null) { + DslPart pactRequestBody = BodyConverter.toPactBody(request.getBody(), DslProperty::getServerValue); + if (request.getBodyMatchers() != null) { + pactRequestBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(request.getBodyMatchers())); + } + pactRequestBody + .setGenerators(ValueGeneratorConverter.extract(request.getBody(), DslProperty::getClientValue)); + pactDslRequest = pactDslRequest.body(pactRequestBody); + } + return pactDslRequest; + } + + private String url(Request request) { + if (request.getUrlPath() != null) { + return request.getUrlPath().getServerValue().toString(); + } + else if (request.getUrl() != null) { + return request.getUrl().getServerValue().toString(); + } + throw new IllegalStateException("No url provided"); + } + + private String query(Request request) { + final StringBuilder query = new StringBuilder(); + QueryParameters params = queryParams(request); + if (params != null) { + AtomicInteger index = new AtomicInteger(); + params.getParameters().forEach(p -> { + query.append(p.getName()).append('=').append(p.getServerValue()); + if (index.incrementAndGet() < params.getParameters().size()) { + query.append('&'); + } + + }); + } + return query.toString(); + } + + private QueryParameters queryParams(Request request) { + if (request.getUrlPath() != null) { + return request.getUrlPath().getQueryParameters(); + } + else if (request.getUrl() != null) { + return request.getUrl().getQueryParameters(); + } + throw new IllegalStateException("No url provided"); + + } + + private PactDslRequestWithPath processHeader(PactDslRequestWithPath pactDslRequest, Header header) { + if (header.isSingleValue()) { + String value = getDslPropertyServerValue(header).toString(); + return pactDslRequest.headers(header.getName(), value); + } + else { + String regex = getDslPropertyClientValue(header).toString(); + String example = getDslPropertyServerValue(header).toString(); + return pactDslRequest.matchHeader(header.getName(), regex, example); + } + } + + private String stubSideCookieExample(Cookies cookies) { + return cookies.asStubSideMap().entrySet().stream().map(Object::toString).collect(joining(";")); + } + + private Object getDslPropertyClientValue(Object o) { + Object value = o; + if (value instanceof DslProperty) { + value = getDslPropertyClientValue(((DslProperty) value).getClientValue()); + } + return value; + } + + private Object getDslPropertyServerValue(Object o) { + Object value = o; + if (value instanceof DslProperty) { + value = getDslPropertyServerValue(((DslProperty) value).getServerValue()); + } + return value; + } + + private PactDslRequestWithPath processCookies(PactDslRequestWithPath pactDslRequest, Cookies cookies) { + Map stubSideCookies = cookies.asStubSideMap(); + Collection regexProperties = stubSideCookies.values().stream() + .filter(r -> r instanceof Pattern || r instanceof RegexProperty).map(RegexProperty::new) + .collect(Collectors.toList()); + if (!regexProperties.isEmpty()) { + String regex = regexProperties.stream().map(RegexProperty::pattern).collect(joining("|")); + return pactDslRequest.matchHeader("Cookie", regex, testSideCookieExample(cookies)); + } + else { + return pactDslRequest.headers("Cookie", testSideCookieExample(cookies)); + } + } + + private String testSideCookieExample(Cookies cookies) { + return cookies.asTestSideMap().entrySet().stream().map(Object::toString).collect(joining(";")); + } + + private PactDslResponse createPactDslResponse(Contract contract, PactDslRequestWithPath pactDslRequest) { + Response response = contract.getResponse(); + PactDslResponse pactDslResponse = pactDslRequest.willRespondWith() + .status((Integer) response.getStatus().getClientValue()); + + PactDslResponse finalPactDslResponse = pactDslResponse; + if (response.getHeaders() != null) { + response.getHeaders().getEntries().forEach(h -> processHeader(finalPactDslResponse, h)); + } + + if (response.getCookies() != null) { + pactDslResponse = processCookies(pactDslResponse, response.getCookies()); + } + if (response.getBody() != null) { + DslPart pactResponseBody = BodyConverter.toPactBody(response.getBody(), DslProperty::getClientValue); + if (response.getBodyMatchers() != null) { + pactResponseBody.setMatchers(MatchingRulesConverter.matchingRulesForBody(response.getBodyMatchers())); + } + pactResponseBody + .setGenerators(ValueGeneratorConverter.extract(response.getBody(), DslProperty::getServerValue)); + pactDslResponse = pactDslResponse.body(pactResponseBody); + } + return pactDslResponse; + } + + private PactDslResponse processHeader(PactDslResponse pactDslResponse, Header header) { + if (header.isSingleValue()) { + String value = getDslPropertyClientValue(header).toString(); + return pactDslResponse.headers(Collections.singletonMap(header.getName(), value)); + } + else { + String regex = getDslPropertyServerValue(header).toString(); + String example = getDslPropertyClientValue(header).toString(); + return pactDslResponse.matchHeader(header.getName(), regex, example); + } + } + + private PactDslResponse processCookies(PactDslResponse pactDslResponse, Cookies cookies) { + Map testSideCookies = cookies.asTestSideMap(); + Collection regexProperties = testSideCookies.values().stream() + .filter(p -> p instanceof Pattern || p instanceof RegexProperty).map(RegexProperty::new) + .collect(Collectors.toList()); + if (!regexProperties.isEmpty()) { + String regex = regexProperties.stream().map(RegexProperty::pattern).collect(joining("|")); + return pactDslResponse.matchHeader("Cookie", regex, stubSideCookieExample(cookies)); + } + else { + return pactDslResponse.headers(Collections.singletonMap("Cookie", stubSideCookieExample(cookies))); + } + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.java new file mode 100644 index 0000000000..709cbabf92 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/RequestResponseSCContractCreator.java @@ -0,0 +1,424 @@ +/* + * 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.spec.pact; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import au.com.dius.pact.core.model.OptionalBody; +import au.com.dius.pact.core.model.ProviderState; +import au.com.dius.pact.core.model.Request; +import au.com.dius.pact.core.model.RequestResponseInteraction; +import au.com.dius.pact.core.model.RequestResponsePact; +import au.com.dius.pact.core.model.Response; +import au.com.dius.pact.core.model.matchingrules.Category; +import au.com.dius.pact.core.model.matchingrules.DateMatcher; +import au.com.dius.pact.core.model.matchingrules.MatchingRule; +import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup; +import au.com.dius.pact.core.model.matchingrules.MaxTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.MinMaxTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.NullMatcher; +import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.RegexMatcher; +import au.com.dius.pact.core.model.matchingrules.RuleLogic; +import au.com.dius.pact.core.model.matchingrules.TimeMatcher; +import au.com.dius.pact.core.model.matchingrules.TimestampMatcher; +import au.com.dius.pact.core.model.matchingrules.TypeMatcher; +import org.apache.commons.collections4.MapUtils; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.RegexPatterns; +import org.springframework.cloud.contract.verifier.util.JsonPaths; +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; + +/** + * Creator of {@link Contract} instances. + * + * @author Tim Ysewyn + * @author Stessy Delcroix + * @since 2.0.0 + */ +class RequestResponseSCContractCreator { + + private static final String FULL_BODY = "$"; + + Collection convertFrom(RequestResponsePact pact) { + return pact.getInteractions().stream().map(interaction -> Contract.make(contract -> { + mapContractDescription(interaction, contract); + mapContractRequest(interaction, contract); + mapContractResponse(interaction, contract); + })).collect(Collectors.toList()); + } + + private void mapContractDescription(RequestResponseInteraction interaction, Contract contract) { + contract.description(buildDescription(interaction)); + } + + private void mapContractRequest(RequestResponseInteraction interaction, Contract contract) { + contract.request((contractRequest) -> { + Request pactRequest = interaction.getRequest(); + contractRequest.method(pactRequest.getMethod()); + mapRequestUrl(contractRequest, pactRequest); + + if (MapUtils.isNotEmpty(pactRequest.getHeaders())) { + mapRequestHeaders(contractRequest, pactRequest); + } + if (pactRequest.getHeaders().containsKey("Cookie")) { + mapRequestCookies(contractRequest, pactRequest); + } + if (pactRequest.getBody().getState() == OptionalBody.State.PRESENT) { + mapRequestBody(contractRequest, pactRequest); + } + Category bodyRules = pactRequest.getMatchingRules().rulesForCategory("body"); + if (MapUtils.isNotEmpty(bodyRules.getMatchingRules())) { + mapRequestBodyRules(contractRequest, bodyRules); + } + }); + } + + private void mapContractResponse(RequestResponseInteraction interaction, Contract contract) { + contract.response((contractResponse) -> { + Response pactResponse = interaction.getResponse(); + contractResponse.status(pactResponse.getStatus()); + if (pactResponse.getBody().isPresent()) { + mapResponseBody(contractResponse, pactResponse); + } + + Category bodyRules = pactResponse.getMatchingRules().rulesForCategory("body"); + if (MapUtils.isNotEmpty(bodyRules.getMatchingRules())) { + mapResponseBodyRules(contractResponse, pactResponse, bodyRules); + } + if (MapUtils.isNotEmpty(pactResponse.getHeaders())) { + mapResponseHeaders(contractResponse, pactResponse); + } + + if (pactResponse.getHeaders().containsKey("Cookie")) { + mapResponseCookies(contractResponse, pactResponse); + } + }); + } + + private void mapResponseBodyRules(org.springframework.cloud.contract.spec.internal.Response contractResponse, + Response pactResponse, Category bodyRules) { + contractResponse.bodyMatchers((bodyMatchers) -> { + bodyRules.getMatchingRules().forEach((key, matchindRuleGroup) -> { + if (matchindRuleGroup.getRuleLogic() != RuleLogic.AND) { + throw new UnsupportedOperationException( + "Currently only the AND combination rule logic is supported"); + } + if (FULL_BODY.equals(key)) { + JsonPaths jsonPaths = JsonToJsonPathsConverter + .transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck( + new String(pactResponse.getBody().getValue())); + jsonPaths.forEach( + (jsonPath) -> bodyMatchers.jsonPath(jsonPath.keyBeforeChecking(), bodyMatchers.byType())); + } + else { + matchindRuleGroup.getRules().forEach((matchingRule) -> { + if (matchingRule instanceof NullMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byNull()); + } + else if (matchingRule instanceof RegexMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex())); + } + else if (matchingRule instanceof DateMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byDate()); + } + else if (matchingRule instanceof TimeMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byTime()); + } + else if (matchingRule instanceof TimestampMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byTimestamp()); + } + else if (matchingRule instanceof MinTypeMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> valueHolder + .minOccurrence((((MinTypeMatcher) matchingRule).getMin())))); + } + else if (matchingRule instanceof MinMaxTypeMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> { + valueHolder.minOccurrence((((MinMaxTypeMatcher) matchingRule).getMin())); + valueHolder.maxOccurrence((((MinMaxTypeMatcher) matchingRule).getMax())); + })); + } + else if (matchingRule instanceof MaxTypeMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byType((valueHolder) -> valueHolder + .maxOccurrence((((MaxTypeMatcher) matchingRule).getMax())))); + } + else if (matchingRule instanceof TypeMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byType()); + } + else if (matchingRule instanceof NumberTypeMatcher) { + switch (((NumberTypeMatcher) matchingRule).getNumberType()) { + case NUMBER: + bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.number())); + break; + case INTEGER: + bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.anInteger())); + break; + case DECIMAL: + bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.aDouble())); + break; + default: + throw new UnsupportedOperationException("Unsupported number type!"); + } + } + }); + } + }); + }); + } + + private void mapResponseBody(org.springframework.cloud.contract.spec.internal.Response contractResponse, + Response pactResponse) { + Object parsedBody = BodyConverter.toSCCBody(pactResponse); + if (parsedBody instanceof Map) { + contractResponse.body((Map) parsedBody); + } + else if (parsedBody instanceof List) { + contractResponse.body((List) parsedBody); + } + else { + contractResponse.body(parsedBody.toString()); + } + } + + private void mapRequestBodyRules(org.springframework.cloud.contract.spec.internal.Request contractRequest, + Category bodyRules) { + contractRequest.bodyMatchers((bodyMatchers) -> { + bodyRules.getMatchingRules().forEach((key, matchingRuleGroup) -> { + if (matchingRuleGroup.getRuleLogic() != RuleLogic.AND) { + throw new UnsupportedOperationException( + "Currently only the AND combination rule logic is supported"); + } + + matchingRuleGroup.getRules().forEach((matchingRule) -> { + if (matchingRule instanceof RegexMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byRegex(((RegexMatcher) matchingRule).getRegex())); + } + else if (matchingRule instanceof DateMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byDate()); + } + else if (matchingRule instanceof TimeMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byTime()); + } + else if (matchingRule instanceof TimestampMatcher) { + bodyMatchers.jsonPath(key, bodyMatchers.byTimestamp()); + } + else if (matchingRule instanceof NumberTypeMatcher) { + switch (((NumberTypeMatcher) matchingRule).getNumberType()) { + case NUMBER: + bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.number())); + break; + case INTEGER: + bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.anInteger())); + break; + case DECIMAL: + bodyMatchers.jsonPath(key, bodyMatchers.byRegex(RegexPatterns.aDouble())); + break; + default: + throw new RuntimeException("Unsupported number type!"); + } + } + }); + }); + }); + } + + private void mapRequestBody(org.springframework.cloud.contract.spec.internal.Request contractRequest, + Request pactRequest) { + Object parsedBody = BodyConverter.toSCCBody(pactRequest); + if (parsedBody instanceof Map) { + contractRequest.body((Map) parsedBody); + } + else if (parsedBody instanceof List) { + contractRequest.body((List) parsedBody); + } + else { + contractRequest.body(parsedBody.toString()); + } + } + + private void mapRequestCookies(org.springframework.cloud.contract.spec.internal.Request contractRequest, + Request pactRequest) { + Category headerRules = pactRequest.getMatchingRules().rulesForCategory("header"); + String[] splitCookiesHeader = pactRequest.getHeaders().get("Cookie").get(0).split(";"); + Map foundCookies = Stream.of(splitCookiesHeader).map((cookieHeader) -> cookieHeader.split("=")) + .collect(Collectors.toMap(splittedCookieHeader -> splittedCookieHeader[0], + splittedCookieHeader -> splittedCookieHeader[1])); + + contractRequest.cookies((cookies) -> foundCookies.forEach((key, value) -> { + if (headerRules.getMatchingRules().containsKey("Cookie")) { + MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get("Cookie"); + if (matchingRuleGroup.getRules().size() > 1) { + throw new UnsupportedOperationException( + "Currently only 1 rule at a time for a header is supported"); + } + MatchingRule matchingRule = matchingRuleGroup.getRules().get(0); + if (matchingRule instanceof RegexMatcher) { + cookies.cookie(key, + contractRequest.$( + contractRequest.c(contractRequest.regex(((RegexMatcher) matchingRule).getRegex())), + contractRequest.p(value))); + } + else { + throw new UnsupportedOperationException( + "Currently only the header matcher of type regex is supported"); + } + } + else { + cookies.cookie(key, value); + } + })); + } + + private void mapResponseCookies(org.springframework.cloud.contract.spec.internal.Response contractResponse, + Response pactResponse) { + Category headerRules = pactResponse.getMatchingRules().rulesForCategory("header"); + String[] splitCookiesHeader = pactResponse.getHeaders().get("Cookie").get(0).split(";"); + Map foundCookies = Stream.of(splitCookiesHeader).map((cookieHeader) -> cookieHeader.split("=")) + .collect(Collectors.toMap(splittedCookieHeader -> splittedCookieHeader[0], + splittedCookieHeader -> splittedCookieHeader[1])); + + contractResponse.cookies((cookies) -> foundCookies.forEach((key, value) -> { + if (headerRules.getMatchingRules().containsKey("Cookie")) { + MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get("Cookie"); + if (matchingRuleGroup.getRules().size() > 1) { + throw new UnsupportedOperationException( + "Currently only 1 rule at a time for a header is supported"); + } + MatchingRule matchingRule = matchingRuleGroup.getRules().get(0); + if (matchingRule instanceof RegexMatcher) { + cookies.cookie(key, contractResponse.$( + contractResponse.p( + contractResponse.regex(Pattern.compile(((RegexMatcher) matchingRule).getRegex()))), + contractResponse.c(value))); + } + else { + throw new UnsupportedOperationException( + "Currently only the header matcher of type regex is supported"); + } + } + else { + cookies.cookie(key, value); + } + })); + } + + private void mapRequestUrl(org.springframework.cloud.contract.spec.internal.Request contractRequest, + Request pactRequest) { + if (MapUtils.isNotEmpty(pactRequest.getQuery())) { + contractRequest.url(pactRequest.getPath(), + (url) -> url.queryParameters((queryParameters) -> pactRequest.getQuery().forEach((key, + values) -> values.forEach((singleValue) -> queryParameters.parameter(key, singleValue))))); + } + else { + contractRequest.url(pactRequest.getPath()); + } + } + + private void mapRequestHeaders(org.springframework.cloud.contract.spec.internal.Request contractRequest, + Request pactRequest) { + Category headerRules = pactRequest.getMatchingRules().rulesForCategory("header"); + contractRequest.headers((headers) -> pactRequest.getHeaders().forEach((key, values) -> { + if (key.equalsIgnoreCase("Cookie")) { + return; + } + if (headerRules.getMatchingRules().containsKey(key)) { + MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get(key); + if (matchingRuleGroup.getRules().size() > 1) { + throw new UnsupportedOperationException( + "Currently only 1 rule at a time for a header is supported"); + } + MatchingRule matchingRule = matchingRuleGroup.getRules().get(0); + if (matchingRule instanceof RegexMatcher) { + values.forEach((value) -> { + headers.header(key, + contractRequest.$( + contractRequest + .c(contractRequest.regex(((RegexMatcher) matchingRule).getRegex())), + contractRequest.p(value))); + }); + } + else { + throw new UnsupportedOperationException( + "Currently only the header matcher of type regex is supported"); + } + } + else { + values.forEach((value) -> headers.header(key, value)); + } + })); + } + + private void mapResponseHeaders(org.springframework.cloud.contract.spec.internal.Response contractResponse, + Response pactResponse) { + Category headerRules = pactResponse.getMatchingRules().rulesForCategory("header"); + contractResponse.headers((headers) -> pactResponse.getHeaders().forEach((key, values) -> { + if (key.equalsIgnoreCase("Cookie")) { + return; + } + if (headerRules.getMatchingRules().containsKey(key)) { + MatchingRuleGroup matchingRuleGroup = headerRules.getMatchingRules().get(key); + if (matchingRuleGroup.getRules().size() > 1) { + throw new UnsupportedOperationException( + "Currently only 1 rule at a time for a header is supported"); + } + MatchingRule matchingRule = matchingRuleGroup.getRules().get(0); + if (matchingRule instanceof RegexMatcher) { + values.forEach((value) -> { + headers.header(key, + contractResponse.$( + contractResponse.p(contractResponse + .regex(Pattern.compile(((RegexMatcher) matchingRule).getRegex()))), + contractResponse.c(value))); + }); + } + else { + throw new UnsupportedOperationException( + "Currently only the header matcher of type regex is supported"); + } + } + else { + values.forEach((value) -> headers.header(key, value)); + } + })); + } + + private String buildDescription(RequestResponseInteraction interaction) { + StringBuilder description = new StringBuilder(interaction.getDescription()); + List providerStates = interaction.getProviderStates(); + for (ProviderState providerState : providerStates) { + description.append(" ").append(providerState.getName()); + Map params = providerState.getParams(); + if (MapUtils.isNotEmpty(params)) { + description.append("("); + params.forEach((k, v) -> { + description.append(k).append(": ").append(v.toString()).append(", "); + }); + description.delete(description.length() - 2, description.length()); + description.append(")"); + } + } + return description.toString(); + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.java new file mode 100644 index 0000000000..fb6833e5d1 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-pact/src/main/java/org/springframework/cloud/contract/verifier/spec/pact/ValueGeneratorConverter.java @@ -0,0 +1,215 @@ +/* + * 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.spec.pact; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.Pattern; + +import au.com.dius.pact.core.model.generators.Category; +import au.com.dius.pact.core.model.generators.DateGenerator; +import au.com.dius.pact.core.model.generators.DateTimeGenerator; +import au.com.dius.pact.core.model.generators.Generator; +import au.com.dius.pact.core.model.generators.Generators; +import au.com.dius.pact.core.model.generators.RandomBooleanGenerator; +import au.com.dius.pact.core.model.generators.RandomDecimalGenerator; +import au.com.dius.pact.core.model.generators.RandomHexadecimalGenerator; +import au.com.dius.pact.core.model.generators.RandomIntGenerator; +import au.com.dius.pact.core.model.generators.RandomStringGenerator; +import au.com.dius.pact.core.model.generators.RegexGenerator; +import au.com.dius.pact.core.model.generators.TimeGenerator; +import au.com.dius.pact.core.model.generators.UuidGenerator; +import groovy.lang.GString; +import org.apache.commons.lang3.StringUtils; + +import org.springframework.cloud.contract.spec.internal.Body; +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.OutputMessage; +import org.springframework.cloud.contract.spec.internal.RegexProperty; +import org.springframework.cloud.contract.verifier.util.ContentUtils; + +/** + * Convert a value for a given {@link Generator}. + * + * @author Tim Ysewyn + * @author Stessy Delcroix + * @since 2.0.0 + */ +final class ValueGeneratorConverter { + + private static final String INTEGER_PATTERN = "-?(\\d+)"; + + private static final Pattern INTEGER = Pattern.compile(INTEGER_PATTERN); + + private static final String DECIMAL_PATTERN = "-?(\\d*\\.\\d+)"; + + private static final Pattern DECIMAL = Pattern.compile(DECIMAL_PATTERN); + + private static final String HEX_PATTERN = "[a-fA-F0-9]+"; + + private static final Pattern HEX = Pattern.compile(HEX_PATTERN); + + private static final String ALPHA_NUMERIC_PATTERN = "[a-zA-Z0-9]+"; + + private static final Pattern ALPHA_NUMERIC = Pattern.compile(ALPHA_NUMERIC_PATTERN); + + private static final String UUID_PATTERN = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"; + + private static final Pattern UUID = Pattern.compile(UUID_PATTERN); + + private static final String ANY_DATE_PATTERN = "(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"; + + private static final Pattern ANY_DATE = Pattern.compile(ANY_DATE_PATTERN); + + private static final String ANY_TIME_PATTERN = "(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"; + + private static final Pattern ANY_TIME = Pattern.compile(ANY_TIME_PATTERN); + + private static final String ANY_DATE_TIME_PATTERN = "([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"; + + private static final Pattern ANY_DATE_TIME = Pattern.compile(ANY_DATE_TIME_PATTERN); + + private static final String TRUE_OR_FALSE_PATTERN = "(true|false)"; + + private static final Pattern TRUE_OR_FALSE = Pattern.compile(TRUE_OR_FALSE_PATTERN); + + private ValueGeneratorConverter() { + + } + + static DslProperty convert(Generator generator, + BiFunction> dslPropertyProvider) { + Pattern pattern = null; + if (generator instanceof RandomIntGenerator) { + pattern = INTEGER; + } + else if (generator instanceof RandomDecimalGenerator) { + pattern = DECIMAL; + } + else if (generator instanceof RandomHexadecimalGenerator) { + pattern = HEX; + } + else if (generator instanceof RandomStringGenerator) { + pattern = ALPHA_NUMERIC; + } + else if (generator instanceof RegexGenerator) { + pattern = Pattern.compile(((RegexGenerator) generator).getRegex()); + } + else if (generator instanceof UuidGenerator) { + pattern = UUID; + } + else if (generator instanceof DateGenerator) { + pattern = getDateTimePattern(((DateGenerator) generator).getFormat(), ANY_DATE); + } + else if (generator instanceof TimeGenerator) { + pattern = getDateTimePattern(((TimeGenerator) generator).getFormat(), ANY_TIME); + } + else if (generator instanceof DateTimeGenerator) { + pattern = getDateTimePattern(((DateTimeGenerator) generator).getFormat(), ANY_DATE_TIME); + } + else if (generator instanceof RandomBooleanGenerator) { + pattern = TRUE_OR_FALSE; + } + if (pattern == null) { + throw new UnsupportedOperationException( + "We currently don't support a generator of type " + generator.getClass().getSimpleName()); + } + else { + Object generatedValue = generator.generate(new HashMap<>()); + return dslPropertyProvider.apply(pattern, generatedValue); + } + } + + private static Pattern getDateTimePattern(String format, Pattern defaultPattern) { + return StringUtils.isNotBlank(format) ? Pattern.compile(format) : defaultPattern; + } + + static Generators extract(Body body, Function, Object> dslPropertyValueProvider) { + Generators generators = new Generators(); + traverse(body, dslPropertyValueProvider, "", generators, Category.BODY); + return generators; + } + + static Generators extract(OutputMessage message, Function, Object> dslPropertyValueProvider) { + Generators generators = new Generators(); + traverse(message.getBody(), dslPropertyValueProvider, "", generators, Category.BODY); + return generators; + } + + private static void traverse(Object value, Function, Object> dslPropertyValueProvider, String path, + Generators generators, Category category) { + Object v = value; + if (v instanceof DslProperty) { + v = dslPropertyValueProvider.apply((DslProperty) v); + } + if (v instanceof GString) { + v = ContentUtils.extractValue((GString) v, dslPropertyValueProvider); + } + if (v instanceof Map) { + ((Map) v).forEach( + (key, val) -> traverse(val, dslPropertyValueProvider, path + "." + key, generators, category)); + } + else if (v instanceof Collection) { + AtomicInteger index = new AtomicInteger(); + ((Collection) v).forEach(val -> traverse(val, dslPropertyValueProvider, + path + "[" + index.getAndIncrement() + "]", generators, category)); + } + else if (v instanceof DslProperty) { + traverse(v, dslPropertyValueProvider, path, generators, category); + } + else if (v instanceof RegexProperty || v instanceof Pattern) { + RegexProperty regexProperty = new RegexProperty(v); + switch (regexProperty.pattern()) { + case INTEGER_PATTERN: + generators.addGenerator(category, path, new RandomIntGenerator(0, Integer.MAX_VALUE)); + break; + case DECIMAL_PATTERN: + generators.addGenerator(category, path, new RandomDecimalGenerator(10)); + break; + case HEX_PATTERN: + generators.addGenerator(category, path, new RandomHexadecimalGenerator(10)); + break; + case ALPHA_NUMERIC_PATTERN: + generators.addGenerator(category, path, new RandomStringGenerator(10)); + break; + case UUID_PATTERN: + generators.addGenerator(category, path, UuidGenerator.INSTANCE); + break; + case ANY_DATE_PATTERN: + generators.addGenerator(category, path, new DateGenerator()); + break; + case ANY_TIME_PATTERN: + generators.addGenerator(category, path, new TimeGenerator()); + break; + case ANY_DATE_TIME_PATTERN: + generators.addGenerator(category, path, new DateTimeGenerator()); + break; + case TRUE_OR_FALSE_PATTERN: + generators.addGenerator(category, path, RandomBooleanGenerator.INSTANCE); + break; + default: + generators.addGenerator(category, path, new RegexGenerator(regexProperty.pattern())); + break; + } + } + } + +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilderSpec.groovy index 84bd9f137f..e33dde4867 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBuilderSpec.groovy @@ -42,7 +42,7 @@ class MethodBuilderSpec extends Specification { } } } - File stubFile = new File("invalid-method:name.groovy") + File stubFile = new File("invalid-method;name.groovy") ContractMetadata metadata = new ContractMetadata(stubFile.toPath(), false, 0, null, contractDsl) SingleContractMetadata singleContractMetadata = new SingleContractMetadata(contractDsl, metadata)