All Downloads are FREE. Search and download functionalities are using the official Maven repository.

run.qontract.core.HttpRequestPattern.kt Maven / Gradle / Ivy

Go to download

A Contract Testing Tool that leverages Gherkin to describe APIs in a human readable and machine enforceable manner

There is a newer version: 0.23.1
Show newest version
package run.qontract.core

import run.qontract.core.Result.Failure
import run.qontract.core.Result.Success
import run.qontract.core.pattern.*
import run.qontract.core.value.StringValue
import java.net.URI

private const val MULTIPART_FORMDATA_BREADCRUMB = "MULTIPART-FORMDATA"
private const val FORM_FIELDS_BREADCRUMB = "FORM-FIELDS"
const val CONTENT_TYPE = "Content-Type"

data class HttpRequestPattern(val headersPattern: HttpHeadersPattern = HttpHeadersPattern(), val urlMatcher: URLMatcher? = null, val method: String? = null, val body: Pattern = EmptyStringPattern, val formFieldsPattern: Map = emptyMap(), val multiPartFormDataPattern: List = emptyList()) {
    fun matches(incomingHttpRequest: HttpRequest, resolver: Resolver, headersResolver: Resolver? = null): Result {
        val result = incomingHttpRequest to resolver to
                ::matchUrl then
                ::matchMethod then
                { (request, defaultResolver) ->
                    matchHeaders(Triple(request, headersResolver, defaultResolver))
                } then
                ::matchFormFields then
                ::matchMultiPartFormData then
                ::matchBody otherwise
                ::handleError toResult
                ::returnResult

        return when(result) {
            is Failure -> result.breadCrumb("REQUEST")
            else -> result
        }
    }

    private fun matchMultiPartFormData(parameters: Pair): MatchingResult> {
        val (httpRequest, resolver) = parameters

        if(multiPartFormDataPattern.isEmpty() && httpRequest.multiPartFormData.isEmpty())
            return MatchSuccess(parameters)

        if (multiPartFormDataPattern.isEmpty() && httpRequest.multiPartFormData.isNotEmpty()) {
            return MatchFailure(Failure("The contract expected no multipart data, but the request contained ${httpRequest.multiPartFormData.size} parts.", breadCrumb = MULTIPART_FORMDATA_BREADCRUMB))
        }

        val results = multiPartFormDataPattern.map { type ->
            val results = httpRequest.multiPartFormData.map { value ->
                type.matches(value, resolver)
            }

            val result = results.find { it is Success } ?: results.find { it is Failure && it.failureReason != FailureReason.PartNameMisMatch }?.breadCrumb(type.name)
            result ?: when {
                    isOptional(type.name) -> Success()
                    else -> Failure("The part named ${type.name} was not found.").breadCrumb(type.name)
                }
        }

        if (results.any { it !is Success }) {
            val reason = results.filter { it !is Success }.joinToString("\n\n") {
                resultReport(it).prependIndent("  ")
            }

            return MatchFailure(Failure("The multipart data in the request did not match the contract:\n$reason", breadCrumb = MULTIPART_FORMDATA_BREADCRUMB))
        }

        val typeKeys = multiPartFormDataPattern.map { withoutOptionality(it.name) }.sorted()
        val valueKeys = httpRequest.multiPartFormData.map { it.name }.sorted()

        val missingInType = valueKeys.filter { it !in typeKeys }
        if(missingInType.isNotEmpty())
            return MatchFailure(Failure("Some parts in the request were missing from the contract, and their names are $missingInType.", breadCrumb = MULTIPART_FORMDATA_BREADCRUMB))

        val originalTypeKeys = multiPartFormDataPattern.map { it.name }.sorted()
        val missingInValue = originalTypeKeys.filter { !isOptional(it) }.filter { withoutOptionality(it) !in valueKeys }.joinToString(", ")
        if(missingInValue.isNotEmpty())
            return MatchFailure(Failure("Some parts in the contract were missing from the request, and their names are $missingInValue", breadCrumb = MULTIPART_FORMDATA_BREADCRUMB))

        return MatchSuccess(parameters)
    }

    fun matchFormFields(parameters: Pair): MatchingResult> {
        val (httpRequest, resolver) = parameters

        val keys: List = formFieldsPattern.keys.filter { key -> isOptional(key) && withoutOptionality(key) !in httpRequest.formFields }
        if(keys.isNotEmpty())
            return MatchFailure(Failure(message = "Fields $keys not found", breadCrumb = FORM_FIELDS_BREADCRUMB))

        val keyError = resolver.findMissingKey(formFieldsPattern, httpRequest.formFields, ::validateUnexpectedKeys)
        if(keyError != null)
            return MatchFailure(missingKeyToResult(keyError, "form field"))

        val result: Result? = formFieldsPattern
            .filterKeys { key -> withoutOptionality(key) in httpRequest.formFields }
            .map { (key, pattern) -> Triple(withoutOptionality(key), pattern, httpRequest.formFields.getValue(key)) }
            .map { (key, pattern, value) ->
                try {
                    when (val result = resolver.matchesPattern(key, pattern, try { pattern.parse(value, resolver) } catch (e: Throwable) { StringValue(value) } )) {
                        is Failure -> result.breadCrumb(key).breadCrumb(FORM_FIELDS_BREADCRUMB)
                        else -> result
                    }
                } catch(e: ContractException) {
                    e.failure().breadCrumb(key).breadCrumb(FORM_FIELDS_BREADCRUMB)
                } catch(e: Throwable) {
                    mismatchResult(pattern, value).breadCrumb(key).breadCrumb(FORM_FIELDS_BREADCRUMB)
                }
            }
            .firstOrNull { it is Failure }

        return when(result) {
            is Failure -> MatchFailure(result)
            else -> MatchSuccess(parameters)
        }
    }

    private fun matchHeaders(parameters: Triple): MatchingResult> {
        val (httpRequest, headersResolver, defaultResolver) = parameters
        val headers = httpRequest.headers
        when (val result = this.headersPattern.matches(headers, headersResolver ?: defaultResolver)) {
            is Failure -> return MatchFailure(result)
        }
        return MatchSuccess(Pair(httpRequest, defaultResolver))
    }

    private fun matchBody(parameters: Pair): MatchingResult> {
        val (httpRequest, resolver) = parameters

        val bodyValue = try {
            if (isPatternToken(httpRequest.bodyString)) StringValue(httpRequest.bodyString) else body.parse(httpRequest.bodyString, resolver)
        } catch (e: ContractException) {
            return MatchFailure(e.failure().breadCrumb("BODY"))
        }

        return when (val result = resolver.matchesPattern(null, body, bodyValue)) {
            is Failure -> MatchFailure(result.breadCrumb("BODY"))
            else -> MatchSuccess(parameters)
        }
    }

    private fun matchMethod(parameters: Pair): MatchingResult> {
        val (httpRequest, _) = parameters
        method.let {
            return if (it != httpRequest.method)
                MatchFailure(mismatchResult(method ?: "", httpRequest.method ?: "").breadCrumb("METHOD"))
            else
                MatchSuccess(parameters)
        }
    }

    private fun matchUrl(parameters: Pair): MatchingResult> {
        val (httpRequest, resolver) = parameters
        urlMatcher.let {
            val result = urlMatcher!!.matches(URI(httpRequest.path!!),
                    httpRequest.queryParams,
                    resolver)
            return if (result is Failure)
                MatchFailure(result.breadCrumb("URL"))
            else
                MatchSuccess(parameters)
        }
    }

    fun generate(request: HttpRequest, resolver: Resolver): HttpRequestPattern {
        var requestType = HttpRequestPattern()

        return attempt(breadCrumb = "REQUEST") {
            if (method == null) {
                throw missingParam("HTTP method")
            }
            if (urlMatcher == null) {
                throw missingParam("URL path")
            }

            requestType = requestType.copy(method = request.method)

            requestType = attempt(breadCrumb = "URL") {
                val path = request.path ?: ""
                val pathTypes = pathToPattern(path)
                val queryParamTypes = toTypeMap(request.queryParams, urlMatcher.queryPattern, resolver).mapKeys { it.key.removeSuffix("?") }

                requestType.copy(urlMatcher = URLMatcher(queryParamTypes, pathTypes, path))
            }

            requestType = attempt(breadCrumb = "HEADERS") {
                requestType.copy(headersPattern = HttpHeadersPattern(toTypeMap(request.headers, headersPattern.pattern, resolver)))
            }

            requestType = attempt(breadCrumb = "BODY") {
                requestType.copy(body = when(request.body) {
                    is StringValue -> encompassedType(request.bodyString, null, body, resolver)
                    else -> request.body.exactMatchElseType()
                })
            }

            requestType = attempt(breadCrumb = "FORM FIELDS") {
                requestType.copy(formFieldsPattern = toTypeMap(request.formFields, formFieldsPattern, resolver))
            }

            val multiPartFormDataRequestMap = request.multiPartFormData.fold(emptyMap()) { acc, part ->
                acc.plus(part.name to part)
            }

            attempt(breadCrumb = "MULTIPART DATA") {
                requestType.copy(multiPartFormDataPattern = multiPartFormDataPattern.filter {
                    withoutOptionality(it.name) in multiPartFormDataRequestMap
                }.map {
                    val key = withoutOptionality(it.name)
                    multiPartFormDataRequestMap.getValue(key).inferType()
                })
            }
        }
    }

    private fun toTypeMap(values: Map, types: Map, resolver: Resolver): Map {
        return types.filterKeys { withoutOptionality(it) in values }.mapValues {
            val key = withoutOptionality(it.key)
            val type = it.value

            attempt(breadCrumb = key) {
                val valueString = values.getValue(key)
                encompassedType(valueString, key, type, resolver)
            }
        }
    }

    private fun encompassedType(valueString: String, key: String?, type: Pattern, resolver: Resolver): Pattern {
        return when {
            isPatternToken(valueString) -> parsedPattern(valueString, key).let { parsedType ->
                when (val result = type.encompasses(parsedType, resolver, resolver)) {
                    is Success -> parsedType
                    else -> throw ContractException(resultReport(result))
                }
            }
            else -> type.parse(valueString, resolver).exactMatchElseType()
        }
    }

    fun generate(resolver: Resolver): HttpRequest {
        var newRequest = HttpRequest()

        return attempt(breadCrumb = "REQUEST") {
            if (method == null) {
                throw missingParam("HTTP method")
            }
            if (urlMatcher == null) {
                throw missingParam("URL path")
            }
            newRequest = newRequest.updateMethod(method)
            attempt(breadCrumb = "URL") {
                newRequest = newRequest.updatePath(urlMatcher.generatePath(resolver))
                val queryParams = urlMatcher.generateQuery(resolver)
                for (key in queryParams.keys) {
                    newRequest = newRequest.updateQueryParam(key, queryParams[key] ?: "")
                }
            }
            val headers = headersPattern.generate(resolver)

            val body = body
            attempt(breadCrumb = "BODY") {
                body.generate(resolver).let { value ->
                    newRequest = newRequest.updateBody(value)
                    newRequest = newRequest.updateHeader(CONTENT_TYPE, value.httpContentType)
                }
            }

            newRequest = newRequest.copy(headers = headers)

            val formFieldsValue = attempt(breadCrumb = "FORM FIELDS") { formFieldsPattern.mapValues { (key, pattern) -> attempt(breadCrumb = key) { resolver.generate(key, pattern).toString() } } }
            newRequest = when (formFieldsValue.size) {
                0 -> newRequest
                else -> newRequest.copy(
                        formFields = formFieldsValue,
                        headers = newRequest.headers.plus(CONTENT_TYPE to "application/x-www-form-urlencoded"))
            }

            val multipartData = attempt(breadCrumb = "MULTIPART DATA") { multiPartFormDataPattern.mapIndexed { index, multiPartFormDataPattern -> attempt(breadCrumb = "[$index]") { multiPartFormDataPattern.generate(resolver) } } }
            when(multipartData.size) {
                0 -> newRequest
                else -> newRequest.copy(
                        multiPartFormData = multipartData,
                        headers = newRequest.headers.plus(CONTENT_TYPE to "multipart/form-data")
                )
            }
        }
    }

    fun newBasedOn(row: Row, resolver: Resolver): List {
        return attempt(breadCrumb = "REQUEST") {
            val newURLMatchers = urlMatcher?.newBasedOn(row, resolver) ?: listOf(null)
            val newBodies = attempt(breadCrumb = "BODY") { body.newBasedOn(row, resolver) }
            val newHeadersPattern = headersPattern.newBasedOn(row, resolver)
            val newFormFieldsPatterns = newBasedOn(formFieldsPattern, row, resolver)
            val newFormDataPartLists = newMultiPartBasedOn(multiPartFormDataPattern, row, resolver)

            newURLMatchers.flatMap { newURLMatcher ->
                newBodies.flatMap { newBody ->
                    newHeadersPattern.flatMap { newHeadersPattern ->
                        newFormFieldsPatterns.flatMap { newFormFieldsPattern ->
                            newFormDataPartLists.map { newFormDataPartList ->
                                HttpRequestPattern(
                                        headersPattern = newHeadersPattern,
                                        urlMatcher = newURLMatcher,
                                        method = method,
                                        body = newBody,
                                        formFieldsPattern = newFormFieldsPattern,
                                        multiPartFormDataPattern = newFormDataPartList)
                            }
                        }
                    }
                }
            }
        }
    }

    override fun toString(): String {
        return "$method ${urlMatcher.toString()}"
    }
}

fun missingParam(missingValue: String): ContractException {
    return ContractException("$missingValue is missing. Can't generate the contract test.")
}

fun newMultiPartBasedOn(partList: List, row: Row, resolver: Resolver): List> {
    val values = partList.map { part ->
        attempt(breadCrumb = part.name) {
            part.newBasedOn(row, resolver)
        }
    }

    return multiPartListCombinations(values)
}

fun multiPartListCombinations(values: List>): List> {
    if(values.isEmpty())
        return listOf(emptyList())

    val value: List = values.last()
    val subLists = multiPartListCombinations(values.dropLast(1))

    return subLists.flatMap { list ->
        value.map { type ->
            when(type) {
                null -> list
                else -> list.plus(type)
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy