diff --git a/consumer/build.gradle b/consumer/build.gradle index 74256dbd8b..01a8106bb8 100644 --- a/consumer/build.gradle +++ b/consumer/build.gradle @@ -2,6 +2,7 @@ dependencies { api project(path: ":core:model", configuration: 'default') api project(path: ":core:matchers", configuration: 'default') + compile 'com.googlecode.java-diff-utils:diffutils:1.3.0', 'dk.brics.automaton:automaton:1.11-8', "org.apache.httpcomponents:httpclient:${project.httpClientVersion}" diff --git a/consumer/java8/build.gradle b/consumer/java8/build.gradle index a0332c2af9..102937eb00 100644 --- a/consumer/java8/build.gradle +++ b/consumer/java8/build.gradle @@ -1,6 +1,8 @@ dependencies { api project(path: ":consumer", configuration: 'default') + implementation "org.apache.commons:commons-lang3:${project.commonsLang3Version}" + testImplementation "org.junit.jupiter:junit-jupiter:${project.junit5Version}" testImplementation "org.junit.jupiter:junit-jupiter-api:${project.junit5Version}" testRuntime "ch.qos.logback:logback-classic:${project.logbackVersion}" diff --git a/consumer/java8/src/main/kotlin/io/pactfoundation/consumer/dsl/DslJsonBodyBuilder.kt b/consumer/java8/src/main/kotlin/io/pactfoundation/consumer/dsl/DslJsonBodyBuilder.kt new file mode 100644 index 0000000000..a30eb23dd0 --- /dev/null +++ b/consumer/java8/src/main/kotlin/io/pactfoundation/consumer/dsl/DslJsonBodyBuilder.kt @@ -0,0 +1,57 @@ +package io.pactfoundation.consumer.dsl + +import org.apache.commons.lang3.time.DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT +import java.time.ZonedDateTime +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.jvmErasure + + +class DslJsonBodyBuilder { + companion object { + private val ISO_PATTERN = ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.pattern + } + + /** + * Build a {@link LambdaDslJsonBody} based on the Data Object required constructor fields + */ + fun basedOnRequiredConstructorFields(kClass: KClass<*>): (LambdaDslJsonBody) -> Unit = + { root: LambdaDslJsonBody -> + root.run { + val constructor = kClass.primaryConstructor + fillBasedOnConstructorFields(constructor, root) + } + } + + private fun fillBasedOnConstructorFields( + constructor: KFunction?, + root: LambdaDslObject + ) { + constructor?.parameters?.filterNot { it.isOptional }?.forEach { + when (val baseField = it.type.jvmErasure) { + String::class -> root.stringType(it.name) + Boolean::class -> root.booleanType(it.name) + Byte::class, + Short::class, + Int::class, + Long::class, + Float::class, + Number::class, + Double::class -> + root.numberType(it.name) + List::class -> root.array(it.name) {} + ZonedDateTime::class -> root.datetime(it.name, ISO_PATTERN) + else -> + root.`object`(it.name) { objDsl -> + objDsl.run { + fillBasedOnConstructorFields( + baseField.primaryConstructor, + objDsl + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/consumer/java8/src/main/kotlin/io/pactfoundation/consumer/dsl/Extensions.kt b/consumer/java8/src/main/kotlin/io/pactfoundation/consumer/dsl/Extensions.kt index 8a9b2a47d3..88617ead2f 100644 --- a/consumer/java8/src/main/kotlin/io/pactfoundation/consumer/dsl/Extensions.kt +++ b/consumer/java8/src/main/kotlin/io/pactfoundation/consumer/dsl/Extensions.kt @@ -2,6 +2,8 @@ package io.pactfoundation.consumer.dsl import au.com.dius.pact.consumer.dsl.DslPart +import kotlin.reflect.KClass + /** * DSL function to simplify creating a [DslPart] generated from a [LambdaDslJsonBody]. */ @@ -9,6 +11,13 @@ fun newJsonObject(body: LambdaDslJsonBody.() -> Unit): DslPart { return LambdaDsl.newJsonBody { it.body() }.build() } +/** + * DSL function to simplify creating a [DslPart] generated from a [LambdaDslJsonBody]. + */ +fun newJsonObject(kClass: KClass<*>): DslPart { + return LambdaDsl.newJsonBody(DslJsonBodyBuilder().basedOnRequiredConstructorFields(kClass)).build() +} + /** * DSL function to simplify creating a [DslPart] generated from a [LambdaDslJsonArray]. */ diff --git a/consumer/java8/src/test/kotlin/io/pactfoundation/consumer/dsl/DslJsonBodyBuilderTest.kt b/consumer/java8/src/test/kotlin/io/pactfoundation/consumer/dsl/DslJsonBodyBuilderTest.kt new file mode 100644 index 0000000000..07673bddc8 --- /dev/null +++ b/consumer/java8/src/test/kotlin/io/pactfoundation/consumer/dsl/DslJsonBodyBuilderTest.kt @@ -0,0 +1,165 @@ +package io.pactfoundation.consumer.dsl + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import kotlin.reflect.KClass +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.time.ZonedDateTime +import java.util.stream.Stream + +internal class DslJsonBodyBuilderTest { + private fun basedOnConstructor(classTest: KClass<*>) = + DslJsonBodyBuilder().basedOnRequiredConstructorFields(classTest) + + @ParameterizedTest + @MethodSource(value = ["stringPropertyOptionalProperties"]) + internal fun `should not map string property optional with default constructor`( + classTest: KClass<*> + ) { + val actualJsonBody = LambdaDsl.newJsonBody(basedOnConstructor(classTest)) + + val expectedBody = + LambdaDsl.newJsonBody { } + + assertThat(actualJsonBody.pactDslObject.toString()) + .isEqualTo(expectedBody.pactDslObject.toString()) + } + + @ParameterizedTest + @MethodSource(value = ["stringPropertyNonOptionalProperties"]) + internal fun `should map string property non-optional`(classTest: KClass<*>) { + val actualJsonBody = LambdaDsl.newJsonBody(basedOnConstructor(classTest)) + + val expectedBody = + LambdaDsl.newJsonBody { it.stringType("property") } + + assertThat(actualJsonBody.pactDslObject.toString()) + .isEqualTo(expectedBody.pactDslObject.toString()) + } + + @Test + internal fun `should map string property non-optional with var`() { + data class StringObjectRequiredProperty(var property: String) + + val actualJsonBody = LambdaDsl.newJsonBody(basedOnConstructor(StringObjectRequiredProperty::class)) + + val expectedBody = + LambdaDsl.newJsonBody { it.stringType("property") } + + assertThat(actualJsonBody.pactDslObject.toString()) + .isEqualTo(expectedBody.pactDslObject.toString()) + } + + @Test + internal fun `should map boolean property non-optional`() { + data class BooleanObjectRequiredProperty(val property: Boolean) + + val actualJsonBody = LambdaDsl.newJsonBody(basedOnConstructor(BooleanObjectRequiredProperty::class)) + + val expectedBody = + LambdaDsl.newJsonBody { it.booleanType("property") } + + assertThat(actualJsonBody.pactDslObject.toString()) + .isEqualTo(expectedBody.pactDslObject.toString()) + } + + @ParameterizedTest + @MethodSource(value = ["numberPropertyNonOptional"]) + internal fun `should map simple number property non-optional`(classTest: KClass<*>) { + val actualJsonBody = LambdaDsl.newJsonBody(basedOnConstructor(classTest)) + + val expectedBody = + LambdaDsl.newJsonBody { it.numberType("property") } + + assertThat(actualJsonBody.pactDslObject.toString()) + .isEqualTo(expectedBody.pactDslObject.toString()) + } + + @Test + internal fun `should map zoned date time field for iso 8601`() { + data class DatetimeRequiredProperty(val property: ZonedDateTime) + + val actualJsonBody = LambdaDsl.newJsonBody(basedOnConstructor(DatetimeRequiredProperty::class)) + + val expectedBody = + LambdaDsl.newJsonBody { it.datetime("property", "yyyy-MM-dd'T'HH:mm:ssZZ") } + + assertThat(actualJsonBody.pactDslObject.toString()) + .isEqualTo(expectedBody.pactDslObject.toString()) + } + + @Test + internal fun `should map array field`() { + data class ListObjectRequiredProperty(val property: List) + + val actualJsonBody = LambdaDsl.newJsonBody(basedOnConstructor(ListObjectRequiredProperty::class)) + + val expectedBody = + LambdaDsl.newJsonBody { it.array("property") {} } + + assertThat(actualJsonBody.pactDslObject.toString()) + .isEqualTo(expectedBody.pactDslObject.toString()) + } + + @Test + internal fun `should map inner object`() { + data class InnerObjectRequiredProperty(val property: String) + data class ObjectRequiredProperty(val inner: InnerObjectRequiredProperty) + + val actualJsonBody = LambdaDsl.newJsonBody(basedOnConstructor(ObjectRequiredProperty::class)) + + val expectedBody = + LambdaDsl.newJsonBody { root -> + root.`object`("inner") { it.stringType("property") } + } + + assertThat(actualJsonBody.pactDslObject.toString()) + .isEqualTo(expectedBody.pactDslObject.toString()) + } + + companion object { + @JvmStatic + private fun numberPropertyNonOptional(): Stream> { + data class ByteObjectNonRequiredProperty(val property: Byte) + data class ShortObjectNonRequiredProperty(val property: Short) + data class IntObjectNonRequiredProperty(val property: Int) + data class LongObjectNonRequiredProperty(val property: Long) + data class FloatObjectNonRequiredProperty(val property: Float) + data class DoubleObjectNonRequiredProperty(val property: Double) + data class NumberObjectNonRequiredProperty(val property: Number) + + return Stream.of( + ByteObjectNonRequiredProperty::class, + ShortObjectNonRequiredProperty::class, + IntObjectNonRequiredProperty::class, + LongObjectNonRequiredProperty::class, + FloatObjectNonRequiredProperty::class, + DoubleObjectNonRequiredProperty::class, + NumberObjectNonRequiredProperty::class + ) + } + + @JvmStatic + private fun stringPropertyOptionalProperties(): Stream> { + data class StringObjectNonRequiredPropertyImmutable(val property: String = "") + data class StringObjectNonRequiredPropertyMutable(var property: String = "") + + return Stream.of( + StringObjectNonRequiredPropertyImmutable::class, + StringObjectNonRequiredPropertyMutable::class + ) + } + + @JvmStatic + private fun stringPropertyNonOptionalProperties(): Stream> { + data class StringObjectRequiredPropertyImmutable(val property: String) + data class StringObjectRequiredPropertyMutable(var property: String) + + return Stream.of( + StringObjectRequiredPropertyImmutable::class, + StringObjectRequiredPropertyMutable::class + ) + } + } +} diff --git a/consumer/java8/src/test/kotlin/io/pactfoundation/consumer/dsl/ExtensionsTest.kt b/consumer/java8/src/test/kotlin/io/pactfoundation/consumer/dsl/ExtensionsTest.kt index 93375d7cbc..6c0ac19019 100644 --- a/consumer/java8/src/test/kotlin/io/pactfoundation/consumer/dsl/ExtensionsTest.kt +++ b/consumer/java8/src/test/kotlin/io/pactfoundation/consumer/dsl/ExtensionsTest.kt @@ -38,4 +38,17 @@ class ExtensionsTest { assertThat(actualJson, equalTo(expectedJson)) } + + @Test + fun `can use Kotlin DSL to create a Json body based on required constructor args`() { + data class DataClassObject(val string: String, val number: Number, val optional: String? = null) + + val expectedJson = """ + |{"number":100,"string":"string"} + |""".trimMargin().replace("\n", "") + + val actualJson = newJsonObject(DataClassObject::class).body.toString() + + assertThat(actualJson, equalTo(expectedJson)) + } } diff --git a/gradle.properties b/gradle.properties index 70d04ea625..1c2cf6536f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ groovyVersion=3.0.9 groovy2Version=2.5.10 kotlinVersion=1.3.72 httpBuilderVersion=1.0.4 -commonsLang3Version=3.4 +commonsLang3Version=3.12.0 httpClientVersion=4.5.13 scalaVersion=2.13.2 specs2Version=4.9.4