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

io.specmatic.core.HttpHeadersPattern.kt Maven / Gradle / Ivy

Go to download

Turn your contracts into executable specifications. Contract Driven Development - Collaboratively Design & Independently Deploy MicroServices & MicroFrontends.

There is a newer version: 2.0.37
Show newest version
package io.specmatic.core

import io.specmatic.core.pattern.*
import io.specmatic.core.value.JSONObjectValue
import io.specmatic.core.value.StringValue
import io.specmatic.core.value.Value
import io.ktor.http.*

const val HEADERS_BREADCRUMB = "HEADERS"

data class HttpHeadersPattern(
    val pattern: Map = emptyMap(),
    val ancestorHeaders: Map? = null,
    val contentType: String? = null
) {
    init {
        val uniqueHeaders = pattern.keys.map { it.lowercase() }.distinct()
        if (uniqueHeaders.size < pattern.size) {
            throw ContractException("Headers are not unique: ${pattern.keys.joinToString(", ")}")
        }
    }

    fun isEmpty(): Boolean {
        return pattern.isEmpty()
    }

    fun matches(headers: Map, resolver: Resolver): Result {
        val result = headers to resolver to
                ::matchEach otherwise
                ::handleError toResult
                ::returnResult

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

    private fun matchEach(parameters: Pair, Resolver>): MatchingResult, Resolver>> {
        val (headers, resolver) = parameters

        val contentTypeHeaderValue = headers["Content-Type"]

        if(contentType != null && contentTypeHeaderValue != null) {
            val parsedContentType = simplifiedContentType(contentType.lowercase())
            val parsedContentTypeHeaderValue  = simplifiedContentType(contentTypeHeaderValue.lowercase())

            if(parsedContentType != parsedContentTypeHeaderValue)
                return MatchFailure(
                    Result.Failure(
                        resolver.mismatchMessages.mismatchMessage(contentType, contentTypeHeaderValue),
                        breadCrumb = "Content-Type",
                        failureReason = FailureReason.ContentTypeMismatch
                    )
                )
        }

        val headersWithRelevantKeys = when {
            ancestorHeaders != null -> withoutIgnorableHeaders(headers, ancestorHeaders)
            else -> withoutContentTypeGeneratedBySpecmatic(headers, pattern)
        }

        val keyErrors: List =
            resolver.withUnexpectedKeyCheck(IgnoreUnexpectedKeys).findKeyErrorListCaseInsensitive(
                pattern,
                headersWithRelevantKeys.mapValues { StringValue(it.value) }
            )

        keyErrors.find { it.name == "SOAPAction" }?.apply {
            return MatchFailure(
                this.missingKeyToResult("header", resolver.mismatchMessages).breadCrumb("SOAPAction")
                    .copy(failureReason = FailureReason.SOAPActionMismatch)
            )
        }

        val keyErrorResults: List = keyErrors.map {
            it.missingKeyToResult("header", resolver.mismatchMessages).breadCrumb(withoutOptionality(it.name))
        }

        val lowercasedHeadersWithRelevantKeys = headersWithRelevantKeys.mapKeys { it.key.lowercase() }

        val results: List = this.pattern.mapKeys { it.key }.map { (key, pattern) ->
            val keyWithoutOptionality = withoutOptionality(key)
            val sampleValue = lowercasedHeadersWithRelevantKeys[keyWithoutOptionality.lowercase()]

            when {
                sampleValue != null -> {
                    try {
                        val result = resolver.matchesPattern(
                            keyWithoutOptionality,
                            pattern,
                            attempt(breadCrumb = keyWithoutOptionality) {
                                parseOrString(
                                    pattern,
                                    sampleValue,
                                    resolver
                                )
                            })

                        result.breadCrumb(keyWithoutOptionality).failureReason(highlightIfSOAPActionMismatch(key))
                    } catch (e: ContractException) {
                        e.failure().copy(failureReason = highlightIfSOAPActionMismatch(key))
                    } catch (e: Throwable) {
                        Result.Failure(e.localizedMessage, breadCrumb = keyWithoutOptionality)
                            .copy(failureReason = highlightIfSOAPActionMismatch(key))
                    }
                }

                else ->
                    null
            }
        }

        val failures: List = keyErrorResults.plus(results.filterIsInstance())

        return if (failures.isNotEmpty())
            MatchFailure(Result.Failure.fromFailures(failures))
        else
            MatchSuccess(parameters)
    }

    private fun simplifiedContentType(contentType: String): String {
        return try {
            ContentType.parse(contentType).let {
                "${it.contentType}/${it.contentSubtype}"
            }
        } catch (e: Throwable) {
            contentType
        }
    }

    private fun highlightIfSOAPActionMismatch(missingKey: String): FailureReason? =
        when (withoutOptionality(missingKey)) {
            "SOAPAction" -> FailureReason.SOAPActionMismatch
            else -> null
        }

    private fun withoutIgnorableHeaders(
        headers: Map,
        ancestorHeaders: Map
    ): Map {
        val ancestorHeadersLowerCase = ancestorHeaders.mapKeys { it.key.lowercase() }

        return headers.filterKeys { key ->
            val headerWithoutOptionality = withoutOptionality(key).lowercase()
            ancestorHeadersLowerCase.containsKey(headerWithoutOptionality) || ancestorHeadersLowerCase.containsKey("$headerWithoutOptionality?")
        }
    }

    private fun withoutContentTypeGeneratedBySpecmatic(
        headers: Map,
        pattern: Map
    ): Map {
        val contentTypeHeader = "Content-Type"
        return when {
            contentTypeHeader in headers && contentTypeHeader !in pattern && "$contentTypeHeader?" !in pattern -> headers.minus(
                contentTypeHeader
            )

            else -> headers
        }
    }

    fun generate(resolver: Resolver): Map {
        val headers = pattern.mapValues { (key, pattern) ->
            attempt(breadCrumb = "HEADERS.$key") {
                toStringLiteral(resolver.withCyclePrevention(pattern) { it.generate(key, pattern) })
            }
        }.map { (key, value) -> withoutOptionality(key) to value }.toMap()
        if (contentType.isNullOrBlank()) return headers
        return headers.plus(CONTENT_TYPE to contentType)
    }

    private fun toStringLiteral(headerValue: Value) = when (headerValue) {
        is JSONObjectValue -> headerValue.toUnformattedStringLiteral()
        else -> headerValue.toStringLiteral()
    }

    fun generateWithAll(resolver: Resolver): Map {
        return attempt(breadCrumb = "HEADERS") {
            pattern.mapValues { (key, pattern) ->
                attempt(breadCrumb = key) {
                    pattern.generateWithAll(resolver).toStringLiteral()
                }
            }
        }.map { (key, value) -> withoutOptionality(key) to value }.toMap()
    }

    fun newBasedOn(row: Row, resolver: Resolver): Sequence> {
        val basedOnExamples = forEachKeyCombinationGivenRowIn(
            row.withoutOmittedKeys(pattern, resolver.defaultExampleResolver),
            row,
            resolver
        ) { pattern ->
            newMapBasedOn(pattern, row, resolver)
        }

        val generatedWithoutExamples: Sequence>> = resolver.generation.fillInTheMissingMapPatterns(
            basedOnExamples.map { it.value },
            pattern,
            null,
            row,
            resolver
        )

        return (basedOnExamples + generatedWithoutExamples).map { example ->
            example.update { map ->
                map.mapKeys { withoutOptionality(it.key) }
            }.ifValue {
                HttpHeadersPattern(it, contentType = contentType)
            }
        }
    }

    fun negativeBasedOn(row: Row, resolver: Resolver): Sequence> {
        return allOrNothingCombinationIn(pattern, row, null, null) { pattern ->
            NegativeNonStringlyPatterns().negativeBasedOn(pattern, row, resolver).map { it.breadCrumb("HEADER") }
        }.map { patternMapR ->
            patternMapR.ifValue { patternMap ->
                HttpHeadersPattern(
                    patternMap.mapKeys { withoutOptionality(it.key) },
                    contentType = contentType
                )
            }
        }
    }

    fun newBasedOn(resolver: Resolver): Sequence =
        allOrNothingCombinationIn(
            pattern,
            Row(),
            null,
            null, returnValues { pattern: Map ->
                newBasedOn(pattern, resolver)
            }).map { it.value }.map { patternMap ->
            HttpHeadersPattern(
                patternMap.mapKeys { withoutOptionality(it.key) },
                contentType = contentType
            )
        }

    fun encompasses(other: HttpHeadersPattern, thisResolver: Resolver, otherResolver: Resolver): Result {
        val myRequiredKeys = pattern.keys.filter { !isOptional(it) }
        val otherRequiredKeys = other.pattern.keys.filter { !isOptional(it) }

        val missingHeaderResult: Result = checkAllMissingHeaders(myRequiredKeys, otherRequiredKeys, thisResolver)

        val otherWithoutOptionality = other.pattern.mapKeys { withoutOptionality(it.key) }
        val thisWithoutOptionality = pattern.filterKeys { withoutOptionality(it) in otherWithoutOptionality }
            .mapKeys { withoutOptionality(it.key) }

        val valueResults: List =
            thisWithoutOptionality.keys.map { headerName ->
                thisWithoutOptionality.getValue(headerName).encompasses(
                    resolvedHop(otherWithoutOptionality.getValue(headerName), otherResolver),
                    thisResolver,
                    otherResolver
                ).breadCrumb(headerName)
            }

        val results = listOf(missingHeaderResult).plus(valueResults)

        return Result.fromResults(results).breadCrumb("HEADER")
    }

    private fun checkAllMissingHeaders(
        myRequiredKeys: List,
        otherRequiredKeys: List,
        resolver: Resolver
    ): Result {
        val failures = myRequiredKeys.filter { it !in otherRequiredKeys }.map { missingFixedKey ->
            MissingKeyError(missingFixedKey).missingKeyToResult("header", resolver.mismatchMessages)
                .breadCrumb(missingFixedKey)
        }

        return Result.fromFailures(failures)
    }

    fun addComplimentaryPatterns(basePatterns: Sequence>, row: Row, resolver: Resolver): Sequence> {
        return addComplimentaryPatterns(
            basePatterns.map { it.ifValue { it.pattern } },
            pattern,
            null,
            row,
            resolver,
        ).map {
            it.ifValue {
                HttpHeadersPattern(it, contentType = contentType)
            }
        }
    }

    fun matches(row: Row, resolver: Resolver): Result {
        return matches(this.pattern, row, resolver, "header")
    }

    fun readFrom(row: Row, resolver: Resolver): Sequence> {
        return attempt(breadCrumb = HEADERS_BREADCRUMB) {
            readFrom(this.pattern, row, resolver)
        }.map {
            HasValue(HttpHeadersPattern(it, contentType = contentType))
        }
    }

    fun fillInTheBlanks(headers: Map, dictionary: Dictionary, resolver: Resolver): ReturnValue> {
        val headersToConsider = ancestorHeaders?.let {
            headers.filterKeys { key -> key in it || "$key?" in it }
        } ?: headers

        val map: Map> = headersToConsider.mapValues { (headerName, headerValue) ->
            val headerPattern = pattern.get(headerName) ?: pattern.get("$headerName?") ?: return@mapValues HasFailure(Result.Failure(resolver.mismatchMessages.unexpectedKey("header", headerName)))

            if(dictionary.contains(headerName)) {
                val dictionaryValue = dictionary.lookup(headerName)
                val matchResult = headerPattern.matches(dictionaryValue, resolver)

                if(matchResult is Result.Failure)
                    HasFailure(matchResult)
                else
                    HasValue(dictionaryValue!!.toStringLiteral())
            } else {
                exception { headerPattern.parse(headerValue, resolver) }?.let { return@mapValues HasException(it) }

                HasValue(headerValue)
            }.breadCrumb(headerName)
        }

        val headersInPartialR = map.mapFold()

        val missingHeadersR = pattern.filterKeys { !it.endsWith("?") && it !in headers }.mapValues { (headerName, headerPattern) ->
            val generatedValue = dictionary.lookup(headerName)?.let { dictionaryValue ->
                val matchResult = headerPattern.matches(dictionaryValue, resolver)
                if(matchResult is Result.Failure)
                    HasFailure(matchResult)
                else
                    HasValue(dictionaryValue.toStringLiteral())
            } ?: HasValue(headerPattern.generate(resolver).toStringLiteral())

            generatedValue.breadCrumb(headerName)
        }.mapFold()

        return headersInPartialR.combine(missingHeadersR) { headersInPartial, missingHeaders ->
            headersInPartial + missingHeaders
        }
    }
}

private fun parseOrString(pattern: Pattern, sampleValue: String, resolver: Resolver) =
    try {
        pattern.parse(sampleValue, resolver)
    } catch (e: Throwable) {
        StringValue(sampleValue)
    }

fun Map.withoutDynamicHeaders(): Map =
    this.filterKeys { key -> key.lowercase() !in DYNAMIC_HTTP_HEADERS.map { it.lowercase() } }

val DYNAMIC_HTTP_HEADERS = listOf(
    HttpHeaders.Authorization,
    HttpHeaders.UserAgent,
    HttpHeaders.Cookie,
    HttpHeaders.Referrer,
    HttpHeaders.AcceptLanguage,
    HttpHeaders.Host,
    HttpHeaders.IfModifiedSince,
    HttpHeaders.IfNoneMatch,
    HttpHeaders.CacheControl,
    HttpHeaders.ContentLength,
    HttpHeaders.Range,
    HttpHeaders.XForwardedFor,
    HttpHeaders.Date,
    HttpHeaders.Server,
    HttpHeaders.Expires,
    HttpHeaders.LastModified,
    HttpHeaders.ETag,
    HttpHeaders.Vary,
    HttpHeaders.AccessControlAllowCredentials,
    HttpHeaders.AccessControlMaxAge,
    HttpHeaders.AccessControlRequestHeaders,
    HttpHeaders.AccessControlRequestMethod
)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy