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.core.HttpHeadersPattern.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.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
)