From bf662357c91828f9298a31e28be662a830ccbe12 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Mon, 15 Jul 2024 16:49:01 +1000 Subject: [PATCH] feat: Add DSL methods to handle matching each key and value in a JSON object #1813 --- .../pact/consumer/junit5/EachKeyLikeTest.java | 25 ++++++++++--- .../pact/consumer/dsl/LambdaDslObject.java | 22 ++++++++++++ .../dius/pact/consumer/dsl/PactDslJsonBody.kt | 28 +++++++++++++++ .../consumer/dsl/PactDslJsonBodySpec.groovy | 36 +++++++++++++++++++ .../dius/pact/core/model/PathExpressions.kt | 2 +- .../core/model/PathExpressionsSpec.groovy | 1 + 6 files changed, 109 insertions(+), 5 deletions(-) diff --git a/consumer/junit5/src/test/java/au/com/dius/pact/consumer/junit5/EachKeyLikeTest.java b/consumer/junit5/src/test/java/au/com/dius/pact/consumer/junit5/EachKeyLikeTest.java index ffac88dc6..92a9b1ec4 100644 --- a/consumer/junit5/src/test/java/au/com/dius/pact/consumer/junit5/EachKeyLikeTest.java +++ b/consumer/junit5/src/test/java/au/com/dius/pact/consumer/junit5/EachKeyLikeTest.java @@ -1,7 +1,7 @@ package au.com.dius.pact.consumer.junit5; import au.com.dius.pact.consumer.MockServer; -import au.com.dius.pact.consumer.dsl.PactDslJsonRootValue; +import au.com.dius.pact.consumer.dsl.Matchers; import au.com.dius.pact.consumer.dsl.PactDslWithProvider; import au.com.dius.pact.core.model.PactSpecVersion; import au.com.dius.pact.core.model.V4Pact; @@ -32,8 +32,8 @@ public V4Pact createFragment(PactDslWithProvider builder) { .method("POST") .body(newJsonBody(body -> body.object("a", aObj -> { - aObj.eachKeyLike("prop1", PactDslJsonRootValue.stringMatcher("prop\\d+", "prop1")); - aObj.eachKeyLike("prop1", propObj -> propObj.stringType("value", "x")); + aObj.eachKeyMatching(Matchers.regexp("prop\\d+", "prop1")); + aObj.eachValueMatching("prop1", propObj -> propObj.stringType("value", "x")); })).build()) .willRespondWith() .status(200) @@ -47,7 +47,7 @@ void runTest(MockServer mockServer) throws IOException { " \"prop1\": {\n" + " \"value\": \"x\"\n" + " },\n" + - " \"prop\": {\n" + + " \"prop2\": {\n" + " \"value\": \"y\"\n" + " }\n" + " }\n" + @@ -57,5 +57,22 @@ void runTest(MockServer mockServer) throws IOException { .execute() .returnResponse(); assertThat(httpResponse.getCode(), is(200)); + +// This should make the test fail +// String json2 = "{\n" + +// " \"a\": {\n" + +// " \"prop1\": {\n" + +// " \"value\": \"x\"\n" + +// " },\n" + +// " \"prop\": {\n" + +// " \"value\": \"y\"\n" + +// " }\n" + +// " }\n" + +// "}"; +// ClassicHttpResponse httpResponse2 = (ClassicHttpResponse) Request.post(mockServer.getUrl()) +// .body(new StringEntity(json2, ContentType.APPLICATION_JSON)) +// .execute() +// .returnResponse(); +// assertThat(httpResponse2.getCode(), is(500)); } } diff --git a/consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDslObject.java b/consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDslObject.java index d2be00b16..c0d5a7c0c 100644 --- a/consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDslObject.java +++ b/consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDslObject.java @@ -1143,4 +1143,26 @@ public LambdaDslObject arrayContaining(String name, Consumer arrayContaining.closeArray(); return this; } + + /** + * Configures a matching rule for each key in the object. + * @param matcher Matcher to apply to each key + */ + public LambdaDslObject eachKeyMatching(Matcher matcher) { + object.eachKeyMatching(matcher); + return this; + } + + /** + * Configures a matching rule for each value in the object, ignoring the keys. + * @param exampleKey Example key to use in the consumer test. + * @param nestedObject Nested object to match each value to. + */ + public LambdaDslObject eachValueMatching(String exampleKey, final Consumer nestedObject) { + final PactDslJsonBody objectLike = object.eachValueMatching(exampleKey); + final LambdaDslObject dslObject = new LambdaDslObject(objectLike); + nestedObject.accept(dslObject); + objectLike.closeObject(); + return this; + } } diff --git a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactDslJsonBody.kt b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactDslJsonBody.kt index e297457ec..f0cd32bef 100755 --- a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactDslJsonBody.kt +++ b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactDslJsonBody.kt @@ -16,6 +16,7 @@ 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 au.com.dius.pact.core.model.matchingrules.EachKeyMatcher import au.com.dius.pact.core.model.matchingrules.EqualsIgnoreOrderMatcher import au.com.dius.pact.core.model.matchingrules.EqualsMatcher import au.com.dius.pact.core.model.matchingrules.MatchingRule @@ -28,6 +29,7 @@ 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.TypeMatcher import au.com.dius.pact.core.model.matchingrules.ValuesMatcher +import au.com.dius.pact.core.model.matchingrules.expressions.MatchingRuleDefinition import au.com.dius.pact.core.support.Json.toJson import au.com.dius.pact.core.support.expressions.DataType.Companion.from import au.com.dius.pact.core.support.json.JsonValue @@ -2274,4 +2276,30 @@ open class PactDslJsonBody : DslPart { else -> body } } + + /** + * Applies a matching rule to each key in the object, ignoring the values. + */ + fun eachKeyMatching(matcher: Matcher): PactDslJsonBody { + val path = if (rootPath.endsWith(".")) rootPath.substring(0, rootPath.length - 1) else rootPath + val value = matcher.value.toString() + if (matcher.matcher != null) { + matchers.addRule(path, EachKeyMatcher(MatchingRuleDefinition(value, matcher.matcher!!, matcher.generator))) + } + if (!body.has(value)) { + when (val body = body) { + is JsonValue.Object -> body.add(value, JsonValue.Null) + else -> {} + } + } + return this + } + + /** + * Applies matching rules to each value in the object, ignoring the keys. + */ + fun eachValueMatching(exampleKey: String): PactDslJsonBody { + val path = constructValidPath("*", rootPath) + return PactDslJsonBody("$path.", exampleKey, this) + } } diff --git a/consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslJsonBodySpec.groovy b/consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslJsonBodySpec.groovy index 5bd8c806d..9237b89be 100644 --- a/consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslJsonBodySpec.groovy +++ b/consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslJsonBodySpec.groovy @@ -2,6 +2,7 @@ package au.com.dius.pact.consumer.dsl import au.com.dius.pact.core.model.PactSpecVersion import au.com.dius.pact.core.model.matchingrules.ArrayContainsMatcher +import au.com.dius.pact.core.model.matchingrules.EachKeyMatcher import au.com.dius.pact.core.model.matchingrules.MatchingRuleCategory import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher @@ -10,6 +11,7 @@ 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.TypeMatcher import au.com.dius.pact.core.model.matchingrules.ValuesMatcher +import au.com.dius.pact.core.model.matchingrules.expressions.MatchingRuleDefinition import kotlin.Triple import spock.lang.Issue import spock.lang.Specification @@ -435,4 +437,38 @@ class PactDslJsonBodySpec extends Specification { new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER), new RegexMatcher('\\d{5}', '90210')]) } + + @Issue('#1813') + def 'matching each key'() { + when: + PactDslJsonBody body = new PactDslJsonBody() + .object('test') + .eachKeyMatching(Matchers.regexp('\\d+\\.\\d{2}', '2.01')) + .closeObject() + body.closeObject() + + then: + body.toString() == '{"test":{"2.01":null}}' + body.matchers.matchingRules.keySet() == ['$.test'] as Set + body.matchers.matchingRules['$.test'] == new MatchingRuleGroup([ + new EachKeyMatcher(new MatchingRuleDefinition('2.01', new RegexMatcher('\\d+\\.\\d{2}', '2.01'), null)) + ]) + } + + @Issue('#1813') + def 'matching each value'() { + when: + PactDslJsonBody body = new PactDslJsonBody() + .eachValueMatching('prop1') + .stringType('value', 'x') + .closeObject() + body.closeObject() + + then: + body.toString() == '{"prop1":{"value":"x"}}' + body.matchers.matchingRules.keySet() == ['$.*.value'] as Set + body.matchers.matchingRules['$.*.value'] == new MatchingRuleGroup([ + TypeMatcher.INSTANCE + ]) + } } diff --git a/core/model/src/main/kotlin/au/com/dius/pact/core/model/PathExpressions.kt b/core/model/src/main/kotlin/au/com/dius/pact/core/model/PathExpressions.kt index 347a93263..2c3c8d4ef 100755 --- a/core/model/src/main/kotlin/au/com/dius/pact/core/model/PathExpressions.kt +++ b/core/model/src/main/kotlin/au/com/dius/pact/core/model/PathExpressions.kt @@ -210,7 +210,7 @@ fun constructValidPath(segment: String, rootPath: String): String { val root = StringUtils.stripEnd(rootPath, ".") if (segment.all { it.isDigit() }) { "$root[$segment]" - } else if (segment.any { !validPathCharacter(it) }) { + } else if (segment != "*" && segment.any { !validPathCharacter(it) }) { "$root['$segment']" } else { "$root.$segment" diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/PathExpressionsSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/PathExpressionsSpec.groovy index 457245be5..63648fd8f 100644 --- a/core/model/src/test/groovy/au/com/dius/pact/core/model/PathExpressionsSpec.groovy +++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/PathExpressionsSpec.groovy @@ -183,6 +183,7 @@ class PathExpressionsSpec extends Specification { 'a$' | 'a.b' || "a.b['a\$']" 'a b' | 'a.b' || "a.b['a b']" '$a.b' | 'a.b' || "a.b['\$a.b']" + '*' | 'a.b' || 'a.b.*' } def 'construct path from tokens'() {