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 eb8dfb7562..28acda592b 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 @@ -55,6 +55,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 String DESCENDANT_OPERATOR = ".." private final ContractVerifierConfigProperties configProperties @@ -87,7 +88,7 @@ class JsonToJsonPathsConverter { def entry = entry(context, matcher.path()) if (entry != null) { context.delete(matcher.path()) - removeTrailingContainers(matcher, context) + removeTrailingContainers(matcher.path(), context) } } catch (RuntimeException e) { if (log.isDebugEnabled()) { @@ -131,21 +132,26 @@ class JsonToJsonPathsConverter { * remove that trailing collection. All in all it's better to use the Groovy based notation for * defining body... */ - private static void removeTrailingContainers(BodyMatcher matcher, DocumentContext context) { - String pathWithoutAnyArray = matcher.path().contains(ANY_ARRAY_NOTATION_IN_JSONPATH) ? matcher.path().substring(0, matcher.path().lastIndexOf(ANY_ARRAY_NOTATION_IN_JSONPATH)) : matcher.path() + 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 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 - if (isIterable(object) && containsOnlyEmptyElements(object)) { + if (isIterable(object) && containsOnlyEmptyElements(object) && isNotRootArray(matcherPath)) { String pathToDelete = pathToDelete(pathWithoutAnyArray) context.delete(pathToDelete) + return pathToDelete.contains(DESCENDANT_OPERATOR) ? false : + removeTrailingContainers(pathToDelete, context) } else { - String lastParent = matcher.path().substring(0, matcher.path().lastIndexOf(".")) + String lastParent = matcherPath.substring(0, matcherPath.lastIndexOf(".")) def lastParentObject = context.read(lastParent) - if (isIterable(lastParentObject) && containsOnlyEmptyElements(lastParentObject)) { + if (isIterable(lastParentObject) && containsOnlyEmptyElements(lastParentObject) && isNotRoot(lastParent)) { context.delete(lastParent) + return removeTrailingContainers(lastParent, context) } } + return false } private static boolean isIterable(Object object) { @@ -157,6 +163,16 @@ class JsonToJsonPathsConverter { return pathWithoutAnyArray == '$' ? '$[*]' : pathWithoutAnyArray } + private static boolean isNotRoot(String path) { + // we can't remove root + return path != '$' + } + + private static boolean isNotRootArray(String path) { + // we can't remove root + return path != '$[*]' + } + private static boolean containsOnlyEmptyElements(Object object) { return object.every { if (it instanceof Map) { 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 c93789c228..7fd0546bd7 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 @@ -1219,4 +1219,47 @@ DocumentContext parsedJson = JsonPath.parse(json); "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } } + @Issue("#727") + def "should not leave empty arrays in a simple structure [#methodBuilderName]"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url '/list' + } + response { + status 200 + body( + [ + content: [ + three: [ + six: "seven" + ] + ] + ] + ) + bodyMatchers { + jsonPath('$.content.three.six', byRegex(".*seven.*")) + jsonPath('$.content.one', byRegex(".*two.*")) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + String test = blockBuilder.toString() + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) + !test.contains('''.isEmpty()''') + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties, classDataForMethod) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties, classDataForMethod) } + "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) } + } + }