-
-
Notifications
You must be signed in to change notification settings - Fork 481
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add initial DSL using builder pattern to replace legacy DSL
- Loading branch information
1 parent
0506476
commit 1d27922
Showing
17 changed files
with
872 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/HttpInteractionBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
170 changes: 170 additions & 0 deletions
170
consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/HttpPartBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
consumer/src/main/kotlin/au/com/dius/pact/consumer/dsl/HttpRequestBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.