Skip to content

Commit

Permalink
feat: Add initial DSL using builder pattern to replace legacy DSL
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Jan 16, 2023
1 parent 0506476 commit 1d27922
Show file tree
Hide file tree
Showing 17 changed files with 872 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,21 @@ public class V4PactBuilderTest {
@Pact(provider="v4_test_provider", consumer="v4_test_consumer")
public V4Pact httpInteraction(PactBuilder builder) {
return builder
.usingLegacyDsl()
.given("good state")
.comment("This is a comment")
.uponReceiving("V4 PactProviderTest test interaction")
.path("/")
.method("GET")
.given("good state")
.comment("Another comment")
.willRespondWith()
.status(200)
.body("{\"responsetest\": true, \"version\": \"v3\"}")
.expectsToReceiveHttpInteraction("V4 PactProviderTest test interaction", httpBuilder -> {
return httpBuilder
.withRequest(requestBuilder -> requestBuilder
.path("/")
.method("GET"))
.willRespondWith(responseBuilder -> responseBuilder
.status(200)
.body("{\"responsetest\": true, \"version\": \"v3\"}"))
.comment("This is also a comment");
})
.comment("This is also a comment")
.toPact(V4Pact.class);
.toPact();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package au.com.dius.pact.consumer.dsl

import au.com.dius.pact.core.model.ProviderState
import au.com.dius.pact.core.model.V4Interaction
import au.com.dius.pact.core.support.json.JsonValue

/**
* Pact HTTP builder DSL that supports V4 formatted Pact files
*/
open class HttpInteractionBuilder(
description: String,
providerStates: MutableList<ProviderState>,
comments: MutableList<JsonValue.StringValue>
) {
val interaction = V4Interaction.SynchronousHttp("", description, providerStates)

init {
if (comments.isNotEmpty()) {
interaction.comments["text"] = JsonValue.Array(comments.toMutableList())
}
}

fun build(): V4Interaction {
return interaction
}

/**
* Build the request part of the interaction using a request builder
*/
fun withRequest(builderFn: (HttpRequestBuilder) -> HttpRequestBuilder?): HttpInteractionBuilder {
val builder = HttpRequestBuilder(interaction.request)
val result = builderFn(builder)
if (result != null) {
interaction.request = result.build()
} else {
interaction.request = builder.build()
}
return this;
}

/**
* Build the response part of the interaction using a response builder
*/
fun willRespondWith(builderFn: (HttpResponseBuilder) -> HttpResponseBuilder?): HttpInteractionBuilder {
val builder = HttpResponseBuilder(interaction.response)
val result = builderFn(builder)
if (result != null) {
interaction.response = result.build()
} else {
interaction.response = builder.build()
}
return this;
}

/**
* Sets the unique key for the interaction. If this is not set, or is empty, a key will be calculated from the
* contents of the interaction.
*/
fun key(key: String?): HttpInteractionBuilder {
interaction.key = key
return this;
}

/**
* Sets the interaction description
*/
fun description(description: String): HttpInteractionBuilder {
interaction.description = description
return this
}

/**
* Adds a provider state to the interaction.
*/
@JvmOverloads
fun state(stateDescription: String, params: Map<String, Any?> = emptyMap()): HttpInteractionBuilder {
interaction.providerStates.add(ProviderState(stateDescription, params))
return this
}

/**
* Adds a provider state to the interaction with a parameter.
*/
fun state(stateDescription: String, paramKey: String, paramValue: Any?): HttpInteractionBuilder {
interaction.providerStates.add(ProviderState(stateDescription, mapOf(paramKey to paramValue)))
return this
}

/**
* Adds a provider state to the interaction with parameters a pairs of key/values.
*/
fun state(stateDescription: String, vararg params: Pair<String, Any?>): HttpInteractionBuilder {
interaction.providerStates.add(ProviderState(stateDescription, params.toMap()))
return this
}

/**
* Marks the interaction as pending.
*/
fun pending(pending: Boolean): HttpInteractionBuilder {
interaction.pending = pending
return this
}

/**
* Adds a text comment to the interaction
*/
fun comment(comment: String): HttpInteractionBuilder {
if (interaction.comments.containsKey("text")) {
interaction.comments["text"]!!.add(JsonValue.StringValue(comment))
} else {
interaction.comments["text"] = JsonValue.Array(mutableListOf(JsonValue.StringValue(comment)))
}
return this
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package au.com.dius.pact.consumer.dsl

import au.com.dius.pact.core.model.IHttpPart
import au.com.dius.pact.core.model.ContentType
import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.support.isNotEmpty
import io.ktor.http.HeaderValue
import io.ktor.http.parseHeaderValue

open class HttpPartBuilder(private val part: IHttpPart) {

/**
* Adds a header to the HTTP part. The value will be converted to a string (using the toString() method), unless it
* is a List. With a List as the value, it will set up a multiple value header. If the value resolves to a single
* String, and the header key is for a header that has multiple values, the values will be split into a list.
*
* For example: `header("OPTIONS", "GET, POST, PUT")` is the same as `header("OPTIONS", List.of("GET", "POST, "PUT"))`
*/
open fun header(key: String, value: Any): HttpPartBuilder {
val headValues = when (value) {
is List<*> -> value.map { it.toString() }
else -> if (isKnowSingleValueHeader(key)) {
listOf(value.toString())
} else {
parseHeaderValue(value.toString()).map { headerToString(it) }
}
}
part.headers[key] = headValues
return this
}

/**
* Adds all the headers to the HTTP part. The values will be converted to a string (using the toString() method),
* and the header key is for a header that has multiple values, the values will be split into a list.
*
* For example: `headers("OPTIONS", "GET, POST, PUT")` is the same as
* `header("OPTIONS", List.of("GET", "POST, "PUT"))`
*/
open fun headers(key: String, value: String, nameValuePairs: Array<out String>): HttpPartBuilder {
require(nameValuePairs.size % 2 == 0) {
"Pairs of key/values should be provided, but there is one key without a value."
}

val headValue = if (isKnowSingleValueHeader(key)) {
mutableListOf(value)
} else {
parseHeaderValue(value).map { headerToString(it) }.toMutableList()
}
val headersMap = nameValuePairs.toList().chunked(2).fold(mutableMapOf(key to headValue)) { acc, values ->
val k = values[0]
val v = if (isKnowSingleValueHeader(k)) {
listOf(values[1])
} else {
parseHeaderValue(values[1]).map { headerToString(it) }
}
if (acc.containsKey(k)) {
acc[k]!!.addAll(v)
} else {
acc[k] = v.toMutableList()
}
acc
}

part.headers.putAll(headersMap)

return this
}

/**
* Adds all the headers to the HTTP part. The values will be converted to a string (using the toString() method),
* and the header key is for a header that has multiple values, the values will be split into a list.
*
* For example: `headers("OPTIONS", "GET, POST, PUT")` is the same as
* `header("OPTIONS", List.of("GET", "POST, "PUT"))`
*/
open fun headers(nameValuePairs: Array<out Pair<String, String>>): HttpPartBuilder {
val headersMap = nameValuePairs.toList().fold(mutableMapOf<String, MutableList<String>>()) { acc, value ->
val k = value.first
val v = if (isKnowSingleValueHeader(k)) {
listOf(value.second)
} else {
parseHeaderValue(value.second).map { headerToString(it) }
}
if (acc.containsKey(k)) {
acc[k]!!.addAll(v)
} else {
acc[k] = v.toMutableList()
}
acc
}

part.headers.putAll(headersMap)

return this
}

/**
* Adds all the headers to the HTTP part. You can either pass a Map<String -> String>, and values will be converted
* to a string (using the toString() method), and the header key is for a header that has multiple values,
* the values will be split into a list.
*
* For example: `headers(Map<"OPTIONS", "GET, POST, PUT">)` is the same as
* `header(Map<"OPTIONS", List<"GET", "POST, "PUT">>))`
*
* Or pass in a Map<String -> List<String>> and no conversion will take place.
*/
open fun headers(values: Map<String, Any>): HttpPartBuilder {
val headersMap = values.mapValues { entry ->
val k = entry.key
if (isKnowSingleValueHeader(k)) {
listOf(entry.value.toString())
} else if (entry.value is List<*>) {
(entry.value as List<*>).map { it.toString() }
} else {
parseHeaderValue(entry.value.toString()).map { headerToString(it) }
}
}

part.headers.putAll(headersMap)

return this
}

/**
* Sets the body of the HTTP part as a string value. If the content type is not already set, it will try to detect
* the content type from the given string, otherwise will default to text/plain.
*/
open fun body(body: String) = body(body, null)

/**
* Sets the body of the HTTP part as a string value. If the content type is not already set, it will try to detect
* the content type from the given string, otherwise will default to text/plain.
*/
open fun body(body: String, contentTypeString: String?): HttpPartBuilder {
val contentTypeHeader = part.contentTypeHeader()
val contentType = if (!contentTypeString.isNullOrEmpty()) {
ContentType.fromString(contentTypeString)
} else if (contentTypeHeader != null) {
ContentType.fromString(contentTypeHeader)
} else {
OptionalBody.detectContentTypeInByteArray(body.toByteArray()) ?: ContentType.TEXT_PLAIN
}

val charset = contentType.asCharset()
part.body = OptionalBody.body(body.toByteArray(charset), contentType)

if (contentTypeHeader == null || contentTypeString.isNotEmpty()) {
part.headers["content-type"] = listOf(contentType.toString())
}

return this
}

private fun isKnowSingleValueHeader(key: String): Boolean {
return SINGLE_VALUE_HEADERS.contains(key.lowercase())
}

private fun headerToString(headerValue: HeaderValue): String {
return if (headerValue.params.isNotEmpty()) {
val params = headerValue.params.joinToString(";") { "${it.name}=${it.value}" }
"${headerValue.value};$params"
} else {
headerValue.value
}
}

companion object {
val SINGLE_VALUE_HEADERS = setOf("date")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package au.com.dius.pact.consumer.dsl

import au.com.dius.pact.core.model.HttpRequest

/**
* Pact HTTP Request builder DSL that supports V4 formatted Pact files
*/
open class HttpRequestBuilder(private val request: HttpRequest): HttpPartBuilder(request) {
/**
* Terminate the builder and return the request object
*/
fun build(): HttpRequest {
return request
}

/**
* HTTP Method for the request.
*/
fun method(method: String): HttpRequestBuilder {
request.method = method
return this
}

/**
* Path for the request.
*/
fun path(path: String): HttpRequestBuilder {
request.path = path
return this
}

override fun header(key: String, value: Any): HttpRequestBuilder {
return super.header(key, value) as HttpRequestBuilder
}

override fun headers(key: String, value: String, vararg nameValuePairs: String): HttpRequestBuilder {
return super.headers(key, value, nameValuePairs) as HttpRequestBuilder
}

override fun headers(vararg nameValuePairs: Pair<String, String>): HttpRequestBuilder {
return super.headers(nameValuePairs) as HttpRequestBuilder
}

override fun headers(values: Map<String, Any>): HttpRequestBuilder {
return super.headers(values) as HttpRequestBuilder
}

override fun body(body: String): HttpRequestBuilder {
return super.body(body) as HttpRequestBuilder
}

override fun body(body: String, contentTypeString: String?): HttpRequestBuilder {
return super.body(body, contentTypeString) as HttpRequestBuilder
}
}

0 comments on commit 1d27922

Please sign in to comment.