Skip to content

Commit 2bb8eef

Browse files
committed
feat: Add DSL methods to handle matching each key and value in a JSON object #1813
1 parent 3030270 commit 2bb8eef

File tree

6 files changed

+109
-5
lines changed

6 files changed

+109
-5
lines changed

consumer/junit5/src/test/java/au/com/dius/pact/consumer/junit5/EachKeyLikeTest.java

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package au.com.dius.pact.consumer.junit5;
22

33
import au.com.dius.pact.consumer.MockServer;
4-
import au.com.dius.pact.consumer.dsl.PactDslJsonRootValue;
4+
import au.com.dius.pact.consumer.dsl.Matchers;
55
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
66
import au.com.dius.pact.core.model.PactSpecVersion;
77
import au.com.dius.pact.core.model.V4Pact;
@@ -32,8 +32,8 @@ public V4Pact createFragment(PactDslWithProvider builder) {
3232
.method("POST")
3333
.body(newJsonBody(body ->
3434
body.object("a", aObj -> {
35-
aObj.eachKeyLike("prop1", PactDslJsonRootValue.stringMatcher("prop\\d+", "prop1"));
36-
aObj.eachKeyLike("prop1", propObj -> propObj.stringType("value", "x"));
35+
aObj.eachKeyMatching(Matchers.regexp("prop\\d+", "prop1"));
36+
aObj.eachValueMatching("prop1", propObj -> propObj.stringType("value", "x"));
3737
})).build())
3838
.willRespondWith()
3939
.status(200)
@@ -47,7 +47,7 @@ void runTest(MockServer mockServer) throws IOException {
4747
" \"prop1\": {\n" +
4848
" \"value\": \"x\"\n" +
4949
" },\n" +
50-
" \"prop\": {\n" +
50+
" \"prop2\": {\n" +
5151
" \"value\": \"y\"\n" +
5252
" }\n" +
5353
" }\n" +
@@ -57,5 +57,22 @@ void runTest(MockServer mockServer) throws IOException {
5757
.execute()
5858
.returnResponse();
5959
assertThat(httpResponse.getCode(), is(200));
60+
61+
// This should make the test fail
62+
// String json2 = "{\n" +
63+
// " \"a\": {\n" +
64+
// " \"prop1\": {\n" +
65+
// " \"value\": \"x\"\n" +
66+
// " },\n" +
67+
// " \"prop\": {\n" +
68+
// " \"value\": \"y\"\n" +
69+
// " }\n" +
70+
// " }\n" +
71+
// "}";
72+
// ClassicHttpResponse httpResponse2 = (ClassicHttpResponse) Request.post(mockServer.getUrl())
73+
// .body(new StringEntity(json2, ContentType.APPLICATION_JSON))
74+
// .execute()
75+
// .returnResponse();
76+
// assertThat(httpResponse2.getCode(), is(500));
6077
}
6178
}

consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDslObject.java

+22
Original file line numberDiff line numberDiff line change
@@ -1143,4 +1143,26 @@ public LambdaDslObject arrayContaining(String name, Consumer<LambdaDslJsonArray>
11431143
arrayContaining.closeArray();
11441144
return this;
11451145
}
1146+
1147+
/**
1148+
* Configures a matching rule for each key in the object.
1149+
* @param matcher Matcher to apply to each key
1150+
*/
1151+
public LambdaDslObject eachKeyMatching(Matcher matcher) {
1152+
object.eachKeyMatching(matcher);
1153+
return this;
1154+
}
1155+
1156+
/**
1157+
* Configures a matching rule for each value in the object, ignoring the keys.
1158+
* @param exampleKey Example key to use in the consumer test.
1159+
* @param nestedObject Nested object to match each value to.
1160+
*/
1161+
public LambdaDslObject eachValueMatching(String exampleKey, final Consumer<LambdaDslObject> nestedObject) {
1162+
final PactDslJsonBody objectLike = object.eachValueMatching(exampleKey);
1163+
final LambdaDslObject dslObject = new LambdaDslObject(objectLike);
1164+
nestedObject.accept(dslObject);
1165+
objectLike.closeObject();
1166+
return this;
1167+
}
11461168
}

consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/PactDslJsonBody.kt

+28
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import au.com.dius.pact.core.model.generators.RandomStringGenerator
1616
import au.com.dius.pact.core.model.generators.RegexGenerator
1717
import au.com.dius.pact.core.model.generators.TimeGenerator
1818
import au.com.dius.pact.core.model.generators.UuidGenerator
19+
import au.com.dius.pact.core.model.matchingrules.EachKeyMatcher
1920
import au.com.dius.pact.core.model.matchingrules.EqualsIgnoreOrderMatcher
2021
import au.com.dius.pact.core.model.matchingrules.EqualsMatcher
2122
import au.com.dius.pact.core.model.matchingrules.MatchingRule
@@ -28,6 +29,7 @@ import au.com.dius.pact.core.model.matchingrules.RegexMatcher
2829
import au.com.dius.pact.core.model.matchingrules.RuleLogic
2930
import au.com.dius.pact.core.model.matchingrules.TypeMatcher
3031
import au.com.dius.pact.core.model.matchingrules.ValuesMatcher
32+
import au.com.dius.pact.core.model.matchingrules.expressions.MatchingRuleDefinition
3133
import au.com.dius.pact.core.support.Json.toJson
3234
import au.com.dius.pact.core.support.expressions.DataType.Companion.from
3335
import au.com.dius.pact.core.support.json.JsonValue
@@ -2231,4 +2233,30 @@ open class PactDslJsonBody : DslPart {
22312233
else -> body
22322234
}
22332235
}
2236+
2237+
/**
2238+
* Applies a matching rule to each key in the object, ignoring the values.
2239+
*/
2240+
fun eachKeyMatching(matcher: Matcher): PactDslJsonBody {
2241+
val path = if (rootPath.endsWith(".")) rootPath.substring(0, rootPath.length - 1) else rootPath
2242+
val value = matcher.value.toString()
2243+
if (matcher.matcher != null) {
2244+
matchers.addRule(path, EachKeyMatcher(MatchingRuleDefinition(value, matcher.matcher!!, matcher.generator)))
2245+
}
2246+
if (!body.has(value)) {
2247+
when (val body = body) {
2248+
is JsonValue.Object -> body.add(value, JsonValue.Null)
2249+
else -> {}
2250+
}
2251+
}
2252+
return this
2253+
}
2254+
2255+
/**
2256+
* Applies matching rules to each value in the object, ignoring the keys.
2257+
*/
2258+
fun eachValueMatching(exampleKey: String): PactDslJsonBody {
2259+
val path = constructValidPath("*", rootPath)
2260+
return PactDslJsonBody("$path.", exampleKey, this)
2261+
}
22342262
}

consumer/src/test/groovy/au/com/dius/pact/consumer/dsl/PactDslJsonBodySpec.groovy

+36
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package au.com.dius.pact.consumer.dsl
22

33
import au.com.dius.pact.core.model.PactSpecVersion
44
import au.com.dius.pact.core.model.matchingrules.ArrayContainsMatcher
5+
import au.com.dius.pact.core.model.matchingrules.EachKeyMatcher
56
import au.com.dius.pact.core.model.matchingrules.MatchingRuleCategory
67
import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup
78
import au.com.dius.pact.core.model.matchingrules.MinTypeMatcher
@@ -10,6 +11,7 @@ import au.com.dius.pact.core.model.matchingrules.RegexMatcher
1011
import au.com.dius.pact.core.model.matchingrules.RuleLogic
1112
import au.com.dius.pact.core.model.matchingrules.TypeMatcher
1213
import au.com.dius.pact.core.model.matchingrules.ValuesMatcher
14+
import au.com.dius.pact.core.model.matchingrules.expressions.MatchingRuleDefinition
1315
import kotlin.Triple
1416
import spock.lang.Issue
1517
import spock.lang.Specification
@@ -435,4 +437,38 @@ class PactDslJsonBodySpec extends Specification {
435437
new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER),
436438
new RegexMatcher('\\d{5}', '90210')])
437439
}
440+
441+
@Issue('#1813')
442+
def 'matching each key'() {
443+
when:
444+
PactDslJsonBody body = new PactDslJsonBody()
445+
.object('test')
446+
.eachKeyMatching(Matchers.regexp('\\d+\\.\\d{2}', '2.01'))
447+
.closeObject()
448+
body.closeObject()
449+
450+
then:
451+
body.toString() == '{"test":{"2.01":null}}'
452+
body.matchers.matchingRules.keySet() == ['$.test'] as Set
453+
body.matchers.matchingRules['$.test'] == new MatchingRuleGroup([
454+
new EachKeyMatcher(new MatchingRuleDefinition('2.01', new RegexMatcher('\\d+\\.\\d{2}', '2.01'), null))
455+
])
456+
}
457+
458+
@Issue('#1813')
459+
def 'matching each value'() {
460+
when:
461+
PactDslJsonBody body = new PactDslJsonBody()
462+
.eachValueMatching('prop1')
463+
.stringType('value', 'x')
464+
.closeObject()
465+
body.closeObject()
466+
467+
then:
468+
body.toString() == '{"prop1":{"value":"x"}}'
469+
body.matchers.matchingRules.keySet() == ['$.*.value'] as Set
470+
body.matchers.matchingRules['$.*.value'] == new MatchingRuleGroup([
471+
TypeMatcher.INSTANCE
472+
])
473+
}
438474
}

core/model/src/main/kotlin/au/com/dius/pact/core/model/PathExpressions.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ fun constructValidPath(segment: String, rootPath: String): String {
210210
val root = StringUtils.stripEnd(rootPath, ".")
211211
if (segment.all { it.isDigit() }) {
212212
"$root[$segment]"
213-
} else if (segment.any { !validPathCharacter(it) }) {
213+
} else if (segment != "*" && segment.any { !validPathCharacter(it) }) {
214214
"$root['$segment']"
215215
} else {
216216
"$root.$segment"

core/model/src/test/groovy/au/com/dius/pact/core/model/PathExpressionsSpec.groovy

+1
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ class PathExpressionsSpec extends Specification {
183183
'a$' | 'a.b' || "a.b['a\$']"
184184
'a b' | 'a.b' || "a.b['a b']"
185185
'$a.b' | 'a.b' || "a.b['\$a.b']"
186+
'*' | 'a.b' || 'a.b.*'
186187
}
187188

188189
def 'construct path from tokens'() {

0 commit comments

Comments
 (0)