From 9d46d36f2fe9649ad8e964f0850194405586e5b1 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Wed, 22 May 2024 11:50:39 +1000 Subject: [PATCH] feat: Allow reusing common DSL parts in different LambdaDslJsonBody objects #1796 --- .../com/dius/pact/consumer/dsl/LambdaDsl.java | 12 +++++ .../com/dius/pact/consumer/dsl/Extensions.kt | 10 ++++ .../dius/pact/consumer/dsl/PactDslJsonBody.kt | 21 ++++++++ .../consumer/dsl/LambdaDslObjectTest.java | 48 ++++++++++++++++++- .../dius/pact/consumer/dsl/ExtensionsTest.kt | 47 ++++++++++++++++++ .../dius/pact/core/support/json/JsonValue.kt | 32 +++++++++++-- 6 files changed, 165 insertions(+), 5 deletions(-) diff --git a/consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDsl.java b/consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDsl.java index 8259612151..459d1313d3 100644 --- a/consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDsl.java +++ b/consumer/src/main/java/au/com/dius/pact/consumer/dsl/LambdaDsl.java @@ -116,4 +116,16 @@ public static LambdaDslJsonBody newJsonBody(Consumer array) { array.accept(dslBody); return dslBody; } + + /** + * DSL function to simplify creating a {@link DslPart} generated from a {@link LambdaDslJsonBody}. This takes a + * base template to copy the attributes from. + */ + public static LambdaDslJsonBody newJsonBody(LambdaDslJsonBody baseTemplate, Consumer array) { + final PactDslJsonBody pactDslJsonBody = new PactDslJsonBody(); + pactDslJsonBody.extendFrom((PactDslJsonBody) baseTemplate.build()); + final LambdaDslJsonBody dslBody = new LambdaDslJsonBody(pactDslJsonBody); + array.accept(dslBody); + return dslBody; + } } diff --git a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/Extensions.kt b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/Extensions.kt index 4075915321..f046aadcab 100644 --- a/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/Extensions.kt +++ b/consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/Extensions.kt @@ -17,6 +17,16 @@ fun newJsonObject(kClass: KClass<*>): DslPart { return LambdaDsl.newJsonBody(DslJsonBodyBuilder().basedOnRequiredConstructorFields(kClass)).build() } +/** + * DSL function to simplify creating a [DslPart] generated from a [LambdaDslJsonBody]. The new object is + * extended from a base template object. + */ +fun newJsonObject(baseTemplate: DslPart, function: LambdaDslJsonBody.() -> Unit): DslPart { + require(baseTemplate is PactDslJsonBody) { "baseTemplate must be a PactDslJsonBody" } + val dslBody = LambdaDslJsonBody(baseTemplate.asBody()) + return LambdaDsl.newJsonBody(dslBody) { it.function() }.build() +} + /** * DSL function to simplify creating a [DslPart] generated from a [LambdaDslJsonArray]. */ 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 e0df2681fe..0aadd08980 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 @@ -2191,4 +2191,25 @@ open class PactDslJsonBody : DslPart { override fun arrayContaining(name: String): DslPart { return PactDslJsonArrayContaining(rootPath, name, this) } + + /** + * Extends this JSON object from a base template. + */ + fun extendFrom(baseTemplate: PactDslJsonBody) { + this.body = copyBody(baseTemplate.body) + matchers = baseTemplate.matchers.copyWithUpdatedMatcherRootPrefix("") + generators = baseTemplate.generators.copyWithUpdatedMatcherRootPrefix("") + } + + // TODO: Replace this with JsonValue.copy in the next major version + private fun copyBody(body: JsonValue): JsonValue { + return when (body) { + is JsonValue.Array -> JsonValue.Array(body.values.map { it.copy() }.toMutableList()) + is JsonValue.Decimal -> JsonValue.Decimal(body.value.chars) + is JsonValue.Integer -> JsonValue.Integer(body.value.chars) + is JsonValue.Object -> JsonValue.Object(body.entries.mapValues { it.value.copy() }.toMutableMap()) + is JsonValue.StringValue -> JsonValue.StringValue(body.value.chars) + else -> body + } + } } diff --git a/consumer/src/test/java/au/com/dius/pact/consumer/dsl/LambdaDslObjectTest.java b/consumer/src/test/java/au/com/dius/pact/consumer/dsl/LambdaDslObjectTest.java index 36bd67aa3e..c369e78413 100644 --- a/consumer/src/test/java/au/com/dius/pact/consumer/dsl/LambdaDslObjectTest.java +++ b/consumer/src/test/java/au/com/dius/pact/consumer/dsl/LambdaDslObjectTest.java @@ -1,6 +1,11 @@ package au.com.dius.pact.consumer.dsl; import au.com.dius.pact.core.model.PactSpecVersion; +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.NumberTypeMatcher; +import au.com.dius.pact.core.model.matchingrules.NullMatcher; +import au.com.dius.pact.core.model.matchingrules.TypeMatcher; import org.junit.jupiter.api.Test; import java.time.LocalDateTime; @@ -16,7 +21,6 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; @@ -1070,4 +1074,46 @@ public void testArrayContains() { Map variant2Matcher = (Map) ((List) map2.get("matchers")).get(0); assertThat(variant2Matcher, hasEntry("match", "number")); } + + // Issue #1796 + @Test + public void allowDslToBExtendedFromACommonBase() { + LambdaDslJsonBody base = newJsonBody(o -> { + o.stringType("a", "foo"); + o.id("b", 0L); + o.integerType("c", 0); + o.booleanType("d", false); + }); + LambdaDslJsonBody y = newJsonBody(base, o -> { + o.stringType("e", "bar"); + }); + LambdaDslJsonBody z = newJsonBody(base, o -> { + o.nullValue("e"); + }); + + String expectedY = "{\"a\":\"foo\",\"b\":0,\"c\":0,\"d\":false,\"e\":\"bar\"}"; + Map yRules = Map.of( + "$.a", new MatchingRuleGroup(List.of(TypeMatcher.INSTANCE)), + "$.b", new MatchingRuleGroup(List.of(TypeMatcher.INSTANCE)), + "$.c", new MatchingRuleGroup(List.of(new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))), + "$.d", new MatchingRuleGroup(List.of(TypeMatcher.INSTANCE)), + "$.e", new MatchingRuleGroup(List.of(TypeMatcher.INSTANCE)) + ); + MatchingRuleCategory expectedYMatchers = new MatchingRuleCategory("body", yRules); + String expectedZ = "{\"a\":\"foo\",\"b\":0,\"c\":0,\"d\":false,\"e\":null}"; + Map zRules = Map.of( + "$.a", new MatchingRuleGroup(List.of(TypeMatcher.INSTANCE)), + "$.b", new MatchingRuleGroup(List.of(TypeMatcher.INSTANCE)), + "$.c", new MatchingRuleGroup(List.of(new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))), + "$.d", new MatchingRuleGroup(List.of(TypeMatcher.INSTANCE)) + ); + MatchingRuleCategory expectedZMatchers = new MatchingRuleCategory("body", zRules); + + DslPart yPart = y.build(); + assertThat(yPart.getBody().toString(), is(expectedY)); + assertThat(yPart.getMatchers(), is(expectedYMatchers)); + DslPart zPart = z.build(); + assertThat(zPart.getBody().toString(), is(expectedZ)); + assertThat(zPart.getMatchers(), is(expectedZMatchers)); + } } diff --git a/consumer/src/test/kotlin/au/com/dius/pact/consumer/dsl/ExtensionsTest.kt b/consumer/src/test/kotlin/au/com/dius/pact/consumer/dsl/ExtensionsTest.kt index 968278b793..8468a316f5 100644 --- a/consumer/src/test/kotlin/au/com/dius/pact/consumer/dsl/ExtensionsTest.kt +++ b/consumer/src/test/kotlin/au/com/dius/pact/consumer/dsl/ExtensionsTest.kt @@ -1,8 +1,15 @@ package au.com.dius.pact.consumer.dsl +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.NumberTypeMatcher +import au.com.dius.pact.core.model.matchingrules.TypeMatcher +import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.junit.jupiter.api.Test +import java.util.List +import java.util.Map class ExtensionsTest { @Test @@ -51,4 +58,44 @@ class ExtensionsTest { assertThat(actualJson, equalTo(expectedJson)) } + + // Issue #1796 + @Test + fun `allow dsl to be extended from a common base`() { + val x = newJsonObject { + stringType("a", "foo") + id("b", 0L) + integerType("c", 0) + booleanType("d", false) + } + val y = newJsonObject(x) { + stringType("e", "bar") + } + val z = newJsonObject(x) { + nullValue("e") + } + + val expectedY = "{\"a\":\"foo\",\"b\":0,\"c\":0,\"d\":false,\"e\":\"bar\"}" + val yRules = Map.of( + "$.a", MatchingRuleGroup(List.of(TypeMatcher)), + "$.b", MatchingRuleGroup(List.of(TypeMatcher)), + "$.c", MatchingRuleGroup(List.of(NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))), + "$.d", MatchingRuleGroup(List.of(TypeMatcher)), + "$.e", MatchingRuleGroup(List.of(TypeMatcher)) + ) + val expectedYMatchers = MatchingRuleCategory("body", yRules) + val expectedZ = "{\"a\":\"foo\",\"b\":0,\"c\":0,\"d\":false,\"e\":null}" + val zRules = Map.of( + "$.a", MatchingRuleGroup(List.of(TypeMatcher)), + "$.b", MatchingRuleGroup(List.of(TypeMatcher)), + "$.c", MatchingRuleGroup(List.of(NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))), + "$.d", MatchingRuleGroup(List.of(TypeMatcher)) + ) + val expectedZMatchers = MatchingRuleCategory("body", zRules) + + assertThat(y.body.toString(), CoreMatchers.`is`(expectedY)) + assertThat(y.matchers, CoreMatchers.`is`(expectedYMatchers)) + assertThat(z.body.toString(), CoreMatchers.`is`(expectedZ)) + assertThat(z.matchers, CoreMatchers.`is`(expectedZMatchers)) + } } diff --git a/core/support/src/main/kotlin/au/com/dius/pact/core/support/json/JsonValue.kt b/core/support/src/main/kotlin/au/com/dius/pact/core/support/json/JsonValue.kt index 0e73a452d8..bc81591d14 100644 --- a/core/support/src/main/kotlin/au/com/dius/pact/core/support/json/JsonValue.kt +++ b/core/support/src/main/kotlin/au/com/dius/pact/core/support/json/JsonValue.kt @@ -7,12 +7,16 @@ sealed class JsonValue { constructor(value: CharArray) : this(JsonToken.Integer(value)) constructor(value: Int) : this(JsonToken.Integer(value.toString().toCharArray())) fun toBigInteger() = String(this.value.chars).toBigInteger() + + override fun copy() = Integer(value.chars) } class Decimal(val value: JsonToken.Decimal) : JsonValue() { constructor(value: CharArray) : this(JsonToken.Decimal(value)) constructor(value: Number) : this(JsonToken.Decimal(value.toString().toCharArray())) fun toBigDecimal() = String(this.value.chars).toBigDecimal() + + override fun copy() = Decimal(value.chars) } class StringValue(val value: JsonToken.StringValue) : JsonValue() { @@ -34,11 +38,21 @@ sealed class JsonValue { result = 31 * result + value.hashCode() return result } + + override fun copy() = StringValue(value.chars) + } + + object True : JsonValue() { + override fun copy() = True + } + + object False : JsonValue() { + override fun copy() = False } - object True : JsonValue() - object False : JsonValue() - object Null : JsonValue() + object Null : JsonValue() { + override fun copy() = Null + } class Array @JvmOverloads constructor (val values: MutableList = mutableListOf()) : JsonValue() { fun find(function: (JsonValue) -> Boolean) = values.find(function) @@ -69,14 +83,19 @@ sealed class JsonValue { } companion object { - fun of(vararg value: JsonValue) = JsonValue.Array(value.toMutableList()) + fun of(vararg value: JsonValue) = Array(value.toMutableList()) } + + override fun copy() = Array(values.map { it.copy() }.toMutableList()) } class Object @JvmOverloads constructor (val entries: MutableMap = mutableMapOf()) : JsonValue() { constructor(vararg values: Pair) : this(values.associate { it }.toMutableMap()) operator fun get(name: String) = entries[name] ?: Null override fun has(field: String) = entries.containsKey(field) + + override fun copy() = Object(entries.mapValues { it.value.copy() }.toMutableMap()) + operator fun set(key: String, value: Any?) { entries[key] = Json.toJson(value) } @@ -313,6 +332,11 @@ sealed class JsonValue { throw UnsupportedOperationException("Can not downcast ${this.name} to type ${T::class}") } } + + /** + * Makes a copy of this JSON value + */ + abstract fun copy(): JsonValue } fun JsonValue?.map(transform: (JsonValue) -> R): List = when {