Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.specmatic.mock.ScenarioStub.kt Maven / Gradle / Ivy
Go to download
Turn your contracts into executable specifications. Contract Driven Development - Collaboratively Design & Independently Deploy MicroServices & MicroFrontends.
package io.specmatic.mock
import io.specmatic.core.*
import io.specmatic.core.pattern.ContractException
import io.specmatic.core.pattern.Pattern
import io.specmatic.core.value.*
import io.specmatic.stub.stringToMockScenario
import java.io.File
data class ScenarioStub(val request: HttpRequest = HttpRequest(), val response: HttpResponse = HttpResponse(0, emptyMap()), val delayInMilliseconds: Long? = null, val stubToken: String? = null, val requestBodyRegex: String? = null, val data: JSONObjectValue = JSONObjectValue(), val filePath: String? = null) {
fun toJSON(): JSONObjectValue {
val mockInteraction = mutableMapOf()
mockInteraction[MOCK_HTTP_REQUEST] = request.toJSON()
mockInteraction[MOCK_HTTP_RESPONSE] = response.toJSON()
return JSONObjectValue(mockInteraction)
}
private fun combinations(data: Map>>): List>>> {
// Helper function to compute Cartesian product of multiple lists
fun cartesianProduct(lists: List>): List> {
return lists.fold(listOf(listOf())) { acc, list ->
acc.flatMap { item -> list.map { value -> item + value } }
}
}
// Generate the Cartesian product of the values in the input map
val product = cartesianProduct(data.map { (key, nestedMap) ->
nestedMap.map { (nestedKey, valueMap) ->
mapOf(key to mapOf(nestedKey to valueMap))
}
})
// Convert each product result into a combined map
return product.map { item ->
item.reduce { acc, map -> acc + map }
}
}
fun findPatterns(input: String): Set {
val pattern = """\{\{(@\w+)\}\}""".toRegex()
return pattern.findAll(input).map { it.groupValues[1] }.toSet()
}
fun dataTemplateNameOnly(wholeDataTemplateName: String): String {
return wholeDataTemplateName.split(".").first()
}
fun requestDataTemplates(): Set {
return findPatterns(request.toLogString()).map(this::dataTemplateNameOnly).toSet()
}
fun responseDataTemplates(): Set {
return findPatterns(response.toLogString()).map(this::dataTemplateNameOnly).toSet()
}
fun resolveDataSubstitutions(scenario: Scenario): List {
val dataTemplates = requestDataTemplates() + responseDataTemplates()
val missingDataTemplates = dataTemplates.filter { it !in data.jsonObject }
if(missingDataTemplates.isNotEmpty())
throw ContractException("Could not find the following data templates defined: ${missingDataTemplates.joinToString(", ")}")
if(data.jsonObject.isEmpty())
return listOf(this)
val substitutions = unwrapSubstitutions(data)
val combinations = combinations(substitutions)
return combinations.map { combination ->
replaceInExample(combination, scenario.httpRequestPattern.body, scenario.resolver)
}
}
private fun unwrapSubstitutions(rawSubstitutions: JSONObjectValue): Map>> {
val substitutions = rawSubstitutions.jsonObject.mapValues {
val json =
it.value as? JSONObjectValue ?: throw ContractException("Invalid structure of data in the example file")
json.jsonObject.mapValues {
val innerJSON =
it.value as? JSONObjectValue ?: throw ContractException("Invalid structure of data in the example file")
innerJSON.jsonObject.mapValues {
it.value
}
}
}
return substitutions
}
private fun replaceInRequestBody(value: JSONObjectValue, substitutions: Map>>, requestTemplatePatterns: Map, resolver: Resolver): Value {
return value.copy(
value.jsonObject.mapValues {
replaceInRequestBody(it.key, it.value, substitutions, requestTemplatePatterns, resolver)
}
)
}
private fun replaceInRequestBody(value: JSONArrayValue, substitutions: Map>>, requestTemplatePatterns: Map, resolver: Resolver): Value {
return value.copy(
value.list.map {
replaceInRequestBody(value, substitutions, requestTemplatePatterns, resolver)
}
)
}
private fun substituteStringInRequest(value: String, substitutions: Map>>): String {
return if(value.hasDataTemplate()) {
val substitutionSetName = value.removeSurrounding("{{", "}}")
val substitutionSet = substitutions[substitutionSetName] ?: throw ContractException("$substitutionSetName does not exist in the data")
substitutionSet.keys.firstOrNull() ?: throw ContractException("$substitutionSetName in data is empty")
} else
value
}
private fun replaceInRequestBody(key: String, value: Value, substitutions: Map>>, requestTemplatePatterns: Map, resolver: Resolver): Value {
return when(value) {
is StringValue -> {
if(value.hasDataTemplate()) {
val substitutionSetName = value.string.removeSurrounding("{{", "}}")
val substitutionSet = substitutions[substitutionSetName] ?: throw ContractException("$substitutionSetName does not exist in the data")
val substitutionKey = substitutionSet.keys.firstOrNull() ?: throw ContractException("$substitutionSetName in data is empty")
val pattern = requestTemplatePatterns.getValue(key)
pattern.parse(substitutionKey, resolver)
} else
value
}
is JSONObjectValue -> {
replaceInRequestBody(value, substitutions, requestTemplatePatterns, resolver)
}
is JSONArrayValue -> {
replaceInRequestBody(value, substitutions, requestTemplatePatterns, resolver)
}
else -> value
}
}
private fun replaceInExample(substitutions: Map>>, requestBody: Pattern, resolver: Resolver): ScenarioStub {
val requestTemplatePatterns = requestBody.getTemplateTypes("", request.body, resolver).value
val newPath = replaceInPath(request.path ?: "", substitutions)
val newRequestHeaders = replaceInRequestHeaders(request.headers, substitutions)
val newQueryParams: Map = replaceInRequestQueryParams(request.queryParams, substitutions)
val newRequestBody = replaceInRequestBody("", request.body, substitutions, requestTemplatePatterns, resolver)
val newRequest = request.copy(
path = newPath,
headers = newRequestHeaders,
queryParams = QueryParameters(newQueryParams),
body = newRequestBody)
val newResponseBody = replaceInResponseBody(response.body, substitutions, "")
val newResponseHeaders = replaceInResponseHeaders(response.headers, substitutions)
val newResponse = response.copy(
headers = newResponseHeaders,
body = newResponseBody
)
return copy(
request = newRequest,
response = newResponse
)
}
private fun replaceInPath(path: String, substitutions: Map>>): String {
val rawPathSegments = path.split("/")
val pathSegments = rawPathSegments.let { if(it.firstOrNull() == "") it.drop(1) else it }
val updatedSegments = pathSegments.map { if(it.hasDataTemplate()) substituteStringInRequest(it, substitutions) else it }
val prefix = if(pathSegments.size != rawPathSegments.size) listOf("") else emptyList()
return (prefix + updatedSegments).joinToString("/")
}
private fun replaceInResponseHeaders(
headers: Map,
substitutions: Map>>
): Map {
return headers.mapValues { (key, value) ->
substituteStringInResponse(value, substitutions, key)
}
}
private fun replaceInRequestQueryParams(
queryParams: QueryParameters,
substitutions: Map>>
): Map {
return queryParams.asMap().mapValues { (key, value) ->
substituteStringInRequest(value, substitutions)
}
}
private fun replaceInRequestHeaders(headers: Map, substitutions: Map>>): Map {
return headers.mapValues { (key, value) ->
substituteStringInRequest(value, substitutions)
}
}
private fun replaceInResponseBody(value: JSONObjectValue, substitutions: Map>>): Value {
return value.copy(
value.jsonObject.mapValues {
replaceInResponseBody(it.value, substitutions, it.key)
}
)
}
private fun replaceInResponseBody(value: JSONArrayValue, substitutions: Map>>): Value {
return value.copy(
value.list.map { item: Value ->
replaceInResponseBody(item, substitutions, "")
}
)
}
private fun substituteStringInResponse(value: String, substitutions: Map>>, key: String): String {
return if(value.hasDataTemplate()) {
val dataSetIdentifiers = DataSetIdentifiers(value, key)
val substitutionSet = substitutions[dataSetIdentifiers.name] ?: throw ContractException("${dataSetIdentifiers.name} does not exist in the data")
val substitutionValue = substitutionSet.values.first()[dataSetIdentifiers.key] ?: throw ContractException("${dataSetIdentifiers.name} does not contain a value for ${dataSetIdentifiers.key}")
substitutionValue.toStringLiteral()
} else
value
}
class DataSetIdentifiers(rawSetName: String, objectKey: String) {
val name: String
val key: String
init {
val substitutionSetPieces = rawSetName.removeSurrounding("{{", "}}").split(".")
name = substitutionSetPieces.getOrNull(0) ?: throw ContractException("Substitution set name {{}} was empty")
key = substitutionSetPieces.getOrNull(1) ?: objectKey
}
}
private fun replaceInResponseBody(value: Value, substitutions: Map>>, key: String): Value {
return when(value) {
is StringValue -> {
if(value.hasDataTemplate()) {
val dataSetIdentifiers = DataSetIdentifiers(value.string, key)
val substitutionSet = substitutions[dataSetIdentifiers.name] ?: throw ContractException("${dataSetIdentifiers.name} does not exist in the data")
val substitutionValue = substitutionSet.values.first()[dataSetIdentifiers.key] ?: throw ContractException("${dataSetIdentifiers.name} does not contain a value for ${dataSetIdentifiers.key}")
substitutionValue
} else
value
}
is JSONObjectValue -> {
replaceInResponseBody(value, substitutions)
}
is JSONArrayValue -> {
replaceInResponseBody(value, substitutions)
}
else -> value
}
}
companion object {
fun parse(text: String): ScenarioStub {
return stringToMockScenario(StringValue(text))
}
fun readFromFile(file: File): ScenarioStub {
return stringToMockScenario(StringValue(file.readText(Charsets.UTF_8))).copy(filePath = file.path)
}
}
}
const val MOCK_HTTP_REQUEST = "http-request"
const val MOCK_HTTP_RESPONSE = "http-response"
const val DELAY_IN_SECONDS = "delay-in-seconds"
const val DELAY_IN_MILLISECONDS = "delay-in-milliseconds"
const val TRANSIENT_MOCK = "http-stub"
const val TRANSIENT_MOCK_ID = "$TRANSIENT_MOCK-id"
const val REQUEST_BODY_REGEX = "bodyRegex"
val MOCK_HTTP_REQUEST_ALL_KEYS = listOf("mock-http-request", MOCK_HTTP_REQUEST)
val MOCK_HTTP_RESPONSE_ALL_KEYS = listOf("mock-http-response", MOCK_HTTP_RESPONSE)
fun validateMock(mockSpec: Map) {
if (MOCK_HTTP_REQUEST_ALL_KEYS.none { mockSpec.containsKey(it) })
throw ContractException(errorMessage = "Stub does not contain http-request/mock-http-request as a top level key.")
if (MOCK_HTTP_RESPONSE_ALL_KEYS.none { mockSpec.containsKey(it) })
throw ContractException(errorMessage = "Stub does not contain http-request/mock-http-request as a top level key.")
}
fun mockFromJSON(mockSpec: Map): ScenarioStub {
val mockRequest: HttpRequest = requestFromJSON(getJSONObjectValue(MOCK_HTTP_REQUEST_ALL_KEYS, mockSpec))
val mockResponse: HttpResponse = HttpResponse.fromJSON(getJSONObjectValue(MOCK_HTTP_RESPONSE_ALL_KEYS, mockSpec))
val data = getJSONObjectValueOrNull("data", mockSpec)?.let { JSONObjectValue(it) } ?: JSONObjectValue()
val delayInSeconds: Int? = getIntOrNull(DELAY_IN_SECONDS, mockSpec)
val delayInMilliseconds: Long? = getLongOrNull(DELAY_IN_MILLISECONDS, mockSpec)
val delayInMs: Long? = delayInMilliseconds ?: delayInSeconds?.let { it.toLong().times(1000) }
val stubToken: String? = getStringOrNull(TRANSIENT_MOCK_ID, mockSpec)
val requestBodyRegex: String? = getRequestBodyRegexOrNull(mockSpec)
return ScenarioStub(request = mockRequest, response = mockResponse, delayInMilliseconds = delayInMs, stubToken = stubToken, requestBodyRegex = requestBodyRegex, data = data)
}
fun getRequestBodyRegexOrNull(mockSpec: Map): String? {
val requestSpec: Map = getJSONObjectValue(MOCK_HTTP_REQUEST_ALL_KEYS, mockSpec)
return requestSpec[REQUEST_BODY_REGEX]?.toStringLiteral()
}
fun getJSONObjectValue(keys: List, mapData: Map): Map {
val key = keys.first { mapData.containsKey(it) }
return getJSONObjectValue(key, mapData)
}
fun getJSONObjectValue(key: String, mapData: Map): Map {
val data = mapData.getValue(key)
if(data !is JSONObjectValue) throw ContractException("$key should be a json object")
return data.jsonObject
}
fun getJSONObjectValueOrNull(key: String, mapData: Map): Map? {
val data = mapData[key] ?: return null
if(data !is JSONObjectValue) throw ContractException("$key should be a json object")
return data.jsonObject
}
fun getIntOrNull(key: String, mapData: Map): Int? {
val data = mapData[key]
return data?.let {
if(data !is NumberValue) throw ContractException("$key should be a number")
return data.number.toInt()
}
}
fun getLongOrNull(key: String, mapData: Map): Long? {
val data = mapData[key]
return data?.let {
if(data !is NumberValue) throw ContractException("$key should be a number")
return data.number.toLong()
}
}
fun getStringOrNull(key: String, mapData: Map): String? {
val data = mapData[key]
return data?.let {
if(data !is StringValue) throw ContractException("$key should be a number")
return data.string
}
}