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

in.specmatic.core.HttpQueryParamPattern.kt Maven / Gradle / Ivy

Go to download

Turn your contracts into executable specifications. Contract Driven Development - Collaboratively Design & Independently Deploy MicroServices & MicroFrontends. Deprecation Notice for group ID "in.specmatic" ****************************************************************************************************** Updates for "specmatic-core" will no longer be available under the deprecated group ID "in.specmatic". Please update your dependencies to use the new group ID "io.specmatic". ******************************************************************************************************

There is a newer version: 1.3.39
Show newest version
package `in`.specmatic.core

import `in`.specmatic.core.pattern.*
import `in`.specmatic.core.utilities.URIUtils
import `in`.specmatic.core.value.JSONArrayValue
import `in`.specmatic.core.value.StringValue
import java.net.URI

const val QUERY_PARAMS_BREADCRUMB = "QUERY-PARAMS"

data class HttpQueryParamPattern(val queryPatterns: Map, val additionalProperties: Pattern? = null) {

    fun generate(resolver: Resolver): List> {
        return attempt(breadCrumb = "QUERY-PARAMS") {
            queryPatterns.map { it.key.removeSuffix("?") to it.value }.flatMap { (parameterName, pattern) ->
                attempt(breadCrumb = parameterName) {
                    val generatedValue =  resolver.withCyclePrevention(pattern) { it.generate(parameterName, pattern) }
                    if(generatedValue is JSONArrayValue) {
                        generatedValue.list.map { parameterName to it.toString() }
                    }
                    else {
                        listOf(parameterName to generatedValue.toString())
                    }
                }
            }.let { queryParamPairs ->
                if(additionalProperties == null)
                    queryParamPairs
                else
                    queryParamPairs.plus(randomString(5) to additionalProperties.generate(resolver).toStringLiteral())
            }
        }
    }

    fun newBasedOn(
        row: Row,
        resolver: Resolver
    ): Sequence> {
        val createdBasedOnExamples = attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) {
            val queryParams = queryPatterns.let {
                if(additionalProperties != null)
                    it.plus(randomString(5) to additionalProperties)
                else
                    it
            }

            val combinations = forEachKeyCombinationIn(row.withoutOmittedKeys(queryParams, resolver.defaultExampleResolver), row) { entry ->
                newBasedOn(entry, row, resolver)
            }

            combinations.map {
                it.mapKeys { withoutOptionality(it.key) }
            }
        }

        return createdBasedOnExamples
    }


    fun addComplimentaryPatterns(basePatterns: Sequence>, row: Row, resolver: Resolver): Sequence> {
        return addComplimentaryPatterns(
            basePatterns,
            queryPatterns,
            additionalProperties,
            row,
            resolver
        )
    }

    fun matches(httpRequest: HttpRequest, resolver: Resolver): Result {
        val queryParams = if(additionalProperties != null) {
            httpRequest.queryParams.withoutMatching(queryPatterns.keys, additionalProperties, resolver)
        } else {
            httpRequest.queryParams
        }

        val keyErrors =
            resolver.findKeyErrorList(queryPatterns, queryParams.asMap().mapValues { StringValue(it.value) })
        val keyErrorList: List = keyErrors.map {
            it.missingKeyToResult("query param", resolver.mismatchMessages).breadCrumb(it.name)
        }

        // 1. key is optional and request does not have the key as well
        // 2. key is mandatory and request does not have the key as well -> Result.Failure
        // 3. key in request but not in groupedPatternPairs -> Result.Failure
        // 4. key in request
        // A. key value pattern is an array
        // B. key value pattern is a scalar (not an array)
        // C. multiple pattern patternPairGroup with the same key


        // We don't need unmatched values when:
        // 1. Running contract tests
        // 2. Backward compatibility
        // 3. Stub
        // A. Setting expectation
        // B. Matching incoming request to a stub without expectations

        // Where we need unmatched values:
        // Matching incoming request to stubbed out API

        val results: List = queryPatterns.mapNotNull { (key, parameterPattern) ->
            val requestValues = queryParams.getValues(withoutOptionality(key))

            if (requestValues.isEmpty()) return@mapNotNull null

            val keyWithoutOptionality = withoutOptionality(key)

            val requestValuesList = JSONArrayValue(requestValues.map {
                StringValue(it)
            })

            resolver.matchesPattern(keyWithoutOptionality, parameterPattern, requestValuesList).breadCrumb(keyWithoutOptionality)

        }

        val failures = keyErrorList.plus(results).filterIsInstance()

        return if (failures.isNotEmpty())
            Result.Failure.fromFailures(failures).breadCrumb(QUERY_PARAMS_BREADCRUMB)
        else
            Result.Success()
    }

    fun newBasedOn(resolver: Resolver): Sequence> {
        return attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) {
            val queryParams = queryPatterns.let {
                if(additionalProperties != null)
                    it.plus(randomString(5) to additionalProperties)
                else
                    it
            }
            allOrNothingCombinationIn(queryParams) { entry ->
                newBasedOn(entry.mapKeys { withoutOptionality(it.key) }, resolver)
            }
        }
    }

    override fun toString(): String {
        return if (queryPatterns.isNotEmpty()) {
            "?" + queryPatterns.mapKeys { it.key.removeSuffix("?") }.map { (key, value) ->
                "$key=$value"
            }.toList().joinToString(separator = "&")
        } else ""
    }

    fun negativeBasedOn(row: Row, resolver: Resolver): Sequence>> {
        return attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) {
            val queryParams: Map = queryPatterns.let {
                if (additionalProperties != null)
                    it.plus(randomString(5) to additionalProperties)
                else
                    it
            }

            forEachKeyCombinationInR(queryParams, row) { entry ->
                NegativeNonStringlyPatterns().negativeBasedOn(
                    entry.mapKeys { withoutOptionality(it.key) },
                    row,
                    resolver
                ).map { it.breadCrumb("QUERY-PARAM") }
            }
        }
    }

    fun matches(uri: URI, queryParams: Map, resolver: Resolver = Resolver()): Result {
        return matches(HttpRequest(path = uri.path, queryParametersMap =  queryParams), resolver)
    }

    fun readFrom(row: Row, resolver: Resolver): Sequence> {
        return attempt(breadCrumb = QUERY_PARAMS_BREADCRUMB) {
            readFrom(queryPatterns, row, resolver)
        }
    }
    fun matches(row: Row, resolver: Resolver): Result {
        return matches(queryPatterns, row, resolver, "query param")
    }
}

internal fun buildQueryPattern(
    urlPattern: URI,
    apiKeyQueryParams: Set = emptySet()
): HttpQueryParamPattern {
    val queryPattern = URIUtils.parseQuery(urlPattern.query).mapKeys {
        "${it.key}?"
    }.mapValues {
        if (isPatternToken(it.value))
            QueryParameterScalarPattern(DeferredPattern(it.value, it.key))
        else
            QueryParameterScalarPattern(ExactValuePattern(StringValue(it.value)))
    }.let { queryParams ->
        apiKeyQueryParams.associate { apiKeyQueryParam ->
            Pair("${apiKeyQueryParam}?", StringPattern())
        }.plus(queryParams)
    }
    return HttpQueryParamPattern(queryPattern)
}

fun addComplimentaryPatterns(baseGeneratedPatterns: Sequence>, patterns: Map, additionalProperties: Pattern?, row: Row, resolver: Resolver): Sequence> {
    val generatedWithoutExamples: Sequence> =
        resolver
            .generation
            .fillInTheMissingMapPatterns(baseGeneratedPatterns, patterns, additionalProperties, row, resolver)
            .map {
                it.mapKeys { withoutOptionality(it.key) }
            }

    return baseGeneratedPatterns + generatedWithoutExamples
}

fun matches(patterns: Map, row: Row, resolver: Resolver, paramType: String): Result {
    val results = patterns.entries.fold(emptyList()) { results, (key, pattern) ->
        val withoutOptionality = withoutOptionality(key)

        if (row.containsField(withoutOptionality)) {
            val value = row.getField(withoutOptionality)
            val patternValue = resolver.parse(pattern, value)

            results.plus(resolver.matchesPattern(withoutOptionality, pattern, patternValue))
        } else if (isOptional(key)) {
            results.plus(Result.Success())
        } else {
            results.plus(Result.Failure("Mandatory $paramType $key not found in row"))
        }
    }

    return Result.fromResults(results)
}

fun readFrom(patterns: Map, row: Row, resolver: Resolver): Sequence> {
    val rowAsPattern = patterns.entries.fold(emptyMap()) { acc, (key, pattern) ->
        val withoutOptionality = withoutOptionality(key)

        if (row.containsField(withoutOptionality)) {
            val value = row.getField(withoutOptionality)
            val patternValue = resolver.parse(pattern, value)

            acc.plus(withoutOptionality to patternValue.exactMatchElseType())
        } else if (isOptional(key)) {
            acc
        } else {
            acc.plus(withoutOptionality to pattern.generate(resolver).exactMatchElseType())
        }
    }

    return sequenceOf(rowAsPattern)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy