For Kotlin, any opinions on a Lambda DSL for the a...
# pact-jvm
d
For Kotlin, any opinions on a Lambda DSL for the actual
uponReceiving ... willRespondWith ...
part? I noticed that some examples in the docs indent the parts to visually separate the request and the response:
Copy code
builder
  .given("foo")
  .uponReceiving("something")
    .path("ping/pong")
    .method("POST")
    .body("Hello")
  .willRespondWith()
    .successStatus()
    .body("World")
  .build()
But with most auto-formatting tools (IDE, Spotless, ...) this might not always be possible to achieve and it will just collapse back to:
Copy code
builder
  .given("foo")
  .uponReceiving("something")
  .path("ping/pong")
  .method("POST")
  .body("Hello")
  .willRespondWith()
  .successStatus()
  .body("World")
  .build()
Now, it is fairly simple to add a Lambda DSL to achieve this indents:
Copy code
builder
  .given("foo")
  .uponReceiving("something")
  .path("ping/pong") {
    method("POST")
    body("Hello")
  }.willRespondWith()
    successStatus()
    body("World")
  }.build()
Should I invest time into creating a PR, or am I overlooking something here? (I noticed one issue between
uponReceiving
and
path
though, since they are changing types from
...WithoutPath
to
...WithPath
, so I only managed to make the DSL cut at the
path
method)
🙌 4
for reference, that would be the code to add such DSLs:
Copy code
fun PactDslRequestWithoutPath.path(
    path: String,
    addRequestMatchers: PactDslRequestWithPath.() -> PactDslRequestWithPath
): PactDslRequestWithPath = addRequestMatchers(path(path))

fun PactDslRequestWithoutPath.matchPath(
    pathRegex: String,
    pathExample: String,
    addRequestMatchers: PactDslRequestWithPath.() -> PactDslRequestWithPath
): PactDslRequestWithPath = addRequestMatchers(matchPath(pathRegex, pathExample))

fun PactDslRequestWithoutPath.pathFromProviderState(
    pathExpression: String,
    pathExample: String,
    addRequestMatchers: PactDslRequestWithPath.() -> PactDslRequestWithPath
): PactDslRequestWithPath = addRequestMatchers(pathFromProviderState(pathExpression, pathExample))

fun PactDslRequestWithPath.willRespondWith(addResponseMatchers: PactDslResponse.() -> PactDslResponse): PactDslResponse =
    addResponseMatchers(willRespondWith())
u
PRs that make things better are always welcome
1000000 1
d
awesome. will look into it, thanks 👍
😊 2
u
Those DSLs are really old. They were written in 2014, and have just been added to over the years. I was planning on creating a new DSL for the V4 Pact format which has separate builders for the different types.
d
ah, sounds cool. looking forward to that 👍
y
Like it @Daniel Tischner thanks for sharing for thoughts with the group and for any additions in the future. We look forward to supporting you with any PR’s! Very neat thought, and will making grokking code in an IDE much nicer 👌
👌 1
b
I have some similar helper functions, I can share them here (or in PR comments) if you'd like more ideas :)
👍 1
y
Spread the helper love @Boris 💛 - I’m sure you’ve many little gems from over the years
b
Pretty sure I've shared these before, but here's my most recent sample :)
Copy code
import au.com.dius.pact.consumer.ConsumerPactBuilder
import au.com.dius.pact.consumer.MockServer
import au.com.dius.pact.consumer.PactTestExecutionContext
import au.com.dius.pact.consumer.PactTestRun
import au.com.dius.pact.consumer.PactVerificationResult
import au.com.dius.pact.consumer.PactVerificationResult.Ok
import au.com.dius.pact.consumer.dsl.LambdaDsl
import au.com.dius.pact.consumer.dsl.LambdaDslJsonBody
import au.com.dius.pact.consumer.dsl.LambdaDslObject
import au.com.dius.pact.consumer.dsl.PactDslRequestWithPath
import au.com.dius.pact.consumer.dsl.PactDslResponse
import au.com.dius.pact.consumer.dsl.PactDslWithProvider
import au.com.dius.pact.consumer.dsl.PactDslWithState
import au.com.dius.pact.consumer.dsl.newJsonArray
import au.com.dius.pact.consumer.dsl.newObject
import au.com.dius.pact.consumer.model.MockProviderConfig
import au.com.dius.pact.consumer.runConsumerTest
import au.com.dius.pact.core.model.BasePact
import au.com.dius.pact.core.model.RequestResponsePact
import io.kotest.matchers.shouldNotBe
import retrofit2.Call
import retrofit2.Response
import java.time.ZonedDateTime
import java.util.Base64

fun pactBuilder() = ConsumerPactBuilder
  .consumer("[redacted]-consumer")
  .hasPactWith("[redacted]-api")

fun buildPact(
  builder: PactDslWithProvider.() -> PactDslResponse
): RequestResponsePact = pactBuilder().builder().toPact()

fun buildPact(
  given: String,
  builder: PactDslWithState.() -> PactDslResponse
): RequestResponsePact = pactBuilder().given(given).builder().toPact()

operator fun <R> BasePact.invoke(auth: Boolean = true, createCall: RedactedClient.() -> Call<R>): R? {
  println()
  val result: PactVerificationResult = runConsumerTest(
    this,
    MockProviderConfig.createDefault(),
    object : PactTestRun<Response<R>> {
      override fun run(mockServer: MockServer, context: PactTestExecutionContext?): Response<R> {
        val client = buildClient<RedactedClient>(mockServer.getUrl(), auth)
        return client.createCall().execute()
      }
    }
  )
  println("testPact result: $result")
  return ((result as? Ok)?.result as? Response<R>?)?.body()
}

fun PactDslRequestWithPath.jsonHeader() =
  matchHeader("Content-Type", "^application/json.*$", "application/json; charset utf-8")

fun PactDslRequestWithPath.authHeader(user: String = "admin", pass: String = "admin") =
  matchHeader("Authorization", "^Basic .*$", "Basic ${"$user:$pass".base64}")

val String.base64: String
  get() = Base64.getEncoder().encodeToString(encodeToByteArray())

fun oneOf(vararg values: String): String = values.joinToString("|")

fun LambdaDslObject.zonedDateTime(
  fieldName: String,
  example: String = "2021-04-28T00:32:16.450595Z"
): LambdaDslObject = datetime(
  fieldName,
  "YYYY-MM-dd'T'HH:mm:ss.SSSSSSXXX",
  ZonedDateTime.parse(example)
)

fun LambdaDslJsonBody.arrayLike(fieldName: String, eachBuilder: LambdaDslObject.() -> Unit): LambdaDslObject =
  minArrayLike(fieldName, 1) { eachBuilder(it) }

infix fun PactDslRequestWithPath.jsonBody(bodyBuilder: LambdaDslJsonBody.() -> Unit): PactDslRequestWithPath =
  body(LambdaDsl.newJsonBody(bodyBuilder).build())

infix fun PactDslResponse.jsonBody(bodyBuilder: LambdaDslJsonBody.() -> Unit): PactDslResponse =
  body(LambdaDsl.newJsonBody(bodyBuilder).build())

fun PactDslResponse.jsonArray(times: Int = 1, body: LambdaDslObject.() -> Unit): PactDslResponse =
  body(newJsonArray { repeat(times) { newObject(body).build() } })

fun PactDslResponse.jsonArray(vararg body: LambdaDslObject.() -> Unit): PactDslResponse =
  body(newJsonArray { body.map { newObject(it).build() } })

infix fun <T> Iterable<T>.shouldContainMatch(findMatch: T.()->Boolean) =
  find(findMatch) shouldNotBe null
d
(for visibility) i opened a PR now: https://github.com/pact-foundation/pact-jvm/pull/1598
b
oh, forgot to include the call-site examples . . . they'd answer your original query a bit better . . .
d
@Boris i like that
arrayLike
, will use that in my own code base until u add it to pact directly. thanks
👍 1
b
ok, I've gone and got some call-site examples for you 🙂
This one uses the ZonedDateTime helper:
Copy code
scenario("incomplete") {
      val pact = buildPact(given = "transaction in progress") {
        uponReceiving("a request to check the status of a running transaction").run {
          method("GET").matchPath(
            "/admin/amendment-batch/${RegexUUID}",
            "/admin/amendment-batch/${ConstUUID}"
          ).authHeader()
        }.willRespondWith().run {
          status(200) jsonBody {
            zonedDateTime("startDate")
            array("amendments") { a ->
              a.`object` {
                it.uuid("id", ConsistentUUID)
                it.zonedDateTime("startDate")
                it.zonedDateTime("endDate")
                it.stringValue("status", "Success")
              }
              a.`object` {
                it.uuid("id", ConsistentUUID)
                it.zonedDateTime("startDate")
                it.stringValue("status", "InProgress")
              }
              a.`object` {
                it.uuid("id", ConsistentUUID)
                it.stringValue("status", "NotStarted")
              }
            }
          }
        }
      }

      val response = pact {
        checkPublishAmendmentStatus(UUID.randomUUID().toString())
      }

      println("In-progress transaction response: $response")
      response!!.apply {
        amendments shouldContainMatch { status == "Success" }
        amendments shouldContainMatch { status == "InProgress" }
        amendments shouldContainMatch { status == "NotStarted" }
      }
    }
this on uses jsonArray, but I think you're already good with it 😅
Copy code
scenario("published only") {
      val pact = buildPact(given = "several published amendments") {
        uponReceiving("a request to list admin amendments").run {
          method("GET").path("/admin/amendments")
            .query("unpublished=false")
            .authHeader()
        }.willRespondWith().run {
          status(200).jsonArray(3) {
            uuid("amendmentId", ConsistentUUID)
            stringType("displayId", "VC999")
            booleanValue("unpublished", false)
          }
        }
      }

      val response = pact {
        listAdminAmendments(unpublished = false)
      }

      response!!.forEach {
        it.amendmentId shouldBe ConsistentUUID
        it.displayId shouldBe "VC999"
        it.unpublished shouldBe false
      }
    }
Before I added quite so many DSL helpers, I was just using the Java8 lambda DSL with lots of
with
,
apply
, and
run
d
yeah. looks cool. thank you 🙂
👍 1
b
Could definitely be improved with a native Kotlin DSL, instead of just a hack on top. But that was good enough while I haven't had time to contribute to the core libs.
👍 1