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

io.specmatic.conversions.OpenApiSpecification.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.conversions

import com.fasterxml.jackson.databind.node.ArrayNode
import io.cucumber.messages.internal.com.fasterxml.jackson.databind.ObjectMapper
import io.cucumber.messages.types.Step
import io.ktor.util.reflect.*
import io.specmatic.core.DEFAULT_RESPONSE_CODE
import io.specmatic.core.Feature
import io.specmatic.core.HttpHeadersPattern
import io.specmatic.core.HttpPathPattern
import io.specmatic.core.HttpQueryParamPattern
import io.specmatic.core.HttpRequest
import io.specmatic.core.HttpRequestPattern
import io.specmatic.core.HttpResponse
import io.specmatic.core.HttpResponsePattern
import io.specmatic.core.MatchFailure
import io.specmatic.core.MatchSuccess
import io.specmatic.core.MatchingResult
import io.specmatic.core.MultiPartContentPattern
import io.specmatic.core.MultiPartFilePattern
import io.specmatic.core.MultiPartFormDataPattern
import io.specmatic.core.NoBodyPattern
import io.specmatic.core.NoBodyValue
import io.specmatic.core.OMIT
import io.specmatic.core.Resolver
import io.specmatic.core.Result
import io.specmatic.core.Result.Failure
import io.specmatic.core.Scenario
import io.specmatic.core.ScenarioInfo
import io.specmatic.core.SecurityConfiguration
import io.specmatic.core.SecuritySchemeConfiguration
import io.specmatic.core.SpecmaticConfig
import io.specmatic.core.URLPathSegmentPattern
import io.specmatic.core.handleError
import io.specmatic.core.log.LogStrategy
import io.specmatic.core.log.logger
import io.specmatic.core.otherwise
import io.specmatic.core.pattern.AnyNonNullJSONValue
import io.specmatic.core.pattern.AnyPattern
import io.specmatic.core.pattern.AnythingPattern
import io.specmatic.core.pattern.Base64StringPattern
import io.specmatic.core.pattern.BinaryPattern
import io.specmatic.core.pattern.BooleanPattern
import io.specmatic.core.pattern.ContractException
import io.specmatic.core.pattern.DatePattern
import io.specmatic.core.pattern.DateTimePattern
import io.specmatic.core.pattern.DeferredPattern
import io.specmatic.core.pattern.DictionaryPattern
import io.specmatic.core.pattern.EmailPattern
import io.specmatic.core.pattern.EnumPattern
import io.specmatic.core.pattern.ExactValuePattern
import io.specmatic.core.pattern.Examples
import io.specmatic.core.pattern.JSONObjectPattern
import io.specmatic.core.pattern.ListPattern
import io.specmatic.core.pattern.NullPattern
import io.specmatic.core.pattern.NumberPattern
import io.specmatic.core.pattern.Pattern
import io.specmatic.core.pattern.PatternInStringPattern
import io.specmatic.core.pattern.QueryParameterArrayPattern
import io.specmatic.core.pattern.QueryParameterScalarPattern
import io.specmatic.core.pattern.ResponseExample
import io.specmatic.core.pattern.ResponseValueExample
import io.specmatic.core.pattern.Row
import io.specmatic.core.pattern.StringPattern
import io.specmatic.core.pattern.TYPE_ATTRIBUTE_NAME
import io.specmatic.core.pattern.UUIDPattern
import io.specmatic.core.pattern.XMLPattern
import io.specmatic.core.pattern.XMLTypeData
import io.specmatic.core.pattern.attempt
import io.specmatic.core.pattern.parsedJSON
import io.specmatic.core.pattern.toJSONObjectPattern
import io.specmatic.core.pattern.withoutOptionality
import io.specmatic.core.then
import io.specmatic.core.to
import io.specmatic.core.utilities.Flags
import io.specmatic.core.utilities.Flags.Companion.IGNORE_INLINE_EXAMPLE_WARNINGS
import io.specmatic.core.utilities.Flags.Companion.getBooleanValue
import io.specmatic.core.utilities.capitalizeFirstChar
import io.specmatic.core.value.JSONObjectValue
import io.specmatic.core.value.NullValue
import io.specmatic.core.value.NumberValue
import io.specmatic.core.value.StringValue
import io.specmatic.core.value.Value
import io.specmatic.core.wsdl.parser.message.MULTIPLE_ATTRIBUTE_VALUE
import io.specmatic.core.wsdl.parser.message.OCCURS_ATTRIBUTE_NAME
import io.specmatic.core.wsdl.parser.message.OPTIONAL_ATTRIBUTE_VALUE
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.Operation
import io.swagger.v3.oas.models.PathItem
import io.swagger.v3.oas.models.examples.Example
import io.swagger.v3.oas.models.headers.Header
import io.swagger.v3.oas.models.media.*
import io.swagger.v3.oas.models.parameters.HeaderParameter
import io.swagger.v3.oas.models.parameters.Parameter
import io.swagger.v3.oas.models.parameters.PathParameter
import io.swagger.v3.oas.models.parameters.QueryParameter
import io.swagger.v3.oas.models.parameters.RequestBody
import io.swagger.v3.oas.models.responses.ApiResponse
import io.swagger.v3.oas.models.responses.ApiResponses
import io.swagger.v3.oas.models.security.SecurityScheme
import io.swagger.v3.parser.OpenAPIV3Parser
import io.swagger.v3.parser.core.models.ParseOptions
import io.swagger.v3.parser.core.models.SwaggerParseResult
import java.io.File

private const val BEARER_SECURITY_SCHEME = "bearer"
const val SERVICE_TYPE_HTTP = "HTTP"

const val testDirectoryEnvironmentVariable = "SPECMATIC_TESTS_DIRECTORY"
const val testDirectoryProperty = "specmaticTestsDirectory"

const val NO_SECURITY_SCHEMA_IN_SPECIFICATION = "NO-SECURITY-SCHEME-IN-SPECIFICATION"

var missingRequestExampleErrorMessageForTest: String = "WARNING: Ignoring response example named %s for test or stub data, because no associated request example named %s was found."
var missingResponseExampleErrorMessageForTest: String = "WARNING: Ignoring request example named %s for test or stub data, because no associated response example named %s was found."

internal fun missingRequestExampleErrorMessageForTest(exampleName: String): String =
    missingRequestExampleErrorMessageForTest.format(exampleName, exampleName)

internal fun missingResponseExampleErrorMessageForTest(exampleName: String): String =
    missingResponseExampleErrorMessageForTest.format(exampleName, exampleName)

private const val SPECMATIC_TEST_WITH_NO_REQ_EX = "SPECMATIC-TEST-WITH-NO-REQ-EX"

class OpenApiSpecification(
    private val openApiFilePath: String,
    private val parsedOpenApi: OpenAPI,
    private val sourceProvider: String? = null,
    private val sourceRepository: String? = null,
    private val sourceRepositoryBranch: String? = null,
    private val specificationPath: String? = null,
    private val securityConfiguration: SecurityConfiguration? = null,
    private val specmaticConfig: SpecmaticConfig = SpecmaticConfig()
) : IncludedSpecification, ApiSpecification {
    init {
        logger.log(openApiSpecificationInfo(openApiFilePath, parsedOpenApi))
    }

    companion object {

        fun fromFile(openApiFilePath: String, relativeTo: String = ""): OpenApiSpecification {
            val openApiFile = File(openApiFilePath).let { openApiFile ->
                if (openApiFile.isAbsolute) {
                    openApiFile
                } else {
                    File(relativeTo).canonicalFile.parentFile.resolve(openApiFile)
                }
            }

            return fromFile(openApiFile.canonicalPath)
        }

        fun fromFile(openApiFilePath: String): OpenApiSpecification {
            return fromFile(openApiFilePath, SpecmaticConfig())
        }

        fun fromFile(openApiFilePath: String, specmaticConfig: SpecmaticConfig): OpenApiSpecification {
            return OpenApiSpecification(openApiFilePath, getParsedOpenApi(openApiFilePath), specmaticConfig = specmaticConfig)
        }

        fun getParsedOpenApi(openApiFilePath: String): OpenAPI {
            return OpenAPIV3Parser().read(openApiFilePath, null, resolveExternalReferences())
        }

        fun isParsable(openApiFilePath: String): Boolean {
            return OpenAPIV3Parser().read(openApiFilePath, null, resolveExternalReferences()) != null
        }

        fun fromYAML(
            yamlContent: String,
            openApiFilePath: String,
            loggerForErrors: LogStrategy = logger,
            sourceProvider: String? = null,
            sourceRepository: String? = null,
            sourceRepositoryBranch: String? = null,
            specificationPath: String? = null,
            securityConfiguration: SecurityConfiguration? = null,
            specmaticConfig: SpecmaticConfig = SpecmaticConfig()
        ): OpenApiSpecification {
            val parseResult: SwaggerParseResult =
                OpenAPIV3Parser().readContents(yamlContent, null, resolveExternalReferences(), openApiFilePath)
            val parsedOpenApi: OpenAPI? = parseResult.openAPI

            if (parsedOpenApi == null) {
                logger.debug("Failed to parse OpenAPI from file $openApiFilePath\n\n$yamlContent")

                printMessages(parseResult, openApiFilePath, loggerForErrors)

                throw ContractException("Could not parse contract $openApiFilePath, please validate the syntax using https://editor.swagger.io")
            } else if (parseResult.messages?.isNotEmpty() == true) {
                logger.log("The OpenAPI file $openApiFilePath was read successfully but with some issues")

                printMessages(parseResult, openApiFilePath, loggerForErrors)
            }

            return OpenApiSpecification(
                openApiFilePath,
                parsedOpenApi,
                sourceProvider,
                sourceRepository,
                sourceRepositoryBranch,
                specificationPath,
                securityConfiguration,
                specmaticConfig
            )
        }

        private fun printMessages(parseResult: SwaggerParseResult, filePath: String, loggerForErrors: LogStrategy) {
            parseResult.messages.filterNotNull().let {
                if (it.isNotEmpty()) {
                    val parserMessages = parseResult.messages.joinToString(System.lineSeparator())
                    loggerForErrors.log("Error parsing file $filePath")
                    loggerForErrors.log(parserMessages.prependIndent("  "))
                }
            }
        }

        private fun resolveExternalReferences(): ParseOptions = ParseOptions().also { it.isResolve = true }
    }

    val patterns = mutableMapOf()

    fun isOpenAPI31(): Boolean {
        return parsedOpenApi.openapi.startsWith("3.1")
    }

    fun toFeature(): Feature {
        val name = File(openApiFilePath).name

        val (scenarioInfos, stubsFromExamples) = toScenarioInfos()

        return Feature(
            scenarioInfos.map { Scenario(it) }, name = name, path = openApiFilePath, sourceProvider = sourceProvider,
            sourceRepository = sourceRepository,
            sourceRepositoryBranch = sourceRepositoryBranch,
            specification = specificationPath,
            serviceType = SERVICE_TYPE_HTTP,
            stubsFromExamples = stubsFromExamples,
            specmaticConfig = specmaticConfig
        )
    }

    override fun toScenarioInfos(): Pair, Map>>> {
        val (
            scenarioInfos: List,
            examplesAsExpectations: Map>>
        ) = openApiToScenarioInfos()

        return scenarioInfos.filter { it.httpResponsePattern.status > 0 } to examplesAsExpectations
    }

    override fun matches(
        specmaticScenarioInfo: ScenarioInfo, steps: List
    ): List {
        val (openApiScenarioInfos, _) = openApiToScenarioInfos()
        if (openApiScenarioInfos.isEmpty() || steps.isEmpty()) return listOf(specmaticScenarioInfo)
        val result: MatchingResult>> =
            specmaticScenarioInfo to openApiScenarioInfos to ::matchesPath then ::matchesMethod then ::matchesStatus then ::updateUrlMatcher otherwise ::handleError
        when (result) {
            is MatchFailure -> throw ContractException(result.error.message)
            is MatchSuccess -> return result.value.second
        }
    }

    private fun matchesPath(parameters: Pair>): MatchingResult>> {
        val (specmaticScenarioInfo, openApiScenarioInfos) = parameters

        // exact + exact   -> values should be equal
        // exact + pattern -> error
        // pattern + exact -> pattern should match exact
        // pattern + pattern -> both generated concrete values should be of same type

        val matchingScenarioInfos = specmaticScenarioInfo.matchesGherkinWrapperPath(openApiScenarioInfos, this)

        return when {
            matchingScenarioInfos.isEmpty() -> MatchFailure(
                Failure(
                    """Scenario: "${specmaticScenarioInfo.scenarioName}" PATH: "${
                        specmaticScenarioInfo.httpRequestPattern.httpPathPattern!!.generate(Resolver())
                    }" is not as per included wsdl / OpenApi spec"""
                )
            )

            else -> MatchSuccess(specmaticScenarioInfo to matchingScenarioInfos)
        }
    }

    override fun patternMatchesExact(
        wrapperURLPart: URLPathSegmentPattern,
        openapiURLPart: URLPathSegmentPattern,
        resolver: Resolver,
    ): Boolean {
        val valueFromWrapper = (wrapperURLPart.pattern as ExactValuePattern).pattern

        val valueToMatch: Value =
            if (valueFromWrapper is StringValue) {
                openapiURLPart.pattern.parse(valueFromWrapper.toStringLiteral(), resolver)
            } else {
                wrapperURLPart.pattern.pattern
            }

        return openapiURLPart.pattern.matches(valueToMatch, resolver) is Result.Success
    }

    override fun exactValuePatternsAreEqual(
        openapiURLPart: URLPathSegmentPattern,
        wrapperURLPart: URLPathSegmentPattern
    ) =
        (openapiURLPart.pattern as ExactValuePattern).pattern.toStringLiteral() == (wrapperURLPart.pattern as ExactValuePattern).pattern.toStringLiteral()

    private fun matchesMethod(parameters: Pair>): MatchingResult>> {
        val (specmaticScenarioInfo, openApiScenarioInfos) = parameters

        val matchingScenarioInfos =
            openApiScenarioInfos.filter { it.httpRequestPattern.method == specmaticScenarioInfo.httpRequestPattern.method }

        return when {
            matchingScenarioInfos.isEmpty() -> MatchFailure(
                Failure(
                    """Scenario: "${specmaticScenarioInfo.scenarioName}" METHOD: "${
                        specmaticScenarioInfo.httpRequestPattern.method
                    }" is not as per included wsdl / OpenApi spec"""
                )
            )

            else -> MatchSuccess(specmaticScenarioInfo to matchingScenarioInfos)
        }
    }

    private fun matchesStatus(parameters: Pair>): MatchingResult>> {
        val (specmaticScenarioInfo, openApiScenarioInfos) = parameters

        val matchingScenarioInfos =
            openApiScenarioInfos.filter { it.httpResponsePattern.status == specmaticScenarioInfo.httpResponsePattern.status }

        return when {
            matchingScenarioInfos.isEmpty() -> MatchFailure(
                Failure(
                    """Scenario: "${specmaticScenarioInfo.scenarioName}" RESPONSE STATUS: "${
                        specmaticScenarioInfo.httpResponsePattern.status
                    }" is not as per included wsdl / OpenApi spec"""
                )
            )

            else -> MatchSuccess(specmaticScenarioInfo to matchingScenarioInfos)
        }
    }

    private fun updateUrlMatcher(parameters: Pair>): MatchingResult>> {
        val (specmaticScenarioInfo, openApiScenarioInfos) = parameters

        return MatchSuccess(specmaticScenarioInfo to openApiScenarioInfos.map { openApiScenario ->
            val queryPattern = openApiScenario.httpRequestPattern.httpQueryParamPattern.queryPatterns
            val zippedPathPatterns =
                (specmaticScenarioInfo.httpRequestPattern.httpPathPattern?.pathSegmentPatterns ?: emptyList()).zip(
                    openApiScenario.httpRequestPattern.httpPathPattern?.pathSegmentPatterns ?: emptyList()
                )

            val pathPatterns = zippedPathPatterns.map { (fromWrapper, fromOpenApi) ->
                if (fromWrapper.pattern is ExactValuePattern)
                    fromWrapper
                else
                    fromOpenApi.copy(key = fromWrapper.key)
            }

            val httpPathPattern =
                HttpPathPattern(pathPatterns, openApiScenario.httpRequestPattern.httpPathPattern?.path ?: "")
            val httpQueryParamPattern = HttpQueryParamPattern(queryPattern)

            val httpRequestPattern = openApiScenario.httpRequestPattern.copy(
                httpPathPattern = httpPathPattern,
                httpQueryParamPattern = httpQueryParamPattern
            )
            openApiScenario.copy(httpRequestPattern = httpRequestPattern)
        })
    }

    data class RequestPatternsData(val requestPattern: HttpRequestPattern, val examples: Map>, val original: Pair? = null)

    private fun openApiToScenarioInfos(): Pair, Map>>> {
        val data: List, Map>>>> =
            openApiPaths().map { (openApiPath, pathItem) ->
                val scenariosAndExamples = openApiOperations(pathItem).map { (httpMethod, openApiOperation) ->
                    try {
                        openApiOperation.validateParameters()
                    } catch (e: ContractException) {
                        throw ContractException("In $httpMethod $openApiPath: ${e.message}")
                    }

                    val operation = openApiOperation.operation

                    val specmaticPathParam = toSpecmaticPathParam(openApiPath, operation)
                    val specmaticQueryParam = toSpecmaticQueryParam(operation)

                    val httpResponsePatterns: List =
                        attempt(breadCrumb = "$httpMethod $openApiPath -> RESPONSE") {
                            toHttpResponsePatterns(operation.responses)
                        }

                    val first2xxResponseStatus =
                        httpResponsePatterns.filter { it.responsePattern.status.toString().startsWith("2") }
                            .minOfOrNull { it.responsePattern.status }

                    val firstNoBodyResponseStatus =
                        httpResponsePatterns.filter { it.responsePattern.body is NoBodyPattern }
                            .minOfOrNull { it.responsePattern.status }

                    val httpResponsePatternsGrouped = httpResponsePatterns.groupBy { it.responsePattern.status }

                    val httpRequestPatterns: List =
                        attempt("In $httpMethod $openApiPath request") {
                            toHttpRequestPatterns(
                                specmaticPathParam, specmaticQueryParam, httpMethod, operation
                            )
                        }

                    val httpRequestPatternDataGroupedByContentType = httpRequestPatterns.groupBy {
                        it.requestPattern.headersPattern.contentType
                    }

                    val requestMediaTypes = httpRequestPatternDataGroupedByContentType.keys

                    val requestResponsePairs = httpResponsePatternsGrouped.flatMap { (status, responses) ->
                        val responsesGrouped = responses.groupBy {
                            it.responsePattern.headersPattern.contentType
                        }

                        if (responsesGrouped.keys.filterNotNull().toSet() == requestMediaTypes.filterNotNull().toSet()) {
                            responsesGrouped.map { (contentType, responsesData) ->
                                httpRequestPatternDataGroupedByContentType.getValue(contentType)
                                    .single() to responsesData.single()
                            }
                        } else {
                            responses.flatMap { responsePatternData ->
                                httpRequestPatterns.map { requestPatternData ->
                                    requestPatternData to responsePatternData
                                }
                            }
                        }

                    }

                    val scenarioInfos = requestResponsePairs.map { (requestPatternData, responsePatternData) ->
                        val (httpRequestPattern, requestExamples: Map>, openApiRequest) = requestPatternData
                        val (response, responseMediaType: MediaType, httpResponsePattern, responseExamples: Map) = responsePatternData

                        val specmaticExampleRows: List =
                            testRowsFromExamples(responseExamples, requestExamples, operation, openApiRequest, first2xxResponseStatus)
                        val scenarioName = scenarioName(operation, response, httpRequestPattern)

                        val ignoreFailure = operation.tags.orEmpty().map { it.trim() }.contains("WIP")

                        val rowsToBeUsed: List = specmaticExampleRows

                        ScenarioInfo(
                            scenarioName = scenarioName,
                            patterns = patterns.toMap(),
                            httpRequestPattern = httpRequestPattern,
                            httpResponsePattern = httpResponsePattern,
                            ignoreFailure = ignoreFailure,
                            examples = rowsToExamples(rowsToBeUsed),
                            sourceProvider = sourceProvider,
                            sourceRepository = sourceRepository,
                            sourceRepositoryBranch = sourceRepositoryBranch,
                            specification = specificationPath,
                            serviceType = SERVICE_TYPE_HTTP
                        )
                    }

                    val responseExamplesList = httpResponsePatterns.map { it.examples }

                    val requestExamples = httpRequestPatterns.map {
                        it.examples
                    }.foldRight(emptyMap>()) { acc, map ->
                        acc.plus(map)
                    }

                    val examples =
                        collateExamplesForExpectations(requestExamples, responseExamplesList, httpRequestPatterns)

                    val requestExampleNames = requestExamples.keys

                    val usedExamples = examples.keys

                    val unusedRequestExampleNames = requestExampleNames - usedExamples

                    val responseThatReturnsNoValues = httpResponsePatterns.find { responsePatternData ->
                        responsePatternData.responsePattern.body == NoBodyPattern
                                && responsePatternData.responsePattern.status == firstNoBodyResponseStatus
                    }

                    val (additionalExamples, updatedScenarios)
                            = when {
                                responseThatReturnsNoValues != null && unusedRequestExampleNames.isNotEmpty() -> {
                                    getUpdatedScenarioInfosWithNoBodyResponseExamples(
                                        responseThatReturnsNoValues,
                                        requestExamples,
                                        unusedRequestExampleNames,
                                        scenarioInfos,
                                        operation,
                                        firstNoBodyResponseStatus
                                    )
                                }

                                else -> emptyMap>>() to scenarioInfos
                            }

                    Triple(updatedScenarios, examples + additionalExamples, requestExampleNames)
                }

                val requestExampleNames = scenariosAndExamples.flatMap { it.third }.toSet()

                val usedExamples = scenariosAndExamples.flatMap { it.second.keys }.toSet()

                val unusedRequestExampleNames = requestExampleNames - usedExamples

                if(Flags.getBooleanValue(IGNORE_INLINE_EXAMPLE_WARNINGS).not()) {
                    unusedRequestExampleNames.forEach { unusedRequestExampleName ->
                        logger.log(missingResponseExampleErrorMessageForTest(unusedRequestExampleName))
                    }
                }

                scenariosAndExamples.map {
                    it.first to it.second
                }
            }.flatten()


        val scenarioInfos = data.map { it.first }.flatten()
        val examples: Map>> =
            data.map { it.second }.foldRight(emptyMap()) { acc, map ->
                acc.plus(map)
            }

        logger.newLine()
        return scenarioInfos to examples
    }


    private fun getUpdatedScenarioInfosWithNoBodyResponseExamples(
        responseThatReturnsNoValues: ResponsePatternData,
        requestExamples: Map>,
        unusedRequestExampleNames: Set,
        scenarioInfos: List,
        operation: Operation,
        firstNoBodyResponseStatus: Int?,
    ): Pair>>, List> {
        val emptyResponse = HttpResponse(
            status = responseThatReturnsNoValues.responsePattern.status,
            headers = emptyMap(),
            body = NoBodyValue
        )
        val examplesOfResponseThatReturnsNoValues: Map>> =
            requestExamples.filterKeys { it in unusedRequestExampleNames }
                .mapValues { (key, examples) ->
                    examples.map { it to emptyResponse }
                }

        val updatedScenarioInfos = scenarioInfos.map { scenarioInfo ->
            if (scenarioInfo.httpResponsePattern.body == NoBodyPattern
                && scenarioInfo.httpResponsePattern.status == firstNoBodyResponseStatus
            ) {
                val unusedRequestExample =
                    requestExamples.filter { it.key in unusedRequestExampleNames }

                val rows = getRowsFromRequestExample(unusedRequestExample, operation, scenarioInfo)

                val updatedExamples: List = listOf(
                    Examples(
                        rows.first().columnNames,
                        scenarioInfo.examples.firstOrNull()?.rows.orEmpty() + rows
                    )
                )
                scenarioInfo.copy(
                    examples = updatedExamples
                )
            } else
                scenarioInfo
        }

        return examplesOfResponseThatReturnsNoValues to updatedScenarioInfos
    }

    private fun getRowsFromRequestExample(
        requestExample: Map>,
        operation: Operation,
        scenarioInfo: ScenarioInfo
    ): List {
        return requestExample.flatMap { (key, requests) ->
            requests.map { request ->
                val paramExamples = (request.headers + request.queryParams.asMap()).toList()
                val pathParameterExamples = try {
                    parameterExamples(operation, key) as Map
                } catch (e: Exception) {
                    emptyMap()
                }.entries.map { it.key to it.value }


                val allExamples = if (scenarioInfo.httpRequestPattern.body is NoBodyPattern) {
                    paramExamples + pathParameterExamples
                } else
                    listOf("(REQUEST-BODY)" to request.body.toStringLiteral()) + paramExamples
                Row(
                    name = key,
                    columnNames = allExamples.map { it.first },
                    values = allExamples.map { it.second }
                )
            }
        }
    }

    private fun getRequestExamplesForRequestWithNoParamsAndBody(
        operation: Operation,
        requestExamples: Map>,
        responseExamplesList: List>,
        httpRequestPatterns: List
    ): Map> {
        if(operation.requestBody != null || operation.parameters != null || requestExamples.isNotEmpty()) {
            return emptyMap()
        }

        return responseExamplesList.flatMap { responseExamples ->
            responseExamples.map {
                it.key to httpRequestPatterns.map { it.requestPattern.generate(Resolver()) }
            }
        }.toMap()
    }

    private fun validateParameters(parameters: List?) {
        parameters.orEmpty().forEach { parameter ->
            if(parameter.name == null)
                throw ContractException("A parameter does not have a name.")

            if(parameter.schema == null)
                throw ContractException("A parameter does not have a schema.")

            if(parameter.schema.type == "array" && parameter.schema.items == null)
                throw ContractException("A parameter of type \"array\" has not defined \"items\".")

        }
    }

    private fun collateExamplesForExpectations(
        requestExamples: Map>,
        responseExamplesList: List>,
        httpRequestPatterns: List
    ): Map>> {
        return responseExamplesList.flatMap { responseExamples ->
            responseExamples.filter { (key, _) ->
                key in requestExamples
            }.map { (key, responseExample) ->
                key to requestExamples.getValue(key).map { it to responseExample }
            }
        }.toMap()
    }

    private fun scenarioName(
        operation: Operation,
        response: ApiResponse,
        httpRequestPattern: HttpRequestPattern
    ): String = operation.summary?.let {
        """${operation.summary}. Response: ${response.description}"""
    } ?: "${httpRequestPattern.testDescription()}. Response: ${response.description}"

    private fun rowsToExamples(specmaticExampleRows: List): List =
        when (specmaticExampleRows) {
            emptyList() -> emptyList()
            else -> {
                val examples = Examples(
                    specmaticExampleRows.first().columnNames,
                    specmaticExampleRows
                )

                listOf(examples)
            }
        }

    private fun testRowsFromExamples(
        responseExamples: Map,
        requestExampleAsHttpRequests: Map>,
        operation: Operation,
        openApiRequest: Pair?,
        first2xxResponseStatus: Int?
    ): List {

        return responseExamples.mapNotNull { (exampleName, responseExample) ->
            val parameterExamples: Map = parameterExamples(operation, exampleName)

            val requestBodyExample: Map =
                requestBodyExample(openApiRequest, exampleName, operation.summary)

            val requestExamples = parameterExamples.plus(requestBodyExample).map { (key, value) ->
                if (value.toString().contains("externalValue")) "${key}_filename" to value
                else key to value
            }.toMap().ifEmpty { mapOf(SPECMATIC_TEST_WITH_NO_REQ_EX to "") }

            if (requestExamples.containsKey(SPECMATIC_TEST_WITH_NO_REQ_EX) && responseExample.status != first2xxResponseStatus) {
                if (getBooleanValue(IGNORE_INLINE_EXAMPLE_WARNINGS).not())
                    logger.log(missingRequestExampleErrorMessageForTest(exampleName))
                return@mapNotNull null
            }

            val resolvedResponseExample: ResponseExample? =
                when {
                    specmaticConfig.isResponseValueValidationEnabled() ->
                        ResponseValueExample(responseExample)

                    else ->
                        null
                }

            Row(
                requestExamples.keys.toList().map { keyName: String -> keyName },
                requestExamples.values.toList().map { value: Any? -> value?.toString() ?: "" }
                    .map { valueString: String ->
                        if (valueString.contains("externalValue")) {
                            ObjectMapper().readValue(valueString, Map::class.java).values.first()
                                .toString()
                        } else valueString
                    },
                name = exampleName,
                responseExampleForValidation = if(resolvedResponseExample != null && responseExample.isNotEmpty()) resolvedResponseExample else null,
                requestExample = requestExampleAsHttpRequests[exampleName]?.first(),
                responseExample = responseExample
            )
        }
    }

    data class OperationIdentifier(val requestMethod: String, val requestPath: String, val responseStatus: Int, val requestContentType: String?, val responseContentType: String?)

    private fun requestBodyExampleNames(
        openApiRequest: Pair?,
    ): Set {
        if(openApiRequest == null)
            return emptySet()

        val (_, requestBodyMediaType) = openApiRequest

        val requestExampleValue =
            requestBodyMediaType.examples.orEmpty().keys

        return requestExampleValue
    }

    private fun requestBodyExample(
        openApiRequest: Pair?,
        exampleName: String,
        operationSummary: String?
    ): Map {
        if(openApiRequest == null)
            return emptyMap()

        val (requestBodyContentType, requestBodyMediaType) = openApiRequest

        val requestExampleValue: Any? =
            resolveExample(requestBodyMediaType.examples?.get(exampleName))?.value

        val requestBodyExample: Map = if (requestExampleValue != null) {
            if (requestBodyContentType == "application/x-www-form-urlencoded" || requestBodyContentType == "multipart/form-data") {
                val operationSummaryClause = operationSummary?.let { "for operation \"${operationSummary}\"" } ?: ""
                val jsonExample =
                    attempt("Could not parse example $exampleName$operationSummaryClause") {
                        parsedJSON(requestExampleValue.toString()) as JSONObjectValue
                    }
                jsonExample.jsonObject.map { (key, value) ->
                    key to value.toString()
                }.toMap()
            } else {
                mapOf("(REQUEST-BODY)" to requestExampleValue)
            }
        } else {
            emptyMap()
        }
        return requestBodyExample
    }

    private fun resolveExample(example: Example?): Example? {
        return example?.`$ref`?.let {
            val exampleName = it.substringAfterLast("/")
            parsedOpenApi.components?.examples?.get(exampleName)
        } ?: example
    }

    private fun parameterExamples(
        operation: Operation,
        exampleName: String
    ): Map = operation.parameters.orEmpty()
        .filter { parameter ->
            parameter.examples.orEmpty().any { it.key == exampleName }
        }.associate {
            val exampleValue: Example = it.examples[exampleName]
                ?: throw ContractException("The value of ${it.name} in example $exampleName was unexpectedly found to be null.")

            it.name to (resolveExample(exampleValue)?.value ?: "")
        }

    private fun openApiPaths() = parsedOpenApi.paths.orEmpty()

    private fun isNumber(value: String): Boolean {
        return value.toIntOrNull() != null
    }

    private fun toHttpResponsePatterns(responses: ApiResponses?): List {
        return responses.orEmpty().map { (status, response) ->
            val headersMap = openAPIHeadersToSpecmatic(response)
            if(!isNumber(status) && status != "default")
                throw ContractException("Response status codes are expected to be numbers, but \"$status\" was found")

            attempt(breadCrumb = status) { openAPIResponseToSpecmatic(response, status, headersMap) }
        }.flatten()
    }

    private fun openAPIHeadersToSpecmatic(response: ApiResponse) =
        response.headers.orEmpty().map { (headerName, header) ->
            toSpecmaticParamName(header.required != true, headerName) to toSpecmaticPattern(
                resolveResponseHeader(header)?.schema ?: throw ContractException(
                    headerComponentMissingError(
                        headerName,
                        response
                    )
                ), emptyList()
            )
        }.toMap()

    data class ResponsePatternData(
        val response: ApiResponse,
        val mediaType: MediaType,
        val responsePattern: HttpResponsePattern,
        val examples: Map
    )

    private fun headerComponentMissingError(headerName: String, response: ApiResponse): String {
        if (response.description != null) {
            return "Header component not found for header $headerName in response \"${response.description}\""
        }

        return "Header component not found for header $headerName"
    }

    private fun resolveResponseHeader(header: Header): Header? {
        return if (header.`$ref` != null) {
            val headerComponentName = header.`$ref`.substringAfterLast("/")
            parsedOpenApi.components?.headers?.get(headerComponentName)
        } else {
            header
        }
    }

    private fun openAPIResponseToSpecmatic(
        response: ApiResponse,
        status: String,
        headersMap: Map
    ): List {
        if (response.content == null || response.content.isEmpty()) {
            val responsePattern = HttpResponsePattern(
                headersPattern = HttpHeadersPattern(headersMap),
                body = NoBodyPattern,
                status = status.toIntOrNull() ?: DEFAULT_RESPONSE_CODE
            )

            return listOf(ResponsePatternData(response, MediaType(), responsePattern, emptyMap()))
        }

        val headerExamples =
            if(specmaticConfig.ignoreInlineExamples || Flags.getBooleanValue(Flags.IGNORE_INLINE_EXAMPLES))
                emptyMap()
            else
                response.headers.orEmpty().entries.fold(emptyMap>()) { acc, (headerName, header) ->
                    extractParameterExamples(header.examples, headerName, acc)
                }

        return response.content.map { (contentType, mediaType) ->
            val responsePattern = HttpResponsePattern(
                headersPattern = HttpHeadersPattern(headersMap, contentType = contentType),
                status = if (status == "default") 1000 else status.toInt(),
                body = when (contentType) {
                    "application/xml" -> toXMLPattern(mediaType)
                    else -> toSpecmaticPattern(mediaType, "response")
                }
            )

            val exampleBodies: Map =
                if(specmaticConfig.ignoreInlineExamples || Flags.getBooleanValue(Flags.IGNORE_INLINE_EXAMPLES))
                    emptyMap()
                else
                    mediaType.examples?.mapValues {
                        resolveExample(it.value)?.value?.toString() ?: ""
                    } ?: emptyMap()

            val examples: Map =
                when (status.toIntOrNull()) {
                    0, null -> emptyMap()
                    else -> exampleBodies.map {
                        it.key to HttpResponse(
                            status.toInt(),
                            body = it.value ?: "",
                            headers = headerExamples[it.key] ?: emptyMap()
                        )
                    }.toMap()
                }

            ResponsePatternData(response, mediaType, responsePattern, examples)
        }
    }

    private fun toHttpRequestPatterns(
        httpPathPattern: HttpPathPattern,
        httpQueryParamPattern: HttpQueryParamPattern,
        httpMethod: String,
        operation: Operation
    ): List {

        val securitySchemes: Map =
            parsedOpenApi.components?.securitySchemes?.mapValues { (schemeName, scheme) ->
                toSecurityScheme(schemeName, scheme)
            } ?: mapOf(NO_SECURITY_SCHEMA_IN_SPECIFICATION to NoSecurityScheme())

        val securitySchemesForRequestPattern: Map =
            (parsedOpenApi.security.orEmpty() + operation.security.orEmpty())
                .flatMap { it.keys }
                .toSet()
                .map {
                    val securityScheme = securitySchemes[it]
                        ?: throw ContractException("Security scheme used in $httpMethod ${httpPathPattern.path} does not exist in the spec")
                    it to securityScheme
                }
            .toMap().ifEmpty {
                    mapOf(NO_SECURITY_SCHEMA_IN_SPECIFICATION to NoSecurityScheme())
                }

        val parameters = operation.parameters

        val headersMap = parameters.orEmpty().filterIsInstance().associate {
            toSpecmaticParamName(it.required != true, it.name) to toSpecmaticPattern(it.schema, emptyList())
        }

        val headersPattern = HttpHeadersPattern(headersMap)
        val requestPattern = HttpRequestPattern(
            httpPathPattern = httpPathPattern,
            httpQueryParamPattern = httpQueryParamPattern,
            method = httpMethod,
            headersPattern = headersPattern,
            securitySchemes = operationSecuritySchemes(operation, securitySchemesForRequestPattern)
        )

        val exampleQueryParams = namedExampleParams(operation, QueryParameter::class.java)
        val examplePathParams = namedExampleParams(operation, PathParameter::class.java)
        val exampleHeaderParams = namedExampleParams(operation, HeaderParameter::class.java)

        val exampleRequestBuilder = ExampleRequestBuilder(
            examplePathParams,
            exampleHeaderParams,
            exampleQueryParams,
            httpPathPattern,
            httpMethod,
            securitySchemesForRequestPattern
        )

        val requestBody = resolveRequestBody(operation)
            ?: return listOf(
                RequestPatternsData(
                    requestPattern.copy(body = NoBodyPattern),
                    exampleRequestBuilder.examplesBasedOnParameters
                )
            )

        return requestBody.content.map { (contentType, mediaType) ->
            when (contentType.lowercase()) {
                "multipart/form-data" -> {
                    val partSchemas = if (mediaType.schema.`$ref` == null) {
                        mediaType.schema
                    } else {
                        resolveReferenceToSchema(mediaType.schema.`$ref`).second
                    }

                    val parts: List =
                        partSchemas.properties.map { (partName, partSchema) ->
                            val partContentType = mediaType.encoding?.get(partName)?.contentType
                            val partNameWithPresence = if (partSchemas.required?.contains(partName) == true)
                                partName
                            else
                                "$partName?"

                            if (partSchema is BinarySchema) {
                                MultiPartFilePattern(
                                    partNameWithPresence,
                                    toSpecmaticPattern(partSchema, emptyList()),
                                    partContentType
                                )
                            } else {
                                MultiPartContentPattern(
                                    partNameWithPresence,
                                    toSpecmaticPattern(partSchema, emptyList()),
                                    partContentType
                                )
                            }
                        }

                    Pair(
                        requestPattern.copy(
                            multiPartFormDataPattern = parts,
                            headersPattern = headersPatternWithContentType(requestPattern, contentType)
                        ), emptyMap()
                    )
                }

                "application/x-www-form-urlencoded" -> Pair(
                    requestPattern.copy(
                        formFieldsPattern = toFormFields(mediaType),
                        headersPattern = headersPatternWithContentType(requestPattern, contentType)
                    ), emptyMap()
                )

                "application/xml" -> Pair(
                    requestPattern.copy(
                        body = toXMLPattern(mediaType),
                        headersPattern = headersPatternWithContentType(requestPattern, contentType)
                    ), emptyMap()
                )

                else -> {
                    val examplesFromMediaType = mediaType.examples ?: emptyMap()

                    val exampleBodies: Map = examplesFromMediaType.mapValues {
                        resolveExample(it.value)?.value?.toString() ?: ""
                    }

                    val allExamples =
                        if(specmaticConfig.ignoreInlineExamples || Flags.getBooleanValue(Flags.IGNORE_INLINE_EXAMPLES))
                            emptyMap()
                        else
                            exampleRequestBuilder.examplesWithRequestBodies(exampleBodies, contentType)

                    val bodyIsRequired: Boolean = requestBody.required ?: true

                    val body = toSpecmaticPattern(mediaType, "request").let {
                        if (bodyIsRequired)
                            it
                        else
                            OptionalBodyPattern.fromPattern(it)
                    }

                    Pair(
                        requestPattern.copy(
                            body = body,
                            headersPattern = headersPatternWithContentType(requestPattern, contentType)
                        ), allExamples
                    )
                }
            }.let { RequestPatternsData(it.first, it.second, Pair(contentType, mediaType)) }
        }
    }

    private fun headersPatternWithContentType(
        requestPattern: HttpRequestPattern,
        contentType: String
    ) = requestPattern.headersPattern.copy(
        contentType = contentType
    )

    private fun  namedExampleParams(
        operation: Operation,
        parameterType: Class
    ): Map> {
        if(specmaticConfig.ignoreInlineExamples || Flags.getBooleanValue(Flags.IGNORE_INLINE_EXAMPLES))
            return emptyMap()

        return operation.parameters.orEmpty()
            .filterIsInstance(parameterType)
            .fold(emptyMap()) { acc, parameter ->
                extractParameterExamples(parameter.examples, parameter.name, acc)
            }
    }

    private fun extractParameterExamples(
        examplesToAdd: Map?,
        parameterName: String,
        examplesAccumulatedSoFar: Map>
    ): Map> {
        return examplesToAdd.orEmpty()
            .entries.filter { it.value.value?.toString().orEmpty() !in OMIT }
            .fold(examplesAccumulatedSoFar) { acc, (exampleName, example) ->
                val exampleValue = resolveExample(example)?.value?.toString() ?: ""
                val exampleMap = acc[exampleName] ?: emptyMap()
                acc.plus(exampleName to exampleMap.plus(parameterName to exampleValue))
            }
    }

    private fun resolveRequestBody(operation: Operation): RequestBody? =
        operation.requestBody?.`$ref`?.let {
            resolveReferenceToRequestBody(it).second
        } ?: operation.requestBody

    private fun operationSecuritySchemes(
        operation: Operation,
        contractSecuritySchemes: Map
    ): List {
        val globalSecurityRequirements: List =
            parsedOpenApi.security?.map { it.keys.toList() }?.flatten() ?: emptyList()
        val operationSecurityRequirements: List =
            operation.security?.map { it.keys.toList() }?.flatten() ?: emptyList()
        val operationSecurityRequirementsSuperSet: List =
            globalSecurityRequirements.plus(operationSecurityRequirements).distinct()
        val operationSecuritySchemes: List =
            contractSecuritySchemes.filter { (name, _: OpenAPISecurityScheme) -> name in operationSecurityRequirementsSuperSet }.values.toList()
        return operationSecuritySchemes.ifEmpty { listOf(NoSecurityScheme()) }
    }

    private fun toSecurityScheme(schemeName: String, securityScheme: SecurityScheme): OpenAPISecurityScheme {
        val securitySchemeConfiguration = securityConfiguration?.OpenAPI?.securitySchemes?.get(schemeName)
        if (securityScheme.scheme == BEARER_SECURITY_SCHEME) {
            return toBearerSecurityScheme(securitySchemeConfiguration, schemeName)
        }

        if (securityScheme.type == SecurityScheme.Type.OAUTH2) {
            return toBearerSecurityScheme(securitySchemeConfiguration, schemeName)
        }

        if (securityScheme.type == SecurityScheme.Type.APIKEY) {
            val apiKey = getSecurityTokenForApiKeyScheme(securitySchemeConfiguration, schemeName)
            if (securityScheme.`in` == SecurityScheme.In.HEADER)
                return APIKeyInHeaderSecurityScheme(securityScheme.name, apiKey)

            if (securityScheme.`in` == SecurityScheme.In.QUERY)
                return APIKeyInQueryParamSecurityScheme(securityScheme.name, apiKey)
        }

        if(securityScheme.type == SecurityScheme.Type.HTTP && securityScheme.scheme == "basic")
            return toBasicAuthSecurityScheme(securitySchemeConfiguration, schemeName)

        throw ContractException("Specmatic only supports oauth2, bearer, and api key authentication (header, query) security schemes at the moment")
    }

    private fun toBearerSecurityScheme(
        securitySchemeConfiguration: SecuritySchemeConfiguration?,
        environmentVariable: String,
    ): BearerSecurityScheme {
        val token = getSecurityTokenForBearerScheme(securitySchemeConfiguration, environmentVariable)
        return BearerSecurityScheme(token)
    }

    private fun toBasicAuthSecurityScheme(
        securitySchemeConfiguration: SecuritySchemeConfiguration?,
        environmentVariable: String,
    ): BasicAuthSecurityScheme {
        val token = getSecurityTokenForBasicAuthScheme(securitySchemeConfiguration, environmentVariable)
        return BasicAuthSecurityScheme(token)
    }

    private fun toFormFields(mediaType: MediaType): Map {
        val schema = mediaType.schema.`$ref`?.let {
            val (_, resolvedSchema) = resolveReferenceToSchema(mediaType.schema.`$ref`)
            resolvedSchema
        } ?: mediaType.schema

        return schema.properties.map { (formFieldName, formFieldValue) ->
            formFieldName to toSpecmaticPattern(
                formFieldValue, emptyList(), jsonInFormData = isJsonInString(mediaType, formFieldName)
            )
        }.toMap()
    }

    data class Discriminator(private val discriminatorDetails: Map>>>> = emptyMap()) {
        fun isNotEmpty(): Boolean {
            return discriminatorDetails.isNotEmpty()
        }

        val values: List
            get() {
                return discriminatorDetails.entries.firstOrNull()?.let {
                    it.value.keys.toList()
                } ?: emptyList()
            }

        val key: String?
            get() {
                return discriminatorDetails.entries.firstOrNull()?.key
            }

        val schemas: List>
            get() {
                return discriminatorDetails.entries.flatMap {
                    it.value.values.flatMap {
                        it.second
                    }
                }
            }

        fun plus(newDiscriminatorDetails: Triple>>>, Discriminator>?): Discriminator {
            if(newDiscriminatorDetails == null)
                return this

            val (propertyName, valuesAndSchemas: Map>>>, newDiscriminator) = newDiscriminatorDetails

            val updatedDiscriminatorDetails: Map>>>> =
                discriminatorDetails.plus(propertyName to valuesAndSchemas)

            return this.copy(updatedDiscriminatorDetails).plus(newDiscriminator)
        }

        fun plus(newDiscriminator: Discriminator): Discriminator {
            return this.copy(discriminatorDetails + newDiscriminator.discriminatorDetails)
        }

        fun hasValueForKey(propertyName: String?): Boolean {
            if(propertyName == null)
                return false

            return propertyName in discriminatorDetails
        }

        fun valueFor(propertyName: String): Pattern {
            if(propertyName !in discriminatorDetails)
                throw ContractException("$propertyName not found in discriminator details")

            return discriminatorDetails.getValue(propertyName).firstNotNullOf { ExactValuePattern(StringValue(it.key), discriminator = true) }
        }

        fun explode(): List {
            return explode(discriminatorDetails)
        }

        private fun explode(discriminatorDetails: Map>>>>): List {
            val propertyName = discriminatorDetails.keys.firstOrNull() ?: return listOf(Discriminator())

            val discriminatorDetailsWithOneKeyLess = discriminatorDetails - propertyName

            val valueOptionsWithSchemasForProperty = discriminatorDetails.getValue(propertyName)

            return valueOptionsWithSchemasForProperty.flatMap { valueOption: Map.Entry>>> ->
                explode(discriminatorDetailsWithOneKeyLess).map { discriminator ->
                    discriminator.plus(Triple(propertyName, mapOf(valueOption.toPair()), Discriminator()))
                }
            }
        }
    }

    private fun isJsonInString(
        mediaType: MediaType, formFieldName: String?
    ) = if (mediaType.encoding.isNullOrEmpty()) false
    else mediaType.encoding[formFieldName]?.contentType == "application/json"

    private fun toSpecmaticPattern(mediaType: MediaType, section: String, jsonInFormData: Boolean = false): Pattern =
        toSpecmaticPattern(mediaType.schema ?: throw ContractException("${section.capitalizeFirstChar()} body definition is missing"), emptyList(), jsonInFormData = jsonInFormData)

    private fun resolveDeepAllOfs(schema: Schema, discriminator: Discriminator, typeStack: Set): Pair>, Discriminator> {
        if (schema.allOf == null)
            return listOf(schema) to discriminator

        // Pair [reffed schema]>>>
        val newDiscriminatorDetails: Triple>>>, Discriminator>? = schema.discriminator?.let { rawDiscriminator ->
            rawDiscriminator.propertyName?.let { propertyName ->
                val mapping = rawDiscriminator.mapping ?: emptyMap()

                val mappingWithSchemaListAndDiscriminator = mapping.mapValues { (discriminatorValue, refPath) ->
                    val (schemaName, schema) = resolveReferenceToSchema(refPath)
                    val componentName = extractComponentName(refPath)
                    if(componentName !in typeStack) {
                        schemaName to resolveDeepAllOfs(schema, discriminator, typeStack + componentName)
                    } else {
                        schemaName to (emptyList>() to Discriminator())
                    }
                }

                val discriminatorsFromResolvedMappingSchemas = mappingWithSchemaListAndDiscriminator.values.map { (possiblePropertyValue, discriminator) ->
                    discriminator.second
                }

                val mergedDiscriminatorFromMappingSchemas = discriminatorsFromResolvedMappingSchemas.fold(Discriminator()) { acc, discriminator ->
                    acc.plus(discriminator)
                }

                val mappingWithSchema: Map>>> = mappingWithSchemaListAndDiscriminator.mapValues { entry: Map.Entry>, Discriminator>>> ->
                    entry.key to (entry.value.second.first)
                }

                Triple(propertyName, mappingWithSchema, mergedDiscriminatorFromMappingSchemas)
            }
        }

        val allOfs = schema.allOf.map { constituentSchema ->
            if (constituentSchema.`$ref` != null) {
                val (_, referredSchema) = resolveReferenceToSchema(constituentSchema.`$ref`)

                val componentName = extractComponentName(constituentSchema.`$ref`)

                if(componentName !in typeStack) {
                    resolveDeepAllOfs(referredSchema, discriminator.plus(newDiscriminatorDetails), typeStack + componentName)
                } else
                    null
            } else listOf(constituentSchema) to discriminator
        }.filterNotNull()

        val discriminatorForThisLevel = newDiscriminatorDetails?.let { Discriminator(mapOf(newDiscriminatorDetails.first to newDiscriminatorDetails.second)) } ?: Discriminator()

        return allOfs.fold(Pair>, Discriminator>(emptyList(), discriminatorForThisLevel)) { acc, item ->
            val (accSchemas, accDiscriminator) = acc
            val (additionalSchemas, additionalSchemasDiscriminator) = item

            accSchemas.plus(additionalSchemas) to accDiscriminator.plus(additionalSchemasDiscriminator)
        }
    }

    private fun toSpecmaticPattern(
        schema: Schema<*>, typeStack: List, patternName: String = "", jsonInFormData: Boolean = false
    ): Pattern {
        val preExistingResult = patterns["($patternName)"]
        val pattern = if (preExistingResult != null && patternName.isNotBlank())
            preExistingResult
        else if (typeStack.filter { it == patternName }.size > 1) {
            DeferredPattern("($patternName)")
        } else when (schema) {
            is StringSchema -> when (schema.enum) {
                null -> StringPattern(
                    minLength = schema.minLength,
                    maxLength = schema.maxLength,
                    example = schema.example?.toString(),
                    regex = schema.pattern
                )

                else -> toEnum(schema, patternName) { enumValue -> StringValue(enumValue.toString()) }.withExample(
                    schema.example?.toString()
                )
            }

            is EmailSchema -> EmailPattern(example = schema.example?.toString())

            is PasswordSchema -> StringPattern(example = schema.example?.toString())

            is IntegerSchema -> when (schema.enum) {
                null -> numberPattern(schema, false)
                else -> toEnum(schema, patternName) { enumValue ->
                    NumberValue(
                        enumValue.toString().toInt()
                    )
                }.withExample(schema.example?.toString())
            }

            is BinarySchema -> BinaryPattern()
            is NumberSchema -> numberPattern(schema, true)
            is UUIDSchema -> UUIDPattern
            is DateTimeSchema -> DateTimePattern
            is DateSchema -> DatePattern
            is BooleanSchema -> BooleanPattern(example = schema.example?.toString())
            is ObjectSchema -> {
                if (schema.additionalProperties is Schema<*>) {
                    toDictionaryPattern(schema, typeStack)
                } else if (noPropertiesDefinedInSchema(schema)) {
                    toFreeFormDictionaryWithStringKeysPattern()
                } else if (schema.xml?.name != null) {
                    toXMLPattern(schema, typeStack = typeStack)
                } else {
                    toJsonObjectPattern(schema, patternName, typeStack)
                }
            }
            is ByteArraySchema -> Base64StringPattern()

            is ArraySchema -> {
                if (schema.xml?.name != null) {
                    toXMLPattern(schema, typeStack = typeStack)
                } else {

                    ListPattern(
                        toSpecmaticPattern(
                            schema.items, typeStack
                        ),
                        example = toListExample(schema.example)
                    )
                }
            }

            is ComposedSchema -> {
                if (schema.allOf != null) {
                    val (deepListOfAllOfs, allDiscriminators) = resolveDeepAllOfs(schema, OpenApiSpecification.Discriminator(), setOf(patternName))

                    val explodedDiscriminators = allDiscriminators.explode()

                    val schemaProperties = explodedDiscriminators.map { discriminator ->
                        val schemasFromDiscriminator = discriminator.schemas

                        val schemaProperties = (deepListOfAllOfs + schemasFromDiscriminator).map { schemaToProcess ->
                            val requiredFields = schemaToProcess.required.orEmpty()
                            toSchemaProperties(schemaToProcess, requiredFields, patternName, typeStack, discriminator)
                        }.fold(emptyMap()) { propertiesAcc, propertiesEntry ->
                            combine(propertiesEntry, propertiesAcc)
                        }

                        schemaProperties
                    }

                    val schemasWithOneOf = deepListOfAllOfs.filter {
                        it.oneOf != null
                    }

                    val oneOfs = schemasWithOneOf.map { oneOfTheSchemas ->
                        val result = oneOfTheSchemas.oneOf.map {
                            val (componentName, schemaToProcess) = resolveReferenceToSchema(it.`$ref`)
                            val requiredFields = schemaToProcess.required.orEmpty()
                            componentName to toSchemaProperties(
                                schemaToProcess,
                                requiredFields,
                                componentName,
                                typeStack
                            )
                        }.flatMap { (componentName, properties) ->
                            schemaProperties.map {
                                componentName to combine(it, properties)
                            }
                        }

                        result
                    }.flatten().map { (componentName, properties) ->
                        toJSONObjectPattern(properties, "(${componentName})")
                    }

                    val pattern = if (oneOfs.size == 1)
                        oneOfs.single()
                    else if (oneOfs.size > 1)
                        AnyPattern(oneOfs)
                    else if(allDiscriminators.isNotEmpty())
                        AnyPattern(schemaProperties.map { toJSONObjectPattern(it, "(${patternName})") }, discriminatorProperty = allDiscriminators.key, discriminatorValues = allDiscriminators.values.toSet())
                    else if(schemaProperties.size > 1)
                        AnyPattern(schemaProperties.map { toJSONObjectPattern(it, "(${patternName})") })
                    else
                        toJSONObjectPattern(schemaProperties.single(), "(${patternName})")

                    cacheComponentPattern(patternName, pattern)

                    pattern
                } else if (schema.oneOf != null) {
                    val candidatePatterns = schema.oneOf.filterNot { nullableEmptyObject(it) }.map { componentSchema ->
                        val (componentName, schemaToProcess) =
                            if (componentSchema.`$ref` != null)
                                resolveReferenceToSchema(componentSchema.`$ref`)
                            else
                                "" to componentSchema

                        toSpecmaticPattern(schemaToProcess, typeStack.plus(componentName), componentName)
                    }

                    val nullable =
                        if (schema.oneOf.any { nullableEmptyObject(it) }) listOf(NullPattern) else emptyList()

                    AnyPattern(candidatePatterns.plus(nullable), discriminatorProperty =  schema.discriminator?.propertyName, discriminatorValues = schema.discriminator?.let { it.mapping.keys.toSet() }.orEmpty())
                } else if (schema.anyOf != null) {
                    throw UnsupportedOperationException("Specmatic does not support anyOf")
                } else {
                    throw UnsupportedOperationException("Unsupported composed schema: $schema")
                }
            }

            else -> {
                if (schema.nullable == true && schema.additionalProperties == null && schema.`$ref` == null) {
                    NullPattern
                } else if (schema.additionalProperties is Schema<*>) {
                    toDictionaryPattern(schema, typeStack)
                } else if (schema.additionalProperties == true) {
                    toFreeFormDictionaryWithStringKeysPattern()
                } else if(schema.properties != null)
                    toJsonObjectPattern(schema, patternName, typeStack)
                else if (schema.`$ref` != null) {
                    val component: String = schema.`$ref`

                    val (componentName, referredSchema) = resolveReferenceToSchema(component)
                    val cyclicReference = typeStack.contains(componentName)
                    if (!cyclicReference) {
                        val componentPattern = toSpecmaticPattern(
                            referredSchema,
                            typeStack.plus(componentName), componentName
                        )
                        cacheComponentPattern(componentName, componentPattern)
                    }
                    DeferredPattern("(${componentName})")
                }
                else {
                    val schemaFragment = if(patternName.isNotBlank()) " in schema $patternName" else " in the schema"

                    if(schema.javaClass.simpleName != "Schema")
                        throw ContractException("${schemaFragment.capitalizeFirstChar()} is not yet supported, please raise an issue on https://github.com/znsio/specmatic/issues")
                    else
                        AnyNonNullJSONValue()
                }
            }
        }.also {
            when {
                it.instanceOf(JSONObjectPattern::class) && jsonInFormData -> {
                    PatternInStringPattern(
                        patterns.getOrDefault("($patternName)", StringPattern()), "($patternName)"
                    )
                }

                else -> it
            }
        }

        return when (schema.nullable) {
            false, null -> pattern
            true -> pattern.toNullable(schema.example?.toString())
        }
    }

    private fun numberPattern(schema: Schema<*>, isDoubleFormat: Boolean) = NumberPattern(
        minimum = schema.minimum ?: NumberPattern.LOWEST_DECIMAL,
        maximum = schema.maximum ?: NumberPattern.HIGHEST_DECIMAL,
        exclusiveMinimum = schema.exclusiveMinimum ?: false,
        exclusiveMaximum = schema.exclusiveMaximum ?: false,
        isDoubleFormat = isDoubleFormat,
        example = schema.example?.toString()
    )

    private fun toListExample(example: Any?): List? {
        if (example == null)
            return null

        if (example !is ArrayNode)
            return null

        return example.toList().flatMap {
            when {
                it.isNull -> listOf(null)
                it.isNumber -> listOf(it.numberValue().toString())
                it.isBoolean -> listOf(it.booleanValue().toString())
                it.isTextual -> listOf(it.textValue())
                else -> emptyList()
            }
        }
    }

    private fun combine(
        propertiesEntry: Map,
        propertiesAcc: Map
    ): Map {
        val updatedPropertiesAcc: Map =
            propertiesEntry.entries.fold(propertiesAcc) { acc, propertyEntry ->
                when (val keyWithoutOptionality = withoutOptionality(propertyEntry.key)) {
                    in acc ->
                        acc

                    propertyEntry.key ->
                        acc.minus("$keyWithoutOptionality?").plus(propertyEntry.key to propertyEntry.value)

                    else ->
                        acc.plus(propertyEntry.key to propertyEntry.value)
                }
            }

        return updatedPropertiesAcc
    }

    private fun  cacheComponentPattern(componentName: String, pattern: T): T {
        if (componentName.isNotBlank() && pattern !is DeferredPattern) {
            val typeName = "(${componentName})"
            val prev = patterns[typeName]
            if (pattern != prev) {
                if (prev != null) {
                    logger.debug("Replacing cached component pattern. name=$componentName, prev=$prev, new=$pattern")
                }
                patterns[typeName] = pattern
            }
        }
        return pattern
    }

    private fun nullableEmptyObject(schema: Schema<*>): Boolean {
        return schema is ObjectSchema && schema.nullable == true
    }

    private fun toXMLPattern(mediaType: MediaType): Pattern {
        return toXMLPattern(mediaType.schema, typeStack = emptyList())
    }

    private fun toXMLPattern(
        schema: Schema, nodeNameFromProperty: String? = null, typeStack: List
    ): XMLPattern {
        val name = schema.xml?.name ?: nodeNameFromProperty

        return when (schema) {
            is ObjectSchema -> {
                if(schema.properties == null) {
                    throw ContractException("XML schema named $name does not have properties.")
                }

                val nodeProperties = schema.properties.filter { entry ->
                    entry.value.xml?.attribute != true
                }

                val nodes = nodeProperties.map { (propertyName: String, propertySchema) ->
                    val type = when (propertySchema.type) {
                        in primitiveOpenAPITypes -> {
                            val innerPattern = DeferredPattern(primitiveOpenAPITypes.getValue(propertySchema.type))
                            XMLPattern(XMLTypeData(propertyName, propertyName, emptyMap(), listOf(innerPattern)))
                        }

                        else -> {
                            toXMLPattern(propertySchema, propertyName, typeStack)
                        }
                    }

                    val optionalAttribute = if (propertyName !in (schema.required ?: emptyList())) mapOf(
                        OCCURS_ATTRIBUTE_NAME to ExactValuePattern(StringValue(OPTIONAL_ATTRIBUTE_VALUE))
                    )
                    else emptyMap()

                    type.copy(pattern = type.pattern.copy(attributes = optionalAttribute.plus(type.pattern.attributes)))
                }

                val attributeProperties = schema.properties.filter { entry ->
                    entry.value.xml?.attribute == true
                }

                val attributes: Map = attributeProperties.map { (name, schema) ->
                    val attributeName = if(name !in schema.required.orEmpty())
                        "$name.opt"
                    else
                        name

                    attributeName to toSpecmaticPattern(schema, emptyList())
                }.toMap()

                name ?: throw ContractException("Could not determine name for an xml node")

                val namespaceAttributes: Map =
                    if (schema.xml?.namespace != null && schema.xml?.prefix != null) {
                        val attributeName = "xmlns:${schema.xml?.prefix}"
                        val attributeValue = ExactValuePattern(StringValue(schema.xml.namespace))
                        mapOf(attributeName to attributeValue)
                    } else {
                        emptyMap()
                    }

                val xmlTypeData = XMLTypeData(name, realName(schema, name), namespaceAttributes.plus(attributes), nodes)

                XMLPattern(xmlTypeData)
            }

            is ArraySchema -> {
                val repeatingSchema = schema.items as Schema

                val repeatingType = when (repeatingSchema.type) {
                    in primitiveOpenAPITypes -> {
                        val innerPattern = DeferredPattern(primitiveOpenAPITypes.getValue(repeatingSchema.type))

                        val innerName = repeatingSchema.xml?.name
                            ?: if (schema.xml?.name != null && schema.xml?.wrapped == true) schema.xml.name else nodeNameFromProperty

                        XMLPattern(
                            XMLTypeData(
                                innerName ?: throw ContractException("Could not determine name for an xml node"),
                                innerName,
                                emptyMap(),
                                listOf(innerPattern)
                            )
                        )
                    }

                    else -> {
                        toXMLPattern(repeatingSchema, name, typeStack)
                    }
                }.let { repeatingType ->
                    repeatingType.copy(
                        pattern = repeatingType.pattern.copy(
                            attributes = repeatingType.pattern.attributes.plus(
                                OCCURS_ATTRIBUTE_NAME to ExactValuePattern(StringValue(MULTIPLE_ATTRIBUTE_VALUE))
                            )
                        )
                    )
                }

                if (schema.xml?.wrapped == true) {
                    val wrappedName = schema.xml?.name ?: nodeNameFromProperty
                    val wrapperTypeData = XMLTypeData(
                        wrappedName ?: throw ContractException("Could not determine name for an xml node"),
                        wrappedName,
                        emptyMap(),
                        listOf(repeatingType)
                    )
                    XMLPattern(wrapperTypeData)
                } else repeatingType
            }

            else -> {
                if (schema.`$ref` != null) {
                    val component = schema.`$ref`
                    val (componentName, componentSchema) = resolveReferenceToSchema(component)

                    val typeName = "($componentName)"

                    val nodeName = componentSchema.xml?.name ?: name ?: componentName

                    if (typeName !in typeStack) {
                        val componentPattern = toXMLPattern(componentSchema, componentName, typeStack.plus(typeName))
                        cacheComponentPattern(componentName, componentPattern)
                    }

                    val xmlRefType = XMLTypeData(
                        nodeName, nodeName, mapOf(
                            TYPE_ATTRIBUTE_NAME to ExactValuePattern(
                                StringValue(
                                    componentName
                                )
                            )
                        ), emptyList()
                    )

                    XMLPattern(xmlRefType)
                } else throw ContractException("Node not recognized as XML type: ${schema.type}")
            }
        }
    }

    private fun realName(schema: ObjectSchema, name: String): String = if (schema.xml?.prefix != null) {
        "${schema.xml?.prefix}:${name}"
    } else {
        name
    }

    private val primitiveOpenAPITypes =
        mapOf("string" to "(string)", "number" to "(number)", "integer" to "(number)", "boolean" to "(boolean)")

    private fun toDictionaryPattern(
        schema: Schema<*>, typeStack: List
    ): DictionaryPattern {
        val valueSchema = schema.additionalProperties as Schema
        val valueSchemaTypeName = valueSchema.`$ref` ?: valueSchema.types?.first() ?: ""
        return DictionaryPattern(
            StringPattern(), toSpecmaticPattern(valueSchema, typeStack, valueSchemaTypeName, false)
        )
    }

    private fun noPropertiesDefinedInSchema(valueSchema: Schema) = valueSchema.properties == null

    private fun toFreeFormDictionaryWithStringKeysPattern(): DictionaryPattern {
        return DictionaryPattern(
            StringPattern(), AnythingPattern
        )
    }


    private fun toJsonObjectPattern(
        schema: Schema<*>, patternName: String, typeStack: List
    ): JSONObjectPattern {
        val requiredFields = schema.required.orEmpty()
        val schemaProperties = toSchemaProperties(schema, requiredFields, patternName, typeStack)
        val minProperties: Int? = schema.minProperties
        val maxProperties: Int? = schema.maxProperties
        val jsonObjectPattern = toJSONObjectPattern(schemaProperties, "(${patternName})").copy(
            minProperties = minProperties,
            maxProperties = maxProperties
        )
        return cacheComponentPattern(patternName, jsonObjectPattern)
    }

    private fun toSchemaProperties(
        schema: Schema<*>, requiredFields: List, patternName: String, typeStack: List, discriminator: Discriminator = Discriminator()
    ): Map {
        val patternMap = schema.properties.orEmpty().map { (propertyName, propertyType) ->
            if (schema.discriminator?.propertyName == propertyName)
                propertyName to ExactValuePattern(StringValue(patternName), discriminator = true)
            else if (discriminator.hasValueForKey(propertyName)) {
                propertyName to discriminator.valueFor(propertyName)
            } else {
                val optional = !requiredFields.contains(propertyName)
                toSpecmaticParamName(optional, propertyName) to attempt(breadCrumb = propertyName) {
                    toSpecmaticPattern(
                        propertyType,
                        typeStack) }
            }
        }.toMap()

        return patternMap
    }

    private fun toEnum(schema: Schema<*>, patternName: String, toSpecmaticValue: (Any) -> Value): EnumPattern {
        val specmaticValues = schema.enum.map { enumValue ->
            when (enumValue) {
                null -> NullValue
                else -> toSpecmaticValue(enumValue)
            }
        }

        if (schema.nullable != true && NullValue in specmaticValues)
            throw ContractException("Enum values cannot contain null since the schema $patternName is not nullable")

        if (schema.nullable == true && NullValue !in specmaticValues)
            throw ContractException("Enum values must contain null since the schema $patternName is nullable")

        return EnumPattern(specmaticValues, nullable = schema.nullable == true, typeAlias = patternName).also {
            cacheComponentPattern(patternName, it)
        }
    }

    private fun toSpecmaticParamName(optional: Boolean, name: String) = when (optional) {
        true -> "${name}?"
        false -> name
    }

    private fun resolveReferenceToSchema(component: String): Pair> {
        val componentName = extractComponentName(component)
        val components = parsedOpenApi.components ?: throw ContractException("Could not find components in the specification (trying to dereference $component")
        val schemas = components.schemas ?: throw ContractException("Could not find schemas components in the specification (trying to dereference $component)")

        val schema =
            schemas[componentName] ?: ObjectSchema().also { it.properties = emptyMap() }

        return componentName to schema as Schema
    }

    private fun resolveReferenceToRequestBody(component: String): Pair {
        val componentName = extractComponentName(component)
        val requestBody = parsedOpenApi.components.requestBodies[componentName] ?: RequestBody()

        return componentName to requestBody
    }

    private fun extractComponentName(component: String): String {
        if(!component.startsWith("#")) {
            val componentPath = component.substringAfterLast("#")
            val filePath = component.substringBeforeLast("#")
            val message = try {
                "Could not dereference $component. Either the file $filePath does not exist, or $componentPath is missing from it."
            } catch (e: Throwable) {
                "Could not dereference $component due an an error (${e.message})."
            }

            throw ContractException(message)
        }

        return componentNameFromReference(component)
    }

    private fun componentNameFromReference(component: String) = component.substringAfterLast("/")

    private fun toSpecmaticQueryParam(operation: Operation): HttpQueryParamPattern {
        val parameters = operation.parameters ?: return HttpQueryParamPattern(emptyMap())

        val queryPattern: Map = parameters.filterIsInstance().associate {
            val specmaticPattern: Pattern? = if (it.schema.type == "array") {
                QueryParameterArrayPattern(listOf(toSpecmaticPattern(schema = it.schema.items, typeStack = emptyList())), it.name)
            } else if (it.schema.type != "object") {
                QueryParameterScalarPattern(toSpecmaticPattern(schema = it.schema, typeStack = emptyList(), patternName = it.name))
            } else null

            val queryParamKey = if(it.required == true)
                it.name
            else
                "${it.name}?"

            queryParamKey to specmaticPattern
        }.filterValues { it != null }.mapValues { it.value!! }

        val additionalProperties = additionalPropertiesInQueryParam(parameters)

        return HttpQueryParamPattern(queryPattern, additionalProperties)
    }

    private fun additionalPropertiesInQueryParam(parameters: List): Pattern? {
        val additionalProperties = parameters.filterIsInstance()
            .find { it.schema.type == "object" && it.schema.additionalProperties != null }?.schema?.additionalProperties

        if(additionalProperties == false)
            return null

        if(additionalProperties == true)
            return AnythingPattern

        if(additionalProperties is Schema<*>)
            return toSpecmaticPattern(additionalProperties, emptyList())

        return null
    }

    private fun toSpecmaticPathParam(openApiPath: String, operation: Operation): HttpPathPattern {
        val parameters = operation.parameters ?: emptyList()

        val pathSegments: List = openApiPath.removePrefix("/").removeSuffix("/").let {
            if (it.isBlank())
                emptyList()
            else it.split("/")
        }
        val pathParamMap: Map =
            parameters.filterIsInstance().associateBy {
                it.name
            }

        val pathPattern: List = pathSegments.map { pathSegment ->
            if (isParameter(pathSegment)) {
                val paramName = pathSegment.removeSurrounding("{", "}")

                val param = pathParamMap[paramName]
                    ?: throw ContractException("The path parameter in $openApiPath is not defined in the specification")

                URLPathSegmentPattern(toSpecmaticPattern(param.schema, emptyList()), paramName)
            } else {
                URLPathSegmentPattern(ExactValuePattern(StringValue(pathSegment)))
            }
        }

        val specmaticPath = toSpecmaticFormattedPathString(parameters, openApiPath)

        return HttpPathPattern(pathPattern, specmaticPath)
    }

    private fun isParameter(pathSegment: String) = pathSegment.startsWith("{") && pathSegment.endsWith("}")

    private fun toSpecmaticFormattedPathString(
        parameters: List,
        openApiPath: String
    ): String {
        return parameters.filterIsInstance().foldRight(openApiPath) { it, specmaticPath ->
            val pattern = if (it.schema.enum != null) StringPattern("") else toSpecmaticPattern(it.schema, emptyList())
            specmaticPath.replace(
                "{${it.name}}", "(${it.name}:${pattern.typeName})"
            )
        }
    }

    private fun openApiOperations(pathItem: PathItem): Map {
        return linkedMapOf(
            "POST" to pathItem.post,
            "GET" to pathItem.get,
            "PATCH" to pathItem.patch,
            "PUT" to pathItem.put,
            "DELETE" to pathItem.delete
        ).filter { (_, value) -> value != null }.map { (key, value) -> key to OpenApiOperation(value!!) }.toMap()
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy