diff --git a/docs/src/main/asciidoc/verifier_contract.adoc b/docs/src/main/asciidoc/verifier_contract.adoc index 0094e93029..0480ac90d0 100644 --- a/docs/src/main/asciidoc/verifier_contract.adoc +++ b/docs/src/main/asciidoc/verifier_contract.adoc @@ -821,7 +821,7 @@ match the regex for an ISO DateTime value. the regex for an ISO Time value. ** `byType()`: The value taken from the producer's response via the provided JSON Path needs to be of the same type as the type defined in the body of the response in the contract. -`byType` can take a closure, in which you can set `minOccurrence` and `maxOccurrence`. +`byType` can take a closure, in which you can set `minOccurrence` and `maxOccurrence`. For the request side, you should use the closure to assert size of the collection. That way, you can assert the size of the flattened collection. To check the size of an unflattened collection, use a custom method with the `byCommand(...)` testMatcher. ** `byCommand(...)`: The value taken from the producer's response via the provided JSON Path is @@ -869,6 +869,8 @@ Below you can find the allowed list of `type`s. ** `by_date` ** `by_timestamp` ** `by_time` +** `by_type` +*** there are 2 additional fields accepted: `minOccurrence` and `maxOccurrence`. * For `testMatchers`: ** `by_equality` ** `by_regex` diff --git a/samples/standalone/dsl/http-client/build.gradle b/samples/standalone/dsl/http-client/build.gradle index cb691c1d73..83d35bab3a 100644 --- a/samples/standalone/dsl/http-client/build.gradle +++ b/samples/standalone/dsl/http-client/build.gradle @@ -56,10 +56,6 @@ test { } } -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' -} - task resolveDependencies { doLast { project.rootProject.allprojects.each { subProject -> diff --git a/samples/standalone/dsl/http-server/build.gradle b/samples/standalone/dsl/http-server/build.gradle index 371049a285..fa4aa79994 100644 --- a/samples/standalone/dsl/http-server/build.gradle +++ b/samples/standalone/dsl/http-server/build.gradle @@ -27,7 +27,7 @@ repositories { } // end::deps_repos[] -apply plugin: 'groovy' +apply plugin: 'java' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'spring-cloud-contract' @@ -65,10 +65,6 @@ test { } } -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' -} - clean.doFirst { delete "~/.m2/repository/com/example/http-server-dsl-gradle" } diff --git a/samples/standalone/messaging/stream-sink/build.gradle b/samples/standalone/messaging/stream-sink/build.gradle index 2cc8737631..247cc7a14a 100644 --- a/samples/standalone/messaging/stream-sink/build.gradle +++ b/samples/standalone/messaging/stream-sink/build.gradle @@ -69,8 +69,4 @@ task resolveDependencies { } } } -} - -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' } \ No newline at end of file diff --git a/samples/standalone/messaging/stream-source/build.gradle b/samples/standalone/messaging/stream-source/build.gradle index 6a7ea21de0..48820d7946 100644 --- a/samples/standalone/messaging/stream-source/build.gradle +++ b/samples/standalone/messaging/stream-source/build.gradle @@ -110,8 +110,4 @@ task resolveDependencies { } } } -} - -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' } \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/build.gradle b/samples/standalone/pact/pact-http-client/build.gradle index 2095c45e52..be53073f52 100644 --- a/samples/standalone/pact/pact-http-client/build.gradle +++ b/samples/standalone/pact/pact-http-client/build.gradle @@ -58,10 +58,6 @@ test { } } -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' -} - task resolveDependencies { doLast { project.rootProject.allprojects.each { subProject -> diff --git a/samples/standalone/pact/pact-http-server/build.gradle b/samples/standalone/pact/pact-http-server/build.gradle index 4e5517b687..b5734dc99a 100644 --- a/samples/standalone/pact/pact-http-server/build.gradle +++ b/samples/standalone/pact/pact-http-server/build.gradle @@ -64,10 +64,6 @@ test { } } -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' -} - clean.doFirst { delete "~/.m2/repository/com/example/http-server-pact-gradle" } diff --git a/samples/standalone/restdocs/http-client/build.gradle b/samples/standalone/restdocs/http-client/build.gradle index eb27f76c3f..baccadaa9a 100644 --- a/samples/standalone/restdocs/http-client/build.gradle +++ b/samples/standalone/restdocs/http-client/build.gradle @@ -60,10 +60,6 @@ test { } } -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' -} - task resolveDependencies { doLast { project.rootProject.allprojects.each { subProject -> diff --git a/samples/standalone/webclient/http-client/build.gradle b/samples/standalone/webclient/http-client/build.gradle index 8502a4fc91..84fae486fc 100644 --- a/samples/standalone/webclient/http-client/build.gradle +++ b/samples/standalone/webclient/http-client/build.gradle @@ -60,10 +60,6 @@ test { } } -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' -} - task resolveDependencies { doLast { project.rootProject.allprojects.each { subProject -> diff --git a/samples/standalone/webclient/http-server/build.gradle b/samples/standalone/webclient/http-server/build.gradle index 7652bad43a..076558b78b 100644 --- a/samples/standalone/webclient/http-server/build.gradle +++ b/samples/standalone/webclient/http-server/build.gradle @@ -62,10 +62,6 @@ test { } } -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' -} - task stubsJar(type: Jar, dependsOn: ['copySnippets', 'copySources', 'copyClasses']) { baseName = project.name classifier = 'stubs' diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy index 9c8fb3246a..2256bf392d 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/BodyMatchers.groovy @@ -73,6 +73,13 @@ class BodyMatchers { return new MatchingTypeValue(MatchingType.EQUALITY, null) } + MatchingTypeValue byType(@DelegatesTo(MatchingTypeValueHolder) Closure closure) { + MatchingTypeValueHolder matchingTypeValue = new MatchingTypeValueHolder() + closure.delegate = matchingTypeValue + closure() + return matchingTypeValue.matchingTypeValue + } + boolean equals(o) { if (this.is(o)) return true if (this.getClass() != o.class) return false @@ -147,3 +154,26 @@ class MatchingTypeValue { */ Integer maxTypeOccurrence } + +@CompileStatic +@ToString(includePackage = false) +@EqualsAndHashCode +class MatchingTypeValueHolder { + MatchingTypeValue matchingTypeValue = new MatchingTypeValue(type: MatchingType.TYPE) + + MatchingTypeValue minOccurrence(int number) { + this.matchingTypeValue.minTypeOccurrence = number + return this.matchingTypeValue + } + + MatchingTypeValue maxOccurrence(int number) { + this.matchingTypeValue.maxTypeOccurrence = number + return this.matchingTypeValue + } + + MatchingTypeValue occurrence(int number) { + this.matchingTypeValue.minTypeOccurrence = number + this.matchingTypeValue.maxTypeOccurrence = number + return this.matchingTypeValue + } +} \ No newline at end of file diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy index e98e5f5f54..d335b288de 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ResponseBodyMatchers.groovy @@ -16,9 +16,7 @@ package org.springframework.cloud.contract.spec.internal import groovy.transform.CompileStatic -import groovy.transform.EqualsAndHashCode import groovy.transform.ToString - /** * Body matchers for the response side (output message, REST response) * @@ -38,31 +36,7 @@ class ResponseBodyMatchers extends BodyMatchers { return new MatchingTypeValue(MatchingType.COMMAND, new ExecutionProperty(execute)) } - MatchingTypeValue byType(@DelegatesTo(MatchingTypeValueHolder) Closure closure) { - MatchingTypeValueHolder matchingTypeValue = new MatchingTypeValueHolder() - closure.delegate = matchingTypeValue - closure() - return matchingTypeValue.matchingTypeValue - } - MatchingTypeValue byNull() { return new MatchingTypeValue(MatchingType.NULL, null) } } - -@CompileStatic -@ToString(includePackage = false) -@EqualsAndHashCode -class MatchingTypeValueHolder { - MatchingTypeValue matchingTypeValue = new MatchingTypeValue(type: MatchingType.TYPE) - - MatchingTypeValue minOccurrence(int number) { - this.matchingTypeValue.minTypeOccurrence = number - return this.matchingTypeValue - } - - MatchingTypeValue maxOccurrence(int number) { - this.matchingTypeValue.maxTypeOccurrence = number - return this.matchingTypeValue - } -} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy index 5aed3adbcf..643f08e55c 100755 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy @@ -581,6 +581,15 @@ class DslToWireMockClientConverterSpec extends Specification { anothervalue: 4 ] ] + ], + valueWithMin: [ + 1,2,3 + ], + valueWithMax: [ + 1,2,3 + ], + valueWithMinMax: [ + 1,2,3 ] ]) bodyMatchers { @@ -594,6 +603,23 @@ class DslToWireMockClientConverterSpec extends Specification { jsonPath('$.dateTime', byTimestamp()) jsonPath('$.time', byTime()) jsonPath('$.list.some.nested.json', byRegex(".*")) + jsonPath('$.valueWithMin', byType { + // results in verification of size of array (min 1) + minOccurrence(1) + }) + jsonPath('$.valueWithMax', byType { + // results in verification of size of array (max 3) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinMax', byType { + // results in verification of size of array (min 1 & max 3) + minOccurrence(1) + maxOccurrence(3) + }) + jsonPath('$.valueWithOccurrence', byType { + // results in verification of size of array (min 4 & max 4) + occurrence(4) + }) } headers { contentType(applicationJson()) @@ -620,6 +646,9 @@ class DslToWireMockClientConverterSpec extends Specification { valueWithMinMax: [ 1,2,3 ], + valueWithOccurrence: [ + 1,2,3,4 + ], ]) bodyMatchers { // asserts the jsonpath value against manual regex @@ -649,6 +678,10 @@ class DslToWireMockClientConverterSpec extends Specification { minOccurrence(1) maxOccurrence(3) }) + jsonPath('$.valueWithOccurrence', byType { + // results in verification of size of array (min 4 & max 4) + occurrence(4) + }) } headers { contentType(applicationJson()) @@ -664,51 +697,60 @@ class DslToWireMockClientConverterSpec extends Specification { ''' { "request" : { - "urlPath" : "/get", - "method" : "POST", - "headers" : { - "Content-Type" : { - "matches" : "application/json.*" - } - }, - "bodyPatterns" : [ { - "matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]" - }, { - "matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]" - }, { - "matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]" - }, { - "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]" - }, { - "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]" - }, { - "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]" - }, { - "matchesJsonPath" : "$[?(@.duck == 123)]" - }, { - "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]" - }, { - "matchesJsonPath" : "$[?(@.alpha == 'abc')]" - }, { - "matchesJsonPath" : "$[?(@.number =~ /(-?(\\\\d*\\\\.\\\\d+|\\\\d+))/)]" - }, { - "matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]" - }, { - "matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]" - }, { - "matchesJsonPath" : "$[?(@.dateTime =~ /(([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]))/)]" - }, { - "matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]" - }, { - "matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]" - } ] + "urlPath" : "/get", + "method" : "POST", + "headers" : { + "Content-Type" : { + "matches" : "application/json.*" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]" + }, { + "matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]" + }, { + "matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]" + }, { + "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]" + }, { + "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]" + }, { + "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]" + }, { + "matchesJsonPath" : "$[?(@.duck == 123)]" + }, { + "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]" + }, { + "matchesJsonPath" : "$[?(@.alpha == 'abc')]" + }, { + "matchesJsonPath" : "$[?(@.number =~ /(-?(\\\\d*\\\\.\\\\d+|\\\\d+))/)]" + }, { + "matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]" + }, { + "matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]" + }, { + "matchesJsonPath" : "$[?(@.dateTime =~ /(([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]))/)]" + }, { + "matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]" + }, { + "matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]" + }, { + "matchesJsonPath" : "$[?(@.valueWithMin.size() >= 1)]" + }, { + "matchesJsonPath" : "$[?(@.valueWithMax.size() <= 3)]" + }, { + "matchesJsonPath" : "$[?(@.valueWithMinMax.size() >= 1 && @.valueWithMinMax.size() <= 3)]" + }, { + "matchesJsonPath" : "$[?(@.valueWithOccurrence.size() >= 4 && @.valueWithOccurrence.size() <= 4)]" + } ] }, "response" : { - "status" : 200, - "body" : "{\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"number\\":123,\\"aBoolean\\":true,\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"valueWithMin\\":[1,2,3],\\"time\\":\\"01:02:34\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3],\\"valueWithoutAMatcher\\":\\"foo\\"}", - "headers" : { - "Content-Type" : "application/json" - } + "status" : 200, + "body" : "{\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"aBoolean\\":true,\\"valueWithMax\\":[1,2,3],\\"valueWithOccurrence\\":[1,2,3,4],\\"number\\":123,\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"valueWithMin\\":[1,2,3],\\"time\\":\\"01:02:34\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMinMax\\":[1,2,3],\\"valueWithoutAMatcher\\":\\"foo\\"}", + "headers" : { + "Content-Type" : "application/json" + }, + "transformers" : [ "response-template" ] } } ''' @@ -744,6 +786,18 @@ class DslToWireMockClientConverterSpec extends Specification { anothervalue: 4 ] ] + ], + valueWithMin: [ + 1,2,3 + ], + valueWithMax: [ + 1,2,3 + ], + valueWithMinMax: [ + 1,2,3 + ], + valueWithOccurrence: [ + 1,2,3,4 ] ])) , String) @@ -768,6 +822,9 @@ class DslToWireMockClientConverterSpec extends Specification { valueWithMinMax: [ 1,2,3 ], + valueWithOccurrence: [ + 1,2,3,4 + ], ]), response.body, false) } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy index c038b52ddc..ba7b7967e8 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy @@ -128,7 +128,9 @@ class ContractsToYaml { request.matchers.body << new YamlContract.BodyStubMatcher( path: matcher.path(), type: stubMatcherType(matcher.matchingType()), - value: matcher.value()?.toString() + value: matcher.value()?.toString(), + minOccurrence: matcher.minTypeOccurrence(), + maxOccurrence: matcher.maxTypeOccurrence(), ) } Object url = contract.request.url?.clientValue diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy index cdbaa232dc..41baa29962 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlContract.groovy @@ -104,6 +104,8 @@ class YamlContract { public StubMatcherType type public String value public PredefinedRegex predefined + public Integer minOccurrence + public Integer maxOccurrence } @CompileStatic @@ -199,7 +201,7 @@ class YamlContract { @CompileStatic static enum StubMatcherType { - by_date, by_time, by_timestamp, by_regex, by_equality, by_null + by_date, by_time, by_timestamp, by_regex, by_equality, by_type, by_null } @CompileStatic diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy index da1058b494..9109d0b2d0 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy @@ -199,6 +199,12 @@ class YamlToContracts { case YamlContract.StubMatcherType.by_equality: value = byEquality() break + case YamlContract.StubMatcherType.by_type: + value = byType { + if (matcher.minOccurrence != null) minOccurrence(matcher.minOccurrence) + if (matcher.maxOccurrence != null) maxOccurrence(matcher.maxOccurrence) + } + break case YamlContract.StubMatcherType.by_null: // do nothing break 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 1a80dd840e..56e994ddbb 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 @@ -142,14 +142,16 @@ class JsonToJsonPathsConverter { static String convertJsonPathAndRegexToAJsonPath(BodyMatcher bodyMatcher, def body = null) { String path = bodyMatcher.path() Object value = bodyMatcher.value() - if (value == null && bodyMatcher.matchingType() != MatchingType.EQUALITY) { + if (value == null && bodyMatcher.matchingType() != MatchingType.EQUALITY && + bodyMatcher.matchingType() != MatchingType.TYPE) { return path } int lastIndexOfDot = lastIndexOfDot(path) String toLastDot = path.substring(0, lastIndexOfDot) String fromLastDot = path.substring(lastIndexOfDot + 1) - String comparison = createComparison(bodyMatcher, value, body) - return "${toLastDot}[?(@.${fromLastDot} ${comparison})]" + String propertyName = "@.${fromLastDot}" + String comparison = createComparison(propertyName, bodyMatcher, value, body) + return "${toLastDot}[?(${comparison})]" } private static int lastIndexOfDot(String path) { @@ -173,7 +175,7 @@ class JsonToJsonPathsConverter { return value } - private static String createComparison(BodyMatcher bodyMatcher, Object value, def body) { + private static String createComparison(String propertyName, BodyMatcher bodyMatcher, Object value, def body) { if (bodyMatcher.matchingType() == MatchingType.EQUALITY) { Object convertedBody = body if (!body) { @@ -185,13 +187,25 @@ class JsonToJsonPathsConverter { } Object retrievedValue = JsonPath.parse(convertedBody).read(bodyMatcher.path()) String wrappedValue = retrievedValue instanceof Number ? retrievedValue : "'${retrievedValue.toString()}'" - return "== ${wrappedValue}" + return "${propertyName} == ${wrappedValue}" } catch (PathNotFoundException e) { throw new IllegalStateException("Value [${bodyMatcher.path()}] not found in JSON [${JsonOutput.toJson(convertedBody)}]", e) } + } else if (bodyMatcher.matchingType() == MatchingType.TYPE) { + Integer min = bodyMatcher.minTypeOccurrence() + Integer max = bodyMatcher.maxTypeOccurrence() + String result = "" + if (min != null) { + result = "${propertyName}.size() >= ${min}" + } + if (max != null) { + String maxResult = "${propertyName}.size() <= ${max}" + result = result ? "${result} && ${maxResult}" : maxResult + } + return result } else { String convertedValue = value.toString().replace('/', '\\\\/') - return "=~ /(${convertedValue})/" + return "${propertyName} =~ /(${convertedValue})/" } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy index 708c0ec7cf..a04907fc56 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy @@ -1192,7 +1192,7 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub where: methodBuilderName | methodBuilder | endOfLineRegexSymbol "JaxRsClientSpockMethodRequestProcessingBodyBuilder"| { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties, generatedClassDataForMethod) } | '\\$' - "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) } | '$' + "JaxRsClientJUnitMethodBodyBuilder" | { org.springframework.cloud.contract.spec.Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, generatedClassDataForMethod) }| '$' } private String stripped(String string) { 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 49627b3a5b..00084f20c4 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 @@ -475,7 +475,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements bodyMatchers { jsonPath('$.items[*].id', byRegex(nonBlank())) jsonPath('$.items[*].title', byRegex(nonBlank())) - jsonPath('$.items[*]', byType { minOccurrence(2); maxOccurrence(2) }) + jsonPath('$.items[*]', byType { occurrence(2) }) } headers { header 'content-type', 'application/...json;charset=UTF-8' } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy index a96636a88e..735dde227d 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverterSpec.groovy @@ -239,6 +239,16 @@ class YamlContractConverterSpec extends Specification { contract.request.bodyMatchers.jsonPathRegexMatchers[8].value() == patterns.isoTime() contract.request.bodyMatchers.jsonPathRegexMatchers[9].path() == "\$.['key'].['complex.key']" contract.request.bodyMatchers.jsonPathRegexMatchers[9].matchingType() == MatchingType.EQUALITY + contract.request.bodyMatchers.jsonPathRegexMatchers[10].path() == '$.valueWithMin' + contract.request.bodyMatchers.jsonPathRegexMatchers[10].matchingType() == MatchingType.TYPE + contract.request.bodyMatchers.jsonPathRegexMatchers[10].minTypeOccurrence() == 1 + contract.request.bodyMatchers.jsonPathRegexMatchers[11].path() == '$.valueWithMax' + contract.request.bodyMatchers.jsonPathRegexMatchers[11].matchingType() == MatchingType.TYPE + contract.request.bodyMatchers.jsonPathRegexMatchers[11].maxTypeOccurrence() == 3 + contract.request.bodyMatchers.jsonPathRegexMatchers[12].path() == '$.valueWithMinMax' + contract.request.bodyMatchers.jsonPathRegexMatchers[12].matchingType() == MatchingType.TYPE + contract.request.bodyMatchers.jsonPathRegexMatchers[12].minTypeOccurrence() == 1 + contract.request.bodyMatchers.jsonPathRegexMatchers[12].maxTypeOccurrence() == 3 contract.request.cookies.entries.find { it.key == "foo" }.clientValue instanceof Pattern contract.request.cookies.entries.find { it.key == "bar" }.serverValue == new ExecutionProperty('equals($it)') and: diff --git a/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml b/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml index 41d0d540ac..a25d12bd73 100644 --- a/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml +++ b/spring-cloud-contract-verifier/src/test/resources/yml/contract_matchers.yml @@ -28,6 +28,20 @@ request: key: "complex.key": 'foo' nullValue: null + valueWithMin: + - 1 + - 2 + - 3 + valueWithMax: + - 1 + - 2 + - 3 + valueWithMinMax: + - 1 + - 2 + - 3 + valueWithMinEmpty: [] + valueWithMaxEmpty: [] matchers: url: regex: /get/[0-9] @@ -90,6 +104,16 @@ request: type: by_equality - path: $.nullvalue type: by_null + - path: $.valueWithMin + type: by_type + minOccurrence: 1 + - path: $.valueWithMax + type: by_type + maxOccurrence: 3 + - path: $.valueWithMinMax + type: by_type + minOccurrence: 1 + maxOccurrence: 3 response: status: 200 cookies: