Skip to content

Commit

Permalink
Merge pull request #536 from znsio/openapi_file_upload
Browse files Browse the repository at this point in the history
openapi file upload support
  • Loading branch information
harikrishnan83 authored Oct 19, 2022
2 parents 798294d + 0a6209e commit 09c7972
Show file tree
Hide file tree
Showing 6 changed files with 383 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import `in`.specmatic.core.value.Value
import `in`.specmatic.core.wsdl.parser.message.MULTIPLE_ATTRIBUTE_VALUE
import `in`.specmatic.core.wsdl.parser.message.OCCURS_ATTRIBUTE_NAME
import `in`.specmatic.core.wsdl.parser.message.OPTIONAL_ATTRIBUTE_VALUE
import io.cucumber.messages.internal.com.fasterxml.jackson.databind.ObjectMapper
import io.cucumber.messages.types.Step
import io.ktor.util.reflect.*
import io.swagger.v3.oas.models.OpenAPI
Expand Down Expand Up @@ -53,14 +54,15 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
}

fun fromYAML(yamlContent: String, filePath: String): OpenApiSpecification {
val parseResult: SwaggerParseResult = OpenAPIV3Parser().readContents(yamlContent, null, resolveExternalReferences(), filePath)
val parseResult: SwaggerParseResult =
OpenAPIV3Parser().readContents(yamlContent, null, resolveExternalReferences(), filePath)
val openApi: OpenAPI? = parseResult.openAPI

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

parseResult.messages.filterNotNull().let {
if(it.isNotEmpty()) {
if (it.isNotEmpty()) {
val parserMessages = parseResult.messages.joinToString(System.lineSeparator())
logger.log("Error parsing file $filePath")
logger.log(parserMessages.prependIndent(" "))
Expand Down Expand Up @@ -228,12 +230,21 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI

val requestBodyExample: Map<String, Any> = requestBodyExample(operation, exampleName)

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

when {
requestExamples.isNotEmpty() -> Row(
requestExamples.keys.toList(),
requestExamples.values.toList().map { value: Any? -> value?.toString() ?: "" })
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
})
else -> Row()
}
}
Expand All @@ -244,7 +255,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
val scenarioName =
scenarioName(operation, response, httpRequestPattern, null)

val scenarioDetails = object: ScenarioDetailsForResult {
val scenarioDetails = object : ScenarioDetailsForResult {
override val ignoreFailure: Boolean
get() = false
override val name: String
Expand Down Expand Up @@ -276,7 +287,12 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
httpRequestPattern = httpRequestPattern,
httpResponsePattern = httpResponsePattern,
ignoreFailure = ignoreFailure,
examples = if(specmaticExampleRows.isNotEmpty()) listOf(Examples(specmaticExampleRows.first().columnNames, specmaticExampleRows)) else emptyList()
examples = if (specmaticExampleRows.isNotEmpty()) listOf(
Examples(
specmaticExampleRows.first().columnNames,
specmaticExampleRows
)
) else emptyList()
)
}
}.flatten()
Expand All @@ -292,8 +308,11 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
operation.requestBody?.content?.values?.firstOrNull()?.examples?.get(exampleName)?.value

val requestBodyExample: Map<String, Any> = if (requestExampleValue != null) {
if(operation.requestBody?.content?.entries?.first()?.key == "application/x-www-form-urlencoded" || operation.requestBody?.content?.entries?.first()?.key == "multipart/form-data") {
val jsonExample = attempt("Could not parse example $exampleName for operation \"${operation.summary}\"") { parsedJSON(requestExampleValue.toString()) as JSONObjectValue }
if (operation.requestBody?.content?.entries?.first()?.key == "application/x-www-form-urlencoded" || operation.requestBody?.content?.entries?.first()?.key == "multipart/form-data") {
val jsonExample =
attempt("Could not parse example $exampleName for operation \"${operation.summary}\"") {
parsedJSON(requestExampleValue.toString()) as JSONObjectValue
}
jsonExample.jsonObject.map { (key, value) ->
key to value.toString()
}.toMap()
Expand Down Expand Up @@ -338,7 +357,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
status: String,
headersMap: Map<String, Pattern>
): List<Triple<ApiResponse, MediaType, HttpResponsePattern>> {
if(response.content == null) {
if (response.content == null) {
val responsePattern = HttpResponsePattern(
headersPattern = HttpHeadersPattern(headersMap),
status = status.toIntOrNull() ?: DEFAULT_RESPONSE_CODE
Expand All @@ -350,7 +369,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
return response.content.map { (contentType, mediaType) ->
val responsePattern = HttpResponsePattern(
headersPattern = HttpHeadersPattern(headersMap),
status = if(status == "default") 1000 else status.toInt(),
status = if (status == "default") 1000 else status.toInt(),
body = when (contentType) {
"application/xml" -> toXMLPattern(mediaType)
else -> toSpecmaticPattern(mediaType)
Expand All @@ -365,9 +384,10 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
path: String, httpMethod: String, operation: Operation
): List<HttpRequestPattern> {

val contractSecuritySchemes: Map<String, OpenAPISecurityScheme> = openApi.components?.securitySchemes?.mapValues { (_, scheme) ->
toSecurityScheme(scheme)
} ?: emptyMap()
val contractSecuritySchemes: Map<String, OpenAPISecurityScheme> =
openApi.components?.securitySchemes?.mapValues { (_, scheme) ->
toSecurityScheme(scheme)
} ?: emptyMap()

val parameters = operation.parameters

Expand All @@ -383,7 +403,10 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
val urlMatcher = toURLMatcherWithOptionalQueryParams(path, securityQueryParams)
val headersPattern = HttpHeadersPattern(headersMap)
val requestPattern = HttpRequestPattern(
urlMatcher = urlMatcher, method = httpMethod, headersPattern = headersPattern, securitySchemes = operationSecuritySchemes
urlMatcher = urlMatcher,
method = httpMethod,
headersPattern = headersPattern,
securitySchemes = operationSecuritySchemes
)

return when (operation.requestBody) {
Expand All @@ -406,15 +429,28 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
resolveReferenceToSchema(mediaType.schema.`$ref`).second
}

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

MultiPartContentPattern(partNameWithPresence, toSpecmaticPattern(partSchema, emptyList()), partContentType)
}
val parts: List<MultiPartFormDataPattern> =
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
)
}
}

requestPattern.copy(multiPartFormDataPattern = parts)
}
Expand Down Expand Up @@ -449,14 +485,14 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
}

private fun toSecurityScheme(securityScheme: SecurityScheme): OpenAPISecurityScheme {
if(securityScheme.scheme == BEARER_SECURITY_SCHEME)
if (securityScheme.scheme == BEARER_SECURITY_SCHEME)
return BearerSecurityScheme()

if(securityScheme.type == SecurityScheme.Type.APIKEY) {
if(securityScheme.`in` == SecurityScheme.In.HEADER)
if (securityScheme.type == SecurityScheme.Type.APIKEY) {
if (securityScheme.`in` == SecurityScheme.In.HEADER)
return APIKeyInHeaderSecurityScheme(securityScheme.name)

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

Expand Down Expand Up @@ -503,6 +539,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
null -> NumberPattern()
else -> toEnum(schema, patternName) { enumValue -> NumberValue(enumValue.toString().toInt()) }
}
is BinarySchema -> BinaryPattern()
is NumberSchema -> NumberPattern()
is UUIDSchema -> StringPattern()
is DateTimeSchema -> DateTimePattern
Expand Down Expand Up @@ -548,8 +585,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI
else -> {
if (schema.nullable == true && schema.additionalProperties == null && schema.`$ref` == null) {
NullPattern
}
else if (schema.additionalProperties != null) {
} else if (schema.additionalProperties != null) {
toDictionaryPattern(schema, typeStack, patternName)
} else {
when (schema.`$ref`) {
Expand Down
49 changes: 49 additions & 0 deletions core/src/main/kotlin/in/specmatic/core/pattern/BinaryPattern.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package `in`.specmatic.core.pattern

import `in`.specmatic.core.Resolver
import `in`.specmatic.core.Result
import `in`.specmatic.core.mismatchResult
import `in`.specmatic.core.value.BinaryValue
import `in`.specmatic.core.value.JSONArrayValue
import `in`.specmatic.core.value.StringValue
import `in`.specmatic.core.value.Value
import org.apache.commons.lang3.RandomUtils

data class BinaryPattern(
override val typeAlias: String? = null,
) : Pattern, ScalarType {

override fun matches(sampleData: Value?, resolver: Resolver): Result {
return when (sampleData) {
is StringValue -> return Result.Success()
else -> mismatchResult("string", sampleData, resolver.mismatchMessages)
}
}

override fun encompasses(
otherPattern: Pattern,
thisResolver: Resolver,
otherResolver: Resolver,
typeStack: TypeStack
): Result {
return encompasses(this, otherPattern, thisResolver, otherResolver, typeStack)
}

override fun listOf(valueList: List<Value>, resolver: Resolver): Value {
return JSONArrayValue(valueList)
}

override fun generate(resolver: Resolver): Value = BinaryValue(RandomUtils.nextBytes(20))

override fun newBasedOn(row: Row, resolver: Resolver): List<Pattern> = listOf(this)
override fun newBasedOn(resolver: Resolver): List<Pattern> = listOf(this)
override fun negativeBasedOn(row: Row, resolver: Resolver): List<Pattern> {
return listOf(NullPattern, NumberPattern(), BooleanPattern)
}

override fun parse(value: String, resolver: Resolver): Value = StringValue(value)
override val typeName: String = "string"

override val pattern: Any = "(string)"
override fun toString(): String = pattern.toString()
}
61 changes: 61 additions & 0 deletions core/src/main/kotlin/in/specmatic/core/value/BinaryValue.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package `in`.specmatic.core.value

import `in`.specmatic.core.ExampleDeclarations
import `in`.specmatic.core.Result
import `in`.specmatic.core.pattern.ExactValuePattern
import `in`.specmatic.core.pattern.Pattern
import `in`.specmatic.core.pattern.StringPattern
import io.ktor.http.*
import io.ktor.util.*
import org.w3c.dom.Document
import org.w3c.dom.Node

data class BinaryValue(val byteArray: ByteArray = ByteArray(0)) : Value, ScalarValue, XMLValue {
override val httpContentType = "application/octet-stream"

override fun valueErrorSnippet(): String = displayableValue()

@OptIn(InternalAPI::class)
override fun displayableValue(): String = toStringLiteral().quote()
override fun toStringLiteral() = byteArray.toString()
override fun displayableType(): String = "binary"
override fun exactMatchElseType(): Pattern = ExactValuePattern(this)

override fun build(document: Document): Node = document.createTextNode(byteArray.toString())

override fun matchFailure(): Result.Failure =
Result.Failure("Unexpected child value found: $byteArray")

override fun addSchema(schema: XMLNode): XMLValue = this

override fun listOf(valueList: List<Value>): Value {
return JSONArrayValue(valueList)
}

override fun type(): Pattern = StringPattern()

override fun typeDeclarationWithKey(
key: String,
types: Map<String, Pattern>,
exampleDeclarations: ExampleDeclarations
): Pair<TypeDeclaration, ExampleDeclarations> =
primitiveTypeDeclarationWithKey(key, types, exampleDeclarations, displayableType(), byteArray.toString())

override fun typeDeclarationWithoutKey(
exampleKey: String,
types: Map<String, Pattern>,
exampleDeclarations: ExampleDeclarations
): Pair<TypeDeclaration, ExampleDeclarations> =
primitiveTypeDeclarationWithoutKey(
exampleKey,
types,
exampleDeclarations,
displayableType(),
byteArray.toString()
)

override val nativeValue: ByteArray
get() = byteArray

override fun toString() = byteArray.toString()
}
Loading

0 comments on commit 09c7972

Please sign in to comment.