diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy index b1bb570bf3..f1b0c532ff 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy @@ -39,6 +39,7 @@ import org.springframework.cloud.contract.spec.internal.MatchingStrategy import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.OptionalProperty import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor +import org.springframework.util.StringUtils import static org.apache.commons.text.StringEscapeUtils.escapeJava import static org.apache.commons.text.StringEscapeUtils.escapeJson @@ -108,7 +109,7 @@ class ContentUtils { * @return JSON structure with replaced client / server side parts */ static Object extractValue(GString bodyAsValue, ContentType contentType, Closure valueProvider) { - if (bodyAsValue.isEmpty()) { + if (!StringUtils.hasText(bodyAsValue.toString())) { return bodyAsValue } if (contentType == ContentType.TEXT || contentType == ContentType.FORM) { 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 8113c1333b..7782fd61a2 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 @@ -140,7 +140,7 @@ class JsonToJsonPathsConverter { } /** - * Related to #391 and #1091. The converted body looks different when done via the String notation than + * Related to #391 and #1091 and #1414. 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 @@ -163,8 +163,16 @@ class JsonToJsonPathsConverter { && isNotRootArray(matcherPath)) { String pathToDelete = pathToDelete(pathWithoutAnyArray) context.delete(pathToDelete) - return pathToDelete.contains(DESCENDANT_OPERATOR) ? false : - removeTrailingContainers(pathToDelete, context) + if (pathToDelete.contains(DESCENDANT_OPERATOR)) { + Object root = context.read('$') + if (rootContainsEmptyContainers(root)) { + // now root contains only empty elements, we should remove the trailing containers + context.delete('$[*]') + return false + } + return false + } + return removeTrailingContainers(pathToDelete, context) } else { int lastIndexOfDot = matcherPath.lastIndexOf(".") @@ -191,6 +199,10 @@ class JsonToJsonPathsConverter { } } + private static boolean rootContainsEmptyContainers(root) { + root instanceof Iterable && root.every { containsOnlyEmptyElements(it) } + } + private static String lastMatch(Matcher matcher) { List matches = [] while ({ @@ -204,6 +216,10 @@ class JsonToJsonPathsConverter { return object instanceof Iterable || object instanceof Map } + private static boolean isEmpty(Object object) { + return isIterable(object) && object instanceof Iterable ? object.isEmpty() : ((Map) object).isEmpty() + } + private static String pathToDelete(String pathWithoutAnyArray) { // we can't remove root return pathWithoutAnyArray == '$' ? '$[*]' : pathWithoutAnyArray diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy index 66954c7d80..9be94f1cdd 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy @@ -1755,6 +1755,65 @@ response: } } + @Issue("#1414") + def "should work with empty arrays after doing array matching [#methodBuilderName]"() { + given: + String yaml = '''\ +name: GET sample +description: sample +request: + method: GET + urlPath: /sample +response: + status: 200 + body: + - foo: "sample1" + bar: true + - foo: "sample2" + bar: false + headers: + Content-Type: application/json + matchers: + body: + - path: $..foo + type: by_regex + value: .* + - path: $..bar + type: by_regex + predefined: any_boolean +''' + File tmpFile = File.createTempFile("foo", ".yml") + tmpFile.createNewFile() + tmpFile.text = yaml + Contract contractDsl = new YamlContractConverter().convertFrom(tmpFile). + first() + methodBuilder() + when: + String test = singleTestGenerator(contractDsl) + then: + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) + !test.contains('''assertThatJson(parsedJson).array().isEmpty()''') + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "spock" | { + properties.testFramework = TestFramework.SPOCK + } + "mockmvc" | { + properties.testMode = TestMode.MOCKMVC + } + "jaxrs-spock" | { + properties.testFramework = TestFramework.SPOCK; properties.testMode = TestMode.JAXRSCLIENT + } + "jaxrs" | { + properties.testFramework = TestFramework.JUNIT; properties.testMode = TestMode.JAXRSCLIENT + } + "testNG" | { + properties.testFramework = TestFramework.TESTNG + } + } + @Issue("#1262") def "should work with the timeout flag for groovy [#methodBuilderName]"() { given: