From be00f6cee23ca0f6d46f324c723d9a0b1698235a Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Mon, 19 Aug 2019 13:07:58 +0200 Subject: [PATCH] Recognise arrays by regex and not only `[*]` literal. (#1172) * Recognise arrays by regex and not only `[*]` literal. Fixes gh-1091. * Add test. * Fix getting index of last matched groups. --- .../util/JsonToJsonPathsConverter.groovy | 22 +++- ...vcMethodBodyBuilderWithMatchersSpec.groovy | 114 ++++++++++++++++++ 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy index 76222e9d62..34a6534bab 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -16,6 +16,7 @@ package org.springframework.cloud.contract.verifier.util +import java.util.regex.Matcher import java.util.regex.Pattern import com.jayway.jsonpath.DocumentContext @@ -42,6 +43,7 @@ import org.springframework.util.SerializationUtils * * @author Marcin Grzejszczak * @author Tim Ysewyn + * @author Olga Maciaszek-Sharma */ @Commons class JsonToJsonPathsConverter { @@ -54,7 +56,7 @@ class JsonToJsonPathsConverter { private static final Boolean SERVER_SIDE = false private static final Boolean CLIENT_SIDE = true - private static final String ANY_ARRAY_NOTATION_IN_JSONPATH = "[*]" + private static final Pattern ANY_ARRAY_NOTATION_IN_JSONPATH = ~/\[(.*?)\]/ private static final String DESCENDANT_OPERATOR = ".." private final ContractVerifierConfigProperties configProperties @@ -126,7 +128,7 @@ class JsonToJsonPathsConverter { } /** - * Related to #391. The converted body looks different when done via the String notation than + * Related to #391 and #1091. The converted body looks different when done via the String notation than * it does when done via a map notation. When working with String body and when matchers * are provided, even when all entries of a map / list got removed, the map / list itself * remains. That leads to unnecessary creation of checks for empty collection. With this method @@ -135,9 +137,10 @@ class JsonToJsonPathsConverter { * defining body... */ private static boolean removeTrailingContainers(String matcherPath, DocumentContext context) { - boolean containsArray = matcherPath.contains(ANY_ARRAY_NOTATION_IN_JSONPATH) - String pathWithoutAnyArray = containsArray ? matcherPath.substring(0, matcherPath. - lastIndexOf(ANY_ARRAY_NOTATION_IN_JSONPATH)) : matcherPath + Matcher matcher = ANY_ARRAY_NOTATION_IN_JSONPATH.matcher(matcherPath) + boolean containsArray = matcher.find() + String pathWithoutAnyArray = containsArray ? matcherPath. + substring(0, matcherPath.lastIndexOf(lastMatch(matcher))) : matcherPath def object = entry(context, pathWithoutAnyArray) // object got removed and it was the only element // let's get its parent and see if it contains an empty element @@ -164,6 +167,15 @@ class JsonToJsonPathsConverter { return false } + private static String lastMatch(Matcher matcher) { + List matches = [] + while ({ + matches << matcher.group() + matcher.find() + }()) continue + return matches[matches.size() - 1] + } + private static boolean isIterable(Object object) { return object instanceof Iterable || object instanceof Map } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy index 18ddedc1a1..269ad318b5 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy @@ -547,4 +547,118 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) } } + @Issue('#1091') + def 'should work for map with array value where matchers cover all array fields for [#methodBuilderName]'() { + given: + Contract contractDsl = Contract.make { + request { + name "ISSUE 1091" + method 'GET' + url '/test' + headers { + contentType(applicationJson()) + } + } + response { + status OK() + body(''' + { + "prices": [ + { + "country" : "ES", + "originalPrice": "1500" + } + ] + } + +''' + ) + bodyMatchers { + jsonPath('$.prices[0].country', byRegex(nonBlank())) + jsonPath('$.prices[0].originalPrice', byRegex(number())) + } + headers { + contentType(applicationJsonUtf8()) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + and: + builder.appendTo(blockBuilder) + String test = blockBuilder.toString() + when: + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) + then: + !test.contains('isEmpty()') + where: + methodBuilderName | methodBuilder + HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties, generatedClassDataForMethod) } + MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) } + JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties, generatedClassDataForMethod) } + JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) } + WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) } + } + + @Issue('#1091') + def 'should work for array containing map with array value where matchers cover all array fields for [#methodBuilderName]'() { + given: + Contract contractDsl = Contract.make { + request { + name "ISSUE 1091" + method 'GET' + url '/test' + headers { + contentType(applicationJson()) + } + } + response { + status OK() + body(''' + { + "test": [ + { + "prices": [ + { + "country" : "ES", + "originalPrice": 1500 + } + ] + } + ] + } + +''' + ) + bodyMatchers { + jsonPath('$.test[0].barcode', byRegex(nonBlank())) + jsonPath('$.test[0].id', byRegex(nonBlank())) + jsonPath('$.test[0].prices[0].country', byRegex(nonBlank())) + jsonPath('$.test[0].prices[0].originalPrice', byRegex(nonBlank())) + jsonPath('$.test[0].prices[?(@.originalPrice==1500)].originalPrice', + byRegex(nonBlank())) + } + headers { + contentType(applicationJsonUtf8()) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + and: + builder.appendTo(blockBuilder) + String test = blockBuilder.toString() + when: + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) + then: + !test.contains('isEmpty()') + where: + methodBuilderName | methodBuilder + HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties, generatedClassDataForMethod) } + MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) } + JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties, generatedClassDataForMethod) } + JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) } + WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) } + } + }