diff --git a/README.md b/README.md index 909afb5..d55d22b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OpenApi Codec -![version](https://img.shields.io/badge/version-0.2.0-blue.svg) +![version](https://img.shields.io/badge/version-0.3.0-blue.svg) This microservice can validate open api dictionary, encode th2 messages or decode http body. @@ -140,10 +140,13 @@ Result of decode: ### Codec configs: -* checkUndefinedFields - Enable or Disable warnings for all undefined fields inside object structures, true by default. +* failOnUndefinedFields - fail on undefined fields inside object structures, (`true` by default). +* dateFormat - setup format of processed date (`yyyy-MM-dd Z` by default) +* dateTimeFormat - setup format of processed date-time (`yyyy/MM/dd HH:mm:ss` by default) + **validationSettings (open api dictionary)** -* enableRecommendations - Enable or Disable recommendations, true by default. +* enableRecommendations - Enable or Disable recommendations, `true` by default. * enableApacheNginxUnderscoreRecommendation - Enable or Disable the recommendation check for Apache/Nginx potentially ignoring header with underscore by default. * enableOneOfWithPropertiesRecommendation - Enable or Disable the recommendation check for schemas containing properties and oneOf definitions. * enableUnusedSchemasRecommendation - Enable or Disable the recommendation check for unused schemas. @@ -152,8 +155,8 @@ Result of decode: * enableInvalidTypeRecommendation - Enable or Disable the recommendation check for the 'type' attribute. **dictionaryParseOption (open api dictionary)** -* resolve - true by default; -* resolveCombinators - true by default; +* resolve - `true` by default; +* resolveCombinators - will combine some schemas into one due allOf statement (`false` by default); * resolveFully; * flatten; * flattenComposedSchemas; @@ -175,6 +178,13 @@ May be empty due to missing required fields ## Release notes +### 0.3.0 + ++ Feature: anyOf, allOf, oneOf support for objects and arrays ++ Feature: Date format support ++ Feature: DateTime format support ++ Fix: Reworked visitor interface + ### 0.2.0 + Feature: check for undefined fields and throw errors if they are found diff --git a/gradle.properties b/gradle.properties index 21726cd..22b590a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ kotlin.code.style=official kotlin_version=1.5.31 -release_version=0.2.0 +release_version=0.3.0 vcs_url=https://github.com/th2-net/th2-codec-open-api \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/OpenApiCodec.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/OpenApiCodec.kt index e291248..1540067 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/OpenApiCodec.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/OpenApiCodec.kt @@ -23,12 +23,14 @@ import com.exactpro.th2.codec.openapi.schemacontainer.RequestContainer import com.exactpro.th2.codec.openapi.schemacontainer.ResponseContainer import com.exactpro.th2.codec.openapi.throwable.DecodeException import com.exactpro.th2.codec.openapi.throwable.EncodeException +import com.exactpro.th2.codec.openapi.utils.containingFormatOrNull import com.exactpro.th2.codec.openapi.utils.extractType +import com.exactpro.th2.codec.openapi.utils.getByMethod import com.exactpro.th2.codec.openapi.utils.getEndPoint import com.exactpro.th2.codec.openapi.utils.getMethods -import com.exactpro.th2.codec.openapi.utils.getSchema import com.exactpro.th2.codec.openapi.writer.SchemaWriter import com.exactpro.th2.codec.openapi.writer.visitors.VisitorFactory +import com.exactpro.th2.codec.openapi.writer.visitors.VisitorSettings import com.exactpro.th2.common.grpc.MessageGroup import com.exactpro.th2.common.grpc.RawMessage import com.exactpro.th2.common.message.plusAssign @@ -49,12 +51,16 @@ import com.exactpro.th2.common.message.sessionAlias import io.swagger.v3.oas.models.PathItem import io.swagger.v3.oas.models.parameters.Parameter import mu.KotlinLogging +import java.lang.IllegalStateException +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter -class OpenApiCodec(private val dictionary: OpenAPI, settings: OpenApiCodecSettings) : IPipelineCodec { +class OpenApiCodec(private val dictionary: OpenAPI, val settings: OpenApiCodecSettings) : IPipelineCodec { private val typeToSchema: Map private val patternToPathItem: List> - private val schemaWriter = SchemaWriter(dictionary, settings.checkUndefinedFields) + private val schemaWriter = SchemaWriter(dictionary) + private val visitorSettings = VisitorSettings(dictionary, SimpleDateFormat(settings.dateFormat), DateTimeFormatter.ofPattern(settings.dateTimeFormat)) init { val mapForName = mutableMapOf() @@ -125,11 +131,13 @@ class OpenApiCodec(private val dictionary: OpenAPI, settings: OpenApiCodecSettin val container = checkNotNull(typeToSchema[messageType]) { "There no message $messageType in dictionary" } - builder += createHeaderMessage(container, parsedMessage).apply { - if (parsedMessage.hasParentEventId()) parentEventId = parsedMessage.parentEventId - sessionAlias = parsedMessage.sessionAlias - metadataBuilder.putAllProperties(parsedMessage.metadata.propertiesMap) - LOGGER.trace { "Created header message for ${parsedMessage.messageType}: ${this.messageType}" } + if (container.headers.isNotEmpty() || container.body == null) { + builder += createHeaderMessage(container, parsedMessage).apply { + if (parsedMessage.hasParentEventId()) parentEventId = parsedMessage.parentEventId + sessionAlias = parsedMessage.sessionAlias + metadataBuilder.putAllProperties(parsedMessage.metadata.propertiesMap) + LOGGER.trace { "Created header message for ${parsedMessage.messageType}: ${this.messageType}" } + } } try { @@ -150,10 +158,13 @@ class OpenApiCodec(private val dictionary: OpenAPI, settings: OpenApiCodecSettin } LOGGER.debug { "Start of message encoding: ${message.messageType}" } - checkNotNull(messageSchema.type) {"Type of schema [${messageSchema.name}] wasn't filled"} - val visitor = VisitorFactory.createEncodeVisitor(container.bodyFormat!!, messageSchema.type, message) - schemaWriter.traverse(visitor, messageSchema) + val visitor = try { + VisitorFactory.createEncodeVisitor(container.bodyFormat!!, messageSchema, message, visitorSettings) + } catch (e: Exception) { + throw IllegalStateException("Cannot create encode visitor for message: ${message.messageType} - ${container.body}", e) + } + schemaWriter.traverse(visitor, messageSchema, settings.checkUndefinedFields) val result = visitor.getResult() LOGGER.trace { "Result of encoded message ${message.messageType}: $result" } @@ -165,9 +176,22 @@ class OpenApiCodec(private val dictionary: OpenAPI, settings: OpenApiCodecSettin container.fillHttpMetadata(metadataBuilder) metadataBuilder.apply { putAllProperties(message.metadata.propertiesMap) - this.id = metadata.id - this.timestamp = metadata.timestamp + this.id = message.metadata.id + this.timestamp = message.metadata.timestamp protocol = message.metadata.protocol + + when (container) { + is ResponseContainer -> this.putProperties(CODE_FIELD, container.code) + is RequestContainer -> { + this.putProperties(URI_FIELD, if (container.params.isNotEmpty()) { + container.uriPattern.resolve(container.params, message.getMessage(URI_PARAMS_FIELD).orEmpty().fieldsMap.mapValues { it.value.simpleValue }) + } else { + container.uriPattern.pattern + }) + this.putProperties(METHOD_FIELD, container.method) + } + else -> error("Wrong type of Http Route Container") + } } body = result }.build() @@ -189,7 +213,7 @@ class OpenApiCodec(private val dictionary: OpenAPI, settings: OpenApiCodecSettin builder += runCatching { decodeBody(message, messages[1].rawMessage!!) }.getOrElse { - throw DecodeException("Cannot parse body of http message", it) + throw DecodeException("Cannot decode body of http message", it) } } @@ -198,57 +222,77 @@ class OpenApiCodec(private val dictionary: OpenAPI, settings: OpenApiCodecSettin private fun decodeBody(header: Message, rawMessage: RawMessage): Message { val body = rawMessage.body + val (messageType, container) = searchContainer(header, rawMessage.metadata) + val schema = dictionary.getEndPoint(checkNotNull(container.body) { "Container: $messageType did not contain schema body" }) + val format = checkNotNull(container.bodyFormat) { "Container: $messageType did not contain schema bodyFormat" } + + val visitor = try { + VisitorFactory.createDecodeVisitor(format, schema, body, visitorSettings) + } catch (e: Exception) { + throw IllegalStateException("Cannot create decode visitor for message: ${header.messageType} - ${container.body}", e) + } + + schemaWriter.traverse(visitor, schema, settings.checkUndefinedFields) + return visitor.getResult().apply { + if(rawMessage.hasParentEventId()) parentEventId = rawMessage.parentEventId + sessionAlias = rawMessage.sessionAlias + this.messageType = messageType + metadataBuilder.apply { + id = rawMessage.metadata.id + timestamp = rawMessage.metadata.timestamp + protocol = rawMessage.metadata.protocol + putAllProperties(rawMessage.metadata.propertiesMap) + } + }.build() + } - val bodyFormat = header.getList(HEADERS_FIELD) + private fun searchContainer(header: Message, rawMetadata: RawMessageMetadata): Pair { + val headerFormat = header.getList(HEADERS_FIELD) ?.first { it.messageValue.getString("name") == "Content-Type" } ?.messageValue ?.getString("value") ?.extractType() ?: "null" - val uri: String val method: String + val schemaFormat: String var code = "" - val pairFound: Pair + val messageType: String + + val patternToPathItem: Pair - val messageSchema = when (header.messageType) { + when (header.messageType) { RESPONSE_MESSAGE -> { - uri = requireNotNull(rawMessage.metadata.propertiesMap[URI_PROPERTY]) { "URI property in metadata from response is required" } - method = requireNotNull(rawMessage.metadata.propertiesMap[METHOD_PROPERTY]?.lowercase()) { "Method property in metadata from response is required" } + val uri = requireNotNull(rawMetadata.propertiesMap[URI_PROPERTY]) { "URI property in metadata from response is required" } + method = requireNotNull(rawMetadata.propertiesMap[METHOD_PROPERTY]?.lowercase()) { "Method property in metadata from response is required" } code = requireNotNull(header.getString(STATUS_CODE_FIELD)) { "Code status field required inside of http response message" } - pairFound = checkNotNull(patternToPathItem.firstOrNull { it.first.matches(uri) }) { "Cannot find path-item by uri: $uri" } - dictionary.getEndPoint(pairFound.second.getSchema(method, code, bodyFormat)) + patternToPathItem = checkNotNull(this.patternToPathItem.firstOrNull { it.first.matches(uri) }) { "Cannot find path-item by uri: $uri" } + val content = patternToPathItem.second.getByMethod(method)?.responses?.get(code)?.content + schemaFormat = checkNotNull(content?.containingFormatOrNull(headerFormat)) { + "Schema Response [${patternToPathItem.first.pattern}] with method: [$method] and code: [$code] did not contain type $headerFormat" + } + messageType = combineName(patternToPathItem.first.pattern, method, code, schemaFormat) } REQUEST_MESSAGE -> { - uri = requireNotNull(header.getString(URI_FIELD)) { "URI field in request is required" } + val uri = requireNotNull(header.getString(URI_FIELD)) { "URI field in request is required" } method = requireNotNull(header.getString(METHOD_FIELD)) { "Method field in request is required" } - pairFound = checkNotNull(patternToPathItem.firstOrNull { it.first.matches(uri) }) { "Cannot find path-item by uri: $uri" } - dictionary.getEndPoint(pairFound.second.getSchema(method, null, bodyFormat)) + patternToPathItem = checkNotNull(this.patternToPathItem.firstOrNull { it.first.matches(uri) }) { "Cannot find path-item by uri: $uri" } + val content = patternToPathItem.second.getByMethod(method)?.requestBody?.content + schemaFormat = checkNotNull(content?.containingFormatOrNull(headerFormat)) { + "Schema Response [${patternToPathItem.first.pattern}] with method: [$method] did not contain type $headerFormat" + } + messageType = combineName(patternToPathItem.first.pattern, method, code, schemaFormat) } - else -> error("Unsupported message type: ${header.messageType}") + else -> error("Unsupported header message type: ${header.messageType}") } - checkNotNull(messageSchema.type) { "Type of schema [${messageSchema.name}] wasn't filled" } + val container = checkNotNull(typeToSchema[messageType]) { "Container for path: [${patternToPathItem.first.pattern}] with method: [$method], code: [$code] and type [$schemaFormat] wasn't found" } - val type = combineName(pairFound.first.pattern, method, code, bodyFormat) + LOGGER.debug { "Container for ${header.messageType} with messageType: $messageType was found" } - LOGGER.debug { "Schema for ${header.messageType} with type-name: $type was found" } - - val visitor = VisitorFactory.createDecodeVisitor(bodyFormat, messageSchema.type, body) - schemaWriter.traverse(visitor, messageSchema) - return visitor.getResult().apply { - if(rawMessage.hasParentEventId()) parentEventId = rawMessage.parentEventId - sessionAlias = rawMessage.sessionAlias - this.messageType = type - metadataBuilder.apply { - id = rawMessage.metadata.id - timestamp = metadata.timestamp - protocol = rawMessage.metadata.protocol - putAllProperties(rawMessage.metadata.propertiesMap) - } - }.build() + return messageType to container } @@ -291,16 +335,14 @@ class OpenApiCodec(private val dictionary: OpenAPI, settings: OpenApiCodecSettin }) } - if (container.headers.isNotEmpty()) { - val headerMessage = message.getMessage(HEADER_PARAMS_FIELD).orEmpty() - container.headers.forEach { (name, value) -> - headerMessage[name]?.let { header -> - headers.add(message().apply { - addField("name", name) - addField("value", header.simpleValue) - }) - } ?: run { if (value.required) error("Header param [$name] is required for ${message.messageType} message") } - } + val headerMessage = message.getMessage(HEADER_PARAMS_FIELD).orEmpty() + container.headers.forEach { (name, value) -> + headerMessage[name]?.let { header -> + headers.add(message().apply { + addField("name", name) + addField("value", header.simpleValue) + }) + } ?: run { if (value.required) error("Header param [$name] is required for ${message.messageType} message") } } addField(HEADERS_FIELD, headers) @@ -308,7 +350,7 @@ class OpenApiCodec(private val dictionary: OpenAPI, settings: OpenApiCodecSettin } } - fun combineName(vararg steps: String) = steps.asSequence().flatMap { it.split(COMBINER_REGEX) }.joinToString("") { it.lowercase().capitalize() } + fun combineName(vararg steps: String) = steps.asSequence().flatMap { it.replace("*","Any").split(COMBINER_REGEX) }.joinToString("") { it.lowercase().capitalize() } companion object { private val LOGGER = KotlinLogging.logger { } diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/OpenApiCodecSettings.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/OpenApiCodecSettings.kt index b6bef8e..4799966 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/OpenApiCodecSettings.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/OpenApiCodecSettings.kt @@ -27,7 +27,9 @@ class OpenApiCodecSettings : IPipelineCodecSettings { val dictionaryParseOption: ParseOptions = ParseOptions().apply { isResolve = true isResolveFully = true - isResolveCombinators = true + isResolveCombinators = false } val checkUndefinedFields: Boolean = true + val dateFormat: String = "yyyy-MM-dd Z" + val dateTimeFormat: String = "yyyy/MM/dd HH:mm:ss" } diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/CommonUtils.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/CommonUtils.kt index 48c7894..d677027 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/CommonUtils.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/CommonUtils.kt @@ -18,6 +18,7 @@ package com.exactpro.th2.codec.openapi.utils import com.exactpro.th2.common.grpc.Message import com.exactpro.th2.common.grpc.Value +import com.exactpro.th2.common.grpc.Value.KindCase.NULL_VALUE import com.exactpro.th2.common.message.get import com.exactpro.th2.common.message.messageType import com.exactpro.th2.common.value.getString @@ -27,7 +28,9 @@ import com.exactpro.th2.common.value.getString * @param required check if value is required, used only if extracted value was null */ fun Message.getField(fieldName: String, required: Boolean): Value? = this[fieldName].apply { - if (required) checkNotNull(this) { "Field [$fieldName] is required for message [$messageType]" } + if (required) { + check(this != null && this.kindCase != NULL_VALUE) { "Field [$fieldName] is required for message [$messageType]" } + } } fun Value.getBoolean(): Boolean? = this.getString()?.toBooleanStrictOrNull() \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/JsonUtils.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/JsonUtils.kt index 3682869..f3dcde5 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/JsonUtils.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/JsonUtils.kt @@ -78,21 +78,6 @@ fun JsonNode.validateAsBigDecimal(): BigDecimal = when { else -> error("Cannot convert $this to BigDecimal") } -fun JsonNode.validateAsInteger(): Int = when { - isNumber -> asInt() - else -> error("Cannot convert $this to Int") -} - -fun JsonNode.validateAsDouble(): Double = when { - isNumber -> asDouble() - else -> error("Cannot convert $this to Double") -} - -fun JsonNode.validateAsFloat(): Float = when { - isNumber -> asText().toFloat() - else -> error("Cannot convert $this to Float") -} - fun JsonNode.validateAsObject(): ObjectNode = when { isObject -> this as ObjectNode else -> error("Cannot convert $this to Object") diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/OpenApiUtils.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/OpenApiUtils.kt index d29ed39..c89c9ff 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/OpenApiUtils.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/utils/OpenApiUtils.kt @@ -19,10 +19,14 @@ package com.exactpro.th2.codec.openapi.utils 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.media.Content import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.parser.models.RefType private val METHODS = listOf("get", "put", "delete", "post") +const val ARRAY_TYPE = "array" +const val JSON_FORMAT = "application/json" +const val ANY_FORMAT = "*/*" fun PathItem.getByMethod(method: String) = when (method.lowercase()) { "get" -> get @@ -42,9 +46,9 @@ fun OpenAPI.findByRef(ref: String): Schema<*>? { } else error("Unsupported ref type: $ref") } -fun OpenAPI.getEndPoint(schema: Schema<*>): Schema<*> = when (schema.`$ref`) { +fun OpenAPI.getEndPoint(schema: Schema<*>): Schema<*> = when(schema.`$ref`) { null -> schema - else -> findByRef(schema.`$ref`) ?: error("Unsupported schema, no reference was found: ${schema.`$ref`}") + else -> findByRef(schema.`$ref`) ?: error("Unsupported Object schema, no reference was found: ${schema.`$ref`}") } enum class JsonSchemaTypes(val type: String) { @@ -63,15 +67,14 @@ fun Schema<*>.checkEnum(value: T?, name: String) { fun String.extractType() = substringBefore(';').trim() -fun PathItem.getSchema(method: String, code: String?, bodyFormat: String): Schema<*> { - val methodRootSchema = this.getByMethod(method) +fun Content.containingFormatOrNull(httpHeader: String) = when { + httpHeader == JSON_FORMAT && ANY_FORMAT in this -> ANY_FORMAT + httpHeader in this -> httpHeader + else -> null +} - val schema = when (code) { - null -> methodRootSchema?.requestBody?.content?.get(bodyFormat)?.schema - else -> methodRootSchema?.responses?.get(code)?.content?.get(bodyFormat)?.schema - } +fun Schema<*>.getExclusiveProperties(against: List>): List = properties.keys.filter { field -> + against.none { field in it.properties } +} - return checkNotNull(schema) { - "Schema with method: [$method], code: [$code] and type [$bodyFormat] wasn't found" - } -} \ No newline at end of file +fun Schema<*>.requiredContains(name: String) = this.required?.contains(name) ?: false \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/SchemaWriter.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/SchemaWriter.kt index 094b5fc..c850f6a 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/SchemaWriter.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/SchemaWriter.kt @@ -16,99 +16,73 @@ package com.exactpro.th2.codec.openapi.writer -import com.exactpro.th2.codec.CodecException +import com.exactpro.th2.codec.openapi.utils.ARRAY_TYPE import com.exactpro.th2.codec.openapi.utils.getEndPoint +import com.exactpro.th2.codec.openapi.utils.requiredContains import com.exactpro.th2.codec.openapi.writer.visitors.SchemaVisitor import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.media.ArraySchema +import io.swagger.v3.oas.models.media.BinarySchema +import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.ByteArraySchema +import io.swagger.v3.oas.models.media.ComposedSchema +import io.swagger.v3.oas.models.media.DateSchema +import io.swagger.v3.oas.models.media.DateTimeSchema +import io.swagger.v3.oas.models.media.EmailSchema +import io.swagger.v3.oas.models.media.FileSchema +import io.swagger.v3.oas.models.media.IntegerSchema +import io.swagger.v3.oas.models.media.MapSchema +import io.swagger.v3.oas.models.media.NumberSchema +import io.swagger.v3.oas.models.media.PasswordSchema import io.swagger.v3.oas.models.media.Schema -import io.swagger.v3.parser.util.SchemaTypeUtil.BOOLEAN_TYPE -import io.swagger.v3.parser.util.SchemaTypeUtil.INTEGER_TYPE -import io.swagger.v3.parser.util.SchemaTypeUtil.NUMBER_TYPE -import io.swagger.v3.parser.util.SchemaTypeUtil.OBJECT_TYPE -import io.swagger.v3.parser.util.SchemaTypeUtil.STRING_TYPE -import java.math.BigDecimal +import io.swagger.v3.oas.models.media.StringSchema +import io.swagger.v3.oas.models.media.UUIDSchema -class SchemaWriter constructor(private val openApi: OpenAPI, private val failOnUndefined: Boolean = true) { +class SchemaWriter constructor(private val openApi: OpenAPI) { fun traverse( - schemaVisitor: SchemaVisitor<*, *>, - msgStructure: Schema<*> + visitor: SchemaVisitor<*, *>, + msgStructure: Schema<*>, + checkForUndefinedFields: Boolean = true ) { - val schema = openApi.getEndPoint(msgStructure) - - when (schema.type) { - ARRAY_TYPE -> processProperty(schema, schemaVisitor, ARRAY_TYPE) - OBJECT_TYPE -> { - requireNotNull(schema.properties) {"Properties in object are required: $schema"} - if (failOnUndefined) { - schemaVisitor.getUndefinedFields(schema.properties.keys)?.let { - check(it.isEmpty()) { "Undefined fields were found inside of ${schema.name}: ${it.joinToString()}" } + when (msgStructure) { + is ArraySchema -> visitor.visit(ARRAY_TYPE, msgStructure, true) + is ComposedSchema -> { + when { + !msgStructure.allOf.isNullOrEmpty() -> visitor.allOf(msgStructure.allOf.map(openApi::getEndPoint)).forEach { + traverse(visitor, it, false) } + !msgStructure.oneOf.isNullOrEmpty() -> visitor.oneOf(msgStructure.oneOf.map(openApi::getEndPoint)).also { + traverse(visitor, it, true) + } + !msgStructure.anyOf.isNullOrEmpty() -> visitor.anyOf(msgStructure.anyOf.map(openApi::getEndPoint)).forEach { + traverse(visitor, it, false) + } + else -> error("Composed schema has no allOf, oneOf, anyOf definitions") } - - schema.properties.forEach { (name, property) -> - processProperty(openApi.getEndPoint(property), schemaVisitor, name, schema.required?.contains(name) ?: false) - } - } - } - } - - private fun processProperty(property: Schema<*>, visitor: SchemaVisitor<*, *>, name: String, required: Boolean = false) { - runCatching { - when(property.type) { - ARRAY_TYPE -> processArrayProperty(property as ArraySchema, visitor, name, required) - INTEGER_TYPE -> when (property.format) { - "int64" -> visitor.visit(name, property.default as? Long, property, required) - null, "", "int32" -> visitor.visit(name, property.default as? Int, property, required) - else -> error("Unsupported format of '$INTEGER_TYPE' property $name: ${property.format}") - } - BOOLEAN_TYPE -> visitor.visit(name, property.default as? Boolean, property, required) - NUMBER_TYPE -> when (property.format) { - "float" -> visitor.visit(name, property.default as? Float, property, required) - "double" -> visitor.visit(name, property.default as? Double, property, required) - "-" -> visitor.visit(name, property.default as? BigDecimal, property, required) - null, "" -> visitor.visit(name, property.default as? String, property, required) - else -> error("Unsupported format of '$NUMBER_TYPE' property $name: ${property.format}") - } - STRING_TYPE -> visitor.visit(name, property.default as? String, property, required) - OBJECT_TYPE -> visitor.visit(name, property.default as? Schema<*>, property, required, this) - else -> error("Unsupported type of property") } - }.onFailure { - throw CodecException("Cannot parse field [$name] inside of schema with type ${property.type}", it) - } - - } - - @Suppress("UNCHECKED_CAST") - private fun processArrayProperty(property: ArraySchema, visitor: SchemaVisitor<*, *>, name: String, required: Boolean = false) { - runCatching { - when(property.items.type) { - INTEGER_TYPE -> when (property.items.format) { - "int64" -> visitor.visitLongCollection(name, property.default as? List, property, required) - null, "", "int32" -> visitor.visitIntegerCollection(name, property.default as? List, property, required) - else -> error("Unsupported format of '$INTEGER_TYPE' property: ${property.format}") + else -> { + if (checkForUndefinedFields) { + visitor.checkUndefined(msgStructure) } - BOOLEAN_TYPE -> visitor.visitBooleanCollection(name, property.default as? List, property, required) - NUMBER_TYPE -> when (property.items.format) { - "float" -> visitor.visitFloatCollection(name, property.default as? List, property, required) - "double" -> visitor.visitDoubleCollection(name, property.default as? List, property, required) - null, "" -> visitor.visitStringCollection(name, property.default as? List, property, required) - "-" -> visitor.visitBigDecimalCollection(name, property.default as? List, property, required) - else -> error("Unsupported format of '$NUMBER_TYPE' property: ${property.format}") + openApi.getEndPoint(msgStructure).let { + it.properties?.forEach { (name, property) -> + when(property) { + is ArraySchema -> visitor.visit(name, property, msgStructure.requiredContains(name)) + is StringSchema -> visitor.visit(name, property , msgStructure.requiredContains(name)) + is IntegerSchema -> visitor.visit(name, property, msgStructure.requiredContains(name)) + is NumberSchema -> visitor.visit(name, property, msgStructure.requiredContains(name)) + is BooleanSchema -> visitor.visit(name, property, msgStructure.requiredContains(name)) + is DateSchema -> visitor.visit(name, property, msgStructure.requiredContains(name)) + is DateTimeSchema -> visitor.visit(name, property, msgStructure.requiredContains(name)) + is ComposedSchema -> visitor.visit(name, property, msgStructure.requiredContains(name)) + is PasswordSchema, is EmailSchema, is BinarySchema, is ByteArraySchema, is FileSchema, is MapSchema, is UUIDSchema -> throw UnsupportedOperationException("${property::class.simpleName} isn't supported for now") + else -> visitor.visit(name, property, msgStructure.requiredContains(name), checkForUndefinedFields) + } + } ?: error("Schema ${it.name}:${it.type} have no properties to process") } - STRING_TYPE -> visitor.visitStringCollection(name, property.default as? List, property, required) - OBJECT_TYPE -> visitor.visitObjectCollection(name, property.default as? List, property, required, this) - else -> error("Unsupported type of property") } - }.onFailure { - throw CodecException("Cannot parse array field [$name] inside of schema with type ${property.type}", it) } } - - companion object { - const val ARRAY_TYPE = "array" - } } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/SchemaVisitor.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/SchemaVisitor.kt index f60d750..c65a621 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/SchemaVisitor.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/SchemaVisitor.kt @@ -16,38 +16,70 @@ package com.exactpro.th2.codec.openapi.writer.visitors -import com.exactpro.th2.codec.openapi.writer.SchemaWriter +import com.exactpro.th2.codec.openapi.utils.getExclusiveProperties import com.exactpro.th2.common.grpc.Message import com.google.protobuf.ByteString +import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.media.ArraySchema +import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.ComposedSchema +import io.swagger.v3.oas.models.media.DateSchema +import io.swagger.v3.oas.models.media.DateTimeSchema +import io.swagger.v3.oas.models.media.IntegerSchema +import io.swagger.v3.oas.models.media.NumberSchema import io.swagger.v3.oas.models.media.Schema -import java.math.BigDecimal +import io.swagger.v3.oas.models.media.StringSchema +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter sealed class SchemaVisitor { + protected abstract val settings: VisitorSettings abstract val from: FromType - abstract fun visit(fieldName: String, defaultValue: Schema<*>?, fldStruct: Schema<*>, required: Boolean = false, schemaWriter: SchemaWriter) - abstract fun visit(fieldName: String, defaultValue: String?, fldStruct: Schema<*>, required: Boolean = false) - abstract fun visit(fieldName: String, defaultValue: Boolean?, fldStruct: Schema<*>, required: Boolean = false) - abstract fun visit(fieldName: String, defaultValue: Int?, fldStruct: Schema<*>, required: Boolean = false) - abstract fun visit(fieldName: String, defaultValue: Float?, fldStruct: Schema<*>, required: Boolean = false) - abstract fun visit(fieldName: String, defaultValue: Double?, fldStruct: Schema<*>, required: Boolean = false) - abstract fun visit(fieldName: String, defaultValue: BigDecimal?, fldStruct: Schema<*>, required: Boolean = false) - abstract fun visit(fieldName: String, defaultValue: Long?, fldStruct: Schema<*>, required: Boolean = false) - abstract fun visitBooleanCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean = false) - abstract fun visitIntegerCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean = false) - abstract fun visitBigDecimalCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean = false) - abstract fun visitStringCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean = false) - abstract fun visitDoubleCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean = false) - abstract fun visitFloatCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean = false) - abstract fun visitLongCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean = false) - abstract fun visitObjectCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean = false, schemaWriter: SchemaWriter) - abstract fun getUndefinedFields(fields: MutableSet): Set? - abstract fun getResult(): ToType + protected abstract fun getFieldNames(): Collection + abstract fun visit(fieldName: String, fldStruct: Schema<*>, required: Boolean, throwUndefined: Boolean = true) + abstract fun visit(fieldName: String, fldStruct: ArraySchema, required: Boolean, throwUndefined: Boolean = true) + abstract fun visit(fieldName: String, fldStruct: ComposedSchema, required: Boolean) + abstract fun visit(fieldName: String, fldStruct: NumberSchema, required: Boolean) + abstract fun visit(fieldName: String, fldStruct: IntegerSchema, required: Boolean) + abstract fun visit(fieldName: String, fldStruct: StringSchema, required: Boolean) + abstract fun visit(fieldName: String, fldStruct: BooleanSchema, required: Boolean) + abstract fun visit(fieldName: String, fldStruct: DateSchema, required: Boolean) + abstract fun visit(fieldName: String, fldStruct: DateTimeSchema, required: Boolean) - abstract class EncodeVisitor : SchemaVisitor() + fun oneOf(list: List>): Schema<*> = chooseOneOf(list.filter(this::checkAgainst)) + + fun anyOf(list: List>): List> = list.filter(this::checkAgainst).also { + check(it.isNotEmpty()) { "AnyOf statement had zero valid schemas" } + } + + fun allOf(list: List>): List> = list.filter(this::checkAgainst).also { + check(list.size != it.size) { "AllOf statement have only ${it.size} valid schemas of ${list.size} available" } + } + fun checkUndefined(objectSchema: Schema<*>) { + val undefined = getFieldNames() - objectSchema.properties.keys + check(undefined.isEmpty()) { "Message have undefined fields: ${undefined.joinToString(", ")}" } + } + + private fun checkAgainst(fldStruct: Schema<*>): Boolean = fldStruct.required.isNullOrEmpty() || getFieldNames().containsAll(fldStruct.required) + + private fun chooseOneOf(schemas: List>): Schema<*> = when(schemas.size) { + 0 -> error("OneOf statement have 0 valid schemas") + 1 -> schemas[0] + else -> getFieldNames().let { names -> + getFieldNames() + schemas.find { schema -> + schema.getExclusiveProperties(schemas - schema).run { + names.any(this::contains) + } + } ?: schemas[0] + } + } + + abstract class EncodeVisitor : SchemaVisitor() abstract class DecodeVisitor : SchemaVisitor() } +data class VisitorSettings(val openAPI: OpenAPI, val dateFormat: SimpleDateFormat, val dateTimeFormat: DateTimeFormatter) diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/VisitorFactory.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/VisitorFactory.kt index 9797eab..34daaed 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/VisitorFactory.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/VisitorFactory.kt @@ -17,39 +17,64 @@ package com.exactpro.th2.codec.openapi.writer.visitors import com.exactpro.th2.codec.openapi.utils.JsonSchemaTypes +import com.exactpro.th2.codec.openapi.utils.getEndPoint import com.exactpro.th2.codec.openapi.writer.visitors.json.DecodeJsonArrayVisitor import com.exactpro.th2.codec.openapi.writer.visitors.json.DecodeJsonObjectVisitor import com.exactpro.th2.codec.openapi.writer.visitors.json.EncodeJsonArrayVisitor import com.exactpro.th2.codec.openapi.writer.visitors.json.EncodeJsonObjectVisitor import com.exactpro.th2.common.grpc.Message import com.google.protobuf.ByteString +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.media.ArraySchema +import io.swagger.v3.oas.models.media.ComposedSchema +import io.swagger.v3.oas.models.media.Schema object VisitorFactory { private const val JSON_FORMAT = "application/json" - fun createEncodeVisitor(format: String, type: String, message: Message): SchemaVisitor.EncodeVisitor<*> { + fun createEncodeVisitor(format: String, schema: Schema<*>, message: Message, visitorSettings: VisitorSettings): SchemaVisitor.EncodeVisitor<*> { when (format) { JSON_FORMAT -> { - return when (JsonSchemaTypes.getType(type)) { - JsonSchemaTypes.ARRAY -> EncodeJsonArrayVisitor(message) - JsonSchemaTypes.OBJECT -> EncodeJsonObjectVisitor(message) - else -> error("Unsupported type of json schema [$type], array or object required") + return when (schema.defineType(visitorSettings.openAPI)) { + JsonSchemaTypes.ARRAY -> EncodeJsonArrayVisitor(message, visitorSettings) + JsonSchemaTypes.OBJECT -> EncodeJsonObjectVisitor(message, visitorSettings) } } - else -> error("Unsupported format of message $format") + else -> error("Unsupported format of message $format for encode") } } - fun createDecodeVisitor(format: String, type: String, data: ByteString): SchemaVisitor.DecodeVisitor<*> { + fun createDecodeVisitor(format: String, schema: Schema<*>, data: ByteString, visitorSettings: VisitorSettings): SchemaVisitor.DecodeVisitor<*> { when (format) { JSON_FORMAT -> { - return when (JsonSchemaTypes.getType(type)) { - JsonSchemaTypes.ARRAY -> DecodeJsonArrayVisitor(data.toStringUtf8()) - JsonSchemaTypes.OBJECT -> DecodeJsonObjectVisitor(data.toStringUtf8()) - else -> error("Unsupported type of json schema [$type], array or object required") + return when (schema.defineType(visitorSettings.openAPI)) { + JsonSchemaTypes.ARRAY -> DecodeJsonArrayVisitor(data.toStringUtf8(), visitorSettings) + JsonSchemaTypes.OBJECT -> DecodeJsonObjectVisitor(data.toStringUtf8(), visitorSettings) } } - else -> error("Unsupported format of message $format") + else -> error("Unsupported format of message $format for decode") } } + + private fun Schema<*>.defineType(dictionary: OpenAPI): JsonSchemaTypes { + return when (this) { + is ArraySchema -> JsonSchemaTypes.ARRAY + is ComposedSchema -> { + val schemas = when { + !this.anyOf.isNullOrEmpty() -> this.anyOf + !this.oneOf.isNullOrEmpty() -> this.oneOf + !this.allOf.isNullOrEmpty() -> this.allOf + else -> error("Unsupported state of composed schema, [ anyOf | oneOf | allOf ] list is empty") + } + when (dictionary.getEndPoint(schemas.first())) { + is ArraySchema -> JsonSchemaTypes.ARRAY + is ComposedSchema -> error("Unsupported level of abstraction, cannot parse composed scheme inside of composed scheme") + //TODO: More than one level of abstraction isn't impossible, need to implement later, this is fast try of realization + else -> JsonSchemaTypes.OBJECT + } + } + else -> JsonSchemaTypes.OBJECT + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/DecodeJsonArrayVisitor.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/DecodeJsonArrayVisitor.kt index f753b86..a4815d0 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/DecodeJsonArrayVisitor.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/DecodeJsonArrayVisitor.kt @@ -16,102 +16,84 @@ package com.exactpro.th2.codec.openapi.writer.visitors.json +import com.exactpro.th2.codec.openapi.utils.getEndPoint import com.exactpro.th2.codec.openapi.utils.validateAsBigDecimal import com.exactpro.th2.codec.openapi.utils.validateAsBoolean -import com.exactpro.th2.codec.openapi.utils.validateAsDouble -import com.exactpro.th2.codec.openapi.utils.validateAsFloat -import com.exactpro.th2.codec.openapi.utils.validateAsInteger import com.exactpro.th2.codec.openapi.utils.validateAsLong import com.exactpro.th2.codec.openapi.utils.validateAsObject import com.exactpro.th2.codec.openapi.writer.SchemaWriter -import com.exactpro.th2.codec.openapi.writer.visitors.SchemaVisitor.DecodeVisitor +import com.exactpro.th2.codec.openapi.writer.visitors.SchemaVisitor +import com.exactpro.th2.codec.openapi.writer.visitors.VisitorSettings import com.exactpro.th2.common.grpc.Message import com.exactpro.th2.common.message.addField import com.exactpro.th2.common.message.message import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.JsonNodeFactory import io.swagger.v3.oas.models.media.ArraySchema +import io.swagger.v3.oas.models.media.BinarySchema +import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.ByteArraySchema +import io.swagger.v3.oas.models.media.ComposedSchema +import io.swagger.v3.oas.models.media.DateSchema +import io.swagger.v3.oas.models.media.DateTimeSchema +import io.swagger.v3.oas.models.media.EmailSchema +import io.swagger.v3.oas.models.media.FileSchema +import io.swagger.v3.oas.models.media.IntegerSchema +import io.swagger.v3.oas.models.media.MapSchema +import io.swagger.v3.oas.models.media.NumberSchema +import io.swagger.v3.oas.models.media.PasswordSchema import io.swagger.v3.oas.models.media.Schema -import java.math.BigDecimal - -class DecodeJsonArrayVisitor(override val from: ArrayNode) : DecodeVisitor() { - - constructor(jsonString: String) : this(mapper.readTree(jsonString) as ArrayNode) - - private val rootMessage = message() - - override fun visit(fieldName: String, defaultValue: Schema<*>?, fldStruct: Schema<*>, required: Boolean, schemaWriter: SchemaWriter) { - throw UnsupportedOperationException("Array visitor supports only collections") - } - - override fun visit(fieldName: String, defaultValue: String?, fldStruct: Schema<*>, required: Boolean) { - throw UnsupportedOperationException("Array visitor supports only collections") - } - - override fun visit(fieldName: String, defaultValue: Boolean?, fldStruct: Schema<*>, required: Boolean) { - throw UnsupportedOperationException("Array visitor supports only collections") - } - - override fun visit(fieldName: String, defaultValue: Int?, fldStruct: Schema<*>, required: Boolean) { - throw UnsupportedOperationException("Array visitor supports only collections") - } - - override fun visit(fieldName: String, defaultValue: Float?, fldStruct: Schema<*>, required: Boolean) { - throw UnsupportedOperationException("Array visitor supports only collections") - } - - override fun visit(fieldName: String, defaultValue: Double?, fldStruct: Schema<*>, required: Boolean) { - throw UnsupportedOperationException("Array visitor supports only collections") - } - - override fun visit(fieldName: String, defaultValue: Long?, fldStruct: Schema<*>, required: Boolean) { - throw UnsupportedOperationException("Array visitor supports only collections") - } - - override fun visit(fieldName: String, defaultValue: BigDecimal?, fldStruct: Schema<*>, required: Boolean) { - throw UnsupportedOperationException("Array visitor supports only collections") - } - - override fun visitBooleanCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootMessage.putListFrom(from, fieldName, defaultValue, required, JsonNode::validateAsBoolean) - - override fun visitIntegerCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootMessage.putListFrom(from, fieldName, defaultValue, required, JsonNode::validateAsInteger) - - override fun visitStringCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootMessage.putListFrom(from, fieldName, defaultValue, required, JsonNode::asText) - - override fun visitDoubleCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootMessage.putListFrom(from, fieldName, defaultValue, required, JsonNode::validateAsDouble) - - override fun visitFloatCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootMessage.putListFrom(from, fieldName, defaultValue, required, JsonNode::validateAsFloat) - - override fun visitLongCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootMessage.putListFrom(from, fieldName, defaultValue, required, JsonNode::validateAsLong) - - override fun visitBigDecimalCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootMessage.putListFrom(from, fieldName, defaultValue, required) { this.validateAsBigDecimal().toPlainString() } - - override fun visitObjectCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean, schemaWriter: SchemaWriter) { - rootMessage.addField(fieldName, from.map { - DecodeJsonObjectVisitor(it.validateAsObject()).apply { - schemaWriter.traverse(this, fldStruct.items) - }.getResult() - }) +import io.swagger.v3.oas.models.media.StringSchema +import io.swagger.v3.oas.models.media.UUIDSchema + +class DecodeJsonArrayVisitor(override val from: JsonNode, override val settings: VisitorSettings) : SchemaVisitor.DecodeVisitor() { + + constructor(jsonString: String, settings: VisitorSettings) : this(mapper.readTree(jsonString), settings) + + internal val rootMessage = message() + private val fromArray = from as ArrayNode + + override fun visit(fieldName: String, fldStruct: ArraySchema, required: Boolean, throwUndefined: Boolean) { + when (val itemSchema = settings.openAPI.getEndPoint(fldStruct.items)) { + is NumberSchema -> rootMessage.addField(fieldName, fromArray.map { it.validateAsBigDecimal() }) + is IntegerSchema -> rootMessage.addField(fieldName, fromArray.map { it.validateAsLong() }) + is BooleanSchema -> rootMessage.addField(fieldName, fromArray.map { it.validateAsBoolean() }) + is StringSchema -> rootMessage.addField(fieldName, fromArray.map { it.asText() }) + is DateSchema -> rootMessage.addField(fieldName, fromArray.map { settings.dateFormat.parse(it.asText()) }) + is DateTimeSchema -> rootMessage.addField(fieldName, fromArray.map { settings.dateTimeFormat.parse(it.asText()) }) + is BinarySchema, is ByteArraySchema, is EmailSchema, is FileSchema, is MapSchema, is PasswordSchema, is UUIDSchema -> throw UnsupportedOperationException("${itemSchema::class.simpleName} for json array isn't supported for now") + else -> rootMessage.addField(fieldName, mutableListOf().apply { + fromArray.forEach { + DecodeJsonObjectVisitor(checkNotNull(it.validateAsObject()) { " Value from list [$fieldName] must be message" }, settings).let { visitor -> + SchemaWriter(settings.openAPI).traverse(visitor, itemSchema, throwUndefined) + visitor.rootMessage.build().run(this::add) + } + } + }) + } } - override fun getUndefinedFields(fields: MutableSet): Nothing? = null + override fun visit(fieldName: String, fldStruct: BooleanSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: NumberSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: IntegerSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: StringSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: ComposedSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: Schema<*>, required: Boolean, throwUndefined: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: DateSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: DateTimeSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun getFieldNames() = throw UnsupportedOperationException("Array visitor supports only collections") override fun getResult(): Message.Builder = rootMessage - private inline fun Message.Builder.putListFrom(node: ArrayNode, name: String, defaultValue: List?, required: Boolean, extract: JsonNode.() -> T) { - if (node.isEmpty) { - when { - required -> error("$name field is required but array node was empty") - !defaultValue.isNullOrEmpty() -> this.addField(name, defaultValue) - } - } else { - this.addField(name, node.map(extract)) - } - } private companion object { - val mapper = ObjectMapper() + val mapper = ObjectMapper().apply { + nodeFactory = JsonNodeFactory.withExactBigDecimals(true) + } } + + } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/DecodeJsonObjectVisitor.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/DecodeJsonObjectVisitor.kt index 4ddffdd..2224546 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/DecodeJsonObjectVisitor.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/DecodeJsonObjectVisitor.kt @@ -17,138 +17,105 @@ package com.exactpro.th2.codec.openapi.writer.visitors.json import com.exactpro.th2.codec.openapi.utils.checkEnum -import com.exactpro.th2.codec.openapi.utils.getRequiredArray +import com.exactpro.th2.codec.openapi.utils.getEndPoint import com.exactpro.th2.codec.openapi.utils.getField +import com.exactpro.th2.codec.openapi.utils.getRequiredArray import com.exactpro.th2.codec.openapi.utils.validateAsBigDecimal import com.exactpro.th2.codec.openapi.utils.validateAsBoolean -import com.exactpro.th2.codec.openapi.utils.validateAsDouble -import com.exactpro.th2.codec.openapi.utils.validateAsFloat -import com.exactpro.th2.codec.openapi.utils.validateAsInteger import com.exactpro.th2.codec.openapi.utils.validateAsLong import com.exactpro.th2.codec.openapi.utils.validateAsObject import com.exactpro.th2.codec.openapi.writer.SchemaWriter -import com.exactpro.th2.codec.openapi.writer.visitors.SchemaVisitor.DecodeVisitor +import com.exactpro.th2.codec.openapi.writer.visitors.SchemaVisitor +import com.exactpro.th2.codec.openapi.writer.visitors.VisitorSettings import com.exactpro.th2.common.grpc.Message import com.exactpro.th2.common.message.addField import com.exactpro.th2.common.message.addFields import com.exactpro.th2.common.message.message +import com.exactpro.th2.common.message.set +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import io.swagger.v3.oas.models.media.ArraySchema +import io.swagger.v3.oas.models.media.BinarySchema +import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.ByteArraySchema +import io.swagger.v3.oas.models.media.ComposedSchema +import io.swagger.v3.oas.models.media.DateSchema +import io.swagger.v3.oas.models.media.DateTimeSchema +import io.swagger.v3.oas.models.media.EmailSchema +import io.swagger.v3.oas.models.media.FileSchema +import io.swagger.v3.oas.models.media.IntegerSchema +import io.swagger.v3.oas.models.media.MapSchema +import io.swagger.v3.oas.models.media.NumberSchema +import io.swagger.v3.oas.models.media.PasswordSchema import io.swagger.v3.oas.models.media.Schema -import java.math.BigDecimal - -class DecodeJsonObjectVisitor(override val from: ObjectNode) : DecodeVisitor() { - - private val rootMessage = message() - - constructor(jsonString: String) : this(mapper.readTree(jsonString) as ObjectNode) - - override fun visit(fieldName: String, defaultValue: Schema<*>?, fldStruct: Schema<*>, required: Boolean, schemaWriter: SchemaWriter) { - from.getField(fieldName, required)?.let { - val visitor = DecodeJsonObjectVisitor(it.validateAsObject()) - schemaWriter.traverse(visitor, fldStruct) - rootMessage.addFields(fieldName, visitor.rootMessage.build()) - } - } - - override fun visit(fieldName: String, defaultValue: String?, fldStruct: Schema<*>, required: Boolean) { - val value = from.getField(fieldName, required)?.asText() - fldStruct.checkEnum(value, fieldName) - rootMessage.addFields(fieldName, value ?: defaultValue) - } - - override fun visit(fieldName: String, defaultValue: Boolean?, fldStruct: Schema<*>, required: Boolean) { - val value = from.getField(fieldName, required)?.validateAsBoolean() - fldStruct.checkEnum(value, fieldName) - rootMessage.addFields(fieldName, value ?: defaultValue) - } - - override fun visit(fieldName: String, defaultValue: Int?, fldStruct: Schema<*>, required: Boolean) { - val value = from.getField(fieldName, required)?.validateAsInteger() - fldStruct.checkEnum(value, fieldName) - rootMessage.addFields(fieldName, value ?: defaultValue) - } - - override fun visit(fieldName: String, defaultValue: Float?, fldStruct: Schema<*>, required: Boolean) { - val value = from.getField(fieldName, required)?.validateAsFloat() - fldStruct.checkEnum(value, fieldName) - rootMessage.addFields(fieldName, value ?: defaultValue) - } - - override fun visit(fieldName: String, defaultValue: Double?, fldStruct: Schema<*>, required: Boolean) { - val value = from.getField(fieldName, required)?.validateAsDouble() - fldStruct.checkEnum(value, fieldName) - rootMessage.addFields(fieldName, value ?: defaultValue) - } - - override fun visit(fieldName: String, defaultValue: BigDecimal?, fldStruct: Schema<*>, required: Boolean) { - val value = from.getField(fieldName, required)?.validateAsBigDecimal() - fldStruct.checkEnum(value, fieldName) - rootMessage.addFields(fieldName, value?.toPlainString() ?: defaultValue) - } - - override fun visit(fieldName: String, defaultValue: Long?, fldStruct: Schema<*>, required: Boolean) { - val value = from.getField(fieldName, required)?.validateAsLong() - fldStruct.checkEnum(value, fieldName) - rootMessage.addFields(fieldName, value ?: defaultValue) - } - - override fun visitBooleanCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) { - from.getRequiredArray(fieldName, required)?.let { array -> - rootMessage.addField(fieldName, array.map { it.validateAsBoolean() }) +import io.swagger.v3.oas.models.media.StringSchema +import io.swagger.v3.oas.models.media.UUIDSchema +import java.time.OffsetDateTime + +open class DecodeJsonObjectVisitor(override val from: JsonNode, override val settings: VisitorSettings) : SchemaVisitor.DecodeVisitor() { + + constructor(jsonString: String, settings: VisitorSettings) : this(mapper.readTree(jsonString), settings) + + internal val rootMessage = message() + private val fromObject = from as ObjectNode + + override fun visit(fieldName: String, fldStruct: Schema<*>, required: Boolean, throwUndefined: Boolean) { + fromObject.getField(fieldName, required)?.let { message -> + val visitor = DecodeJsonObjectVisitor(message.validateAsObject(), settings) + val writer = SchemaWriter(settings.openAPI) + writer.traverse(visitor, fldStruct, throwUndefined) + rootMessage.addField(fieldName, visitor.rootMessage) + } ?: fldStruct.default?.let { error("Default values isn't supported for objects") } + } + + override fun visit(fieldName: String, fldStruct: ArraySchema, required: Boolean, throwUndefined: Boolean) { + val itemSchema = settings.openAPI.getEndPoint(fldStruct.items) + + fromObject.getRequiredArray(fieldName, required)?.let { arrayNode -> + when (itemSchema) { + is NumberSchema -> rootMessage[fieldName] = arrayNode.asSequence().map(JsonNode::validateAsBigDecimal).iterator() + is IntegerSchema -> rootMessage[fieldName] = arrayNode.asSequence().map(JsonNode::validateAsLong).iterator() + is BooleanSchema -> rootMessage[fieldName] = arrayNode.asSequence().map(JsonNode::validateAsBoolean).iterator() + is StringSchema -> rootMessage[fieldName] = arrayNode.asSequence().map(JsonNode::asText).iterator() + is DateSchema -> rootMessage[fieldName] = arrayNode.asSequence().map { settings.dateFormat.parse(it.asText()).toString() }.iterator() + is DateTimeSchema -> rootMessage[fieldName] = arrayNode.map { settings.dateTimeFormat.parse(it.asText()).toString() }.iterator() + is BinarySchema, is ByteArraySchema, is EmailSchema, is FileSchema, is MapSchema, is PasswordSchema, is UUIDSchema -> throw UnsupportedOperationException("${itemSchema::class.simpleName} for json array isn't supported for now") + else -> rootMessage[fieldName] = arrayNode.map { + val message = checkNotNull(it.validateAsObject()) { "'$fieldName' field element is not a message: $it" } + DecodeJsonObjectVisitor(message, settings).let { visitor -> + SchemaWriter(settings.openAPI).traverse(visitor, itemSchema, throwUndefined) + visitor.rootMessage + } + } + } + } ?: fldStruct.default?.let { error("Default values isn't supported for arrays") } + } + + override fun visit(fieldName: String, fldStruct: ComposedSchema, required: Boolean) { + fromObject.getField(fieldName, required)?.let { message -> + val visitor = DecodeJsonObjectVisitor(message.validateAsObject(), settings) + val writer = SchemaWriter(settings.openAPI) + writer.traverse(visitor, fldStruct, false) + rootMessage.addField(fieldName, visitor.rootMessage) + } ?: fldStruct.default?.let { error("Default values isn't supported for composed objects") } + } + + override fun visit(fieldName: String, fldStruct: NumberSchema, required: Boolean) = visitPrimitive(fieldName, fromObject.getField(fieldName, required)?.validateAsBigDecimal(), fldStruct) + override fun visit(fieldName: String, fldStruct: IntegerSchema, required: Boolean) = visitPrimitive(fieldName, fromObject.getField(fieldName, required)?.validateAsLong(), fldStruct) + override fun visit(fieldName: String, fldStruct: StringSchema, required: Boolean) = visitPrimitive(fieldName, fromObject.getField(fieldName, required)?.asText(), fldStruct) + override fun visit(fieldName: String, fldStruct: BooleanSchema, required: Boolean) = visitPrimitive(fieldName, fromObject.getField(fieldName, required)?.validateAsBoolean(), fldStruct) + override fun visit(fieldName: String, fldStruct: DateSchema, required: Boolean) = visitPrimitive(fieldName, fromObject.getField(fieldName, required)?.asText()?.let(settings.dateFormat::parse), fldStruct) + override fun visit(fieldName: String, fldStruct: DateTimeSchema, required: Boolean) = visitPrimitive(fieldName, fromObject.getField(fieldName, required)?.asText()?.let { settings.dateTimeFormat.parse(it) as OffsetDateTime }, fldStruct) + + private fun visitPrimitive(fieldName: String, value: T?, fldStruct: Schema) { + (value?.also { fldStruct.checkEnum(it, fieldName) } ?: fldStruct.default)?.let { + rootMessage.addFields(fieldName, it) } } - override fun visitIntegerCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) { - from.getRequiredArray(fieldName, required)?.let { array -> - rootMessage.addField(fieldName, array.map { it.validateAsInteger() }) - } - } - - override fun visitStringCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) { - from.getRequiredArray(fieldName, required)?.map { it.asText() }?.let { - rootMessage.addField(fieldName, it) - } - } - - override fun visitDoubleCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) { - from.getRequiredArray(fieldName, required)?.let { array -> - rootMessage.addField(fieldName, array.map { it.validateAsDouble() }) - } - } - - override fun visitFloatCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) { - from.getRequiredArray(fieldName, required)?.let { array -> - rootMessage.addField(fieldName, array.map { it.validateAsFloat() }) - } - } - - override fun visitLongCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) { - from.getRequiredArray(fieldName, required)?.let { array -> - rootMessage.addField(fieldName, array.map { it.validateAsLong() }) - } - } - - override fun visitBigDecimalCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) { - from.getRequiredArray(fieldName, required)?.let { array -> - rootMessage.addField(fieldName, array.map { it.validateAsBigDecimal().toPlainString() }) - } - } - - override fun visitObjectCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean, schemaWriter: SchemaWriter) { - from.getRequiredArray(fieldName, required)?.let { array -> - rootMessage.addField(fieldName, array.map { - DecodeJsonObjectVisitor(it.validateAsObject()).apply { - schemaWriter.traverse(this, fldStruct.items) - }.getResult() - }) - } - } - - override fun getUndefinedFields(fields: MutableSet): Set = from.fieldNames().asSequence().filterNot { it in fields }.toSet() - + override fun getFieldNames(): Collection = fromObject.fieldNames().asSequence().toList() override fun getResult(): Message.Builder = rootMessage private companion object { @@ -156,4 +123,5 @@ class DecodeJsonObjectVisitor(override val from: ObjectNode) : DecodeVisitor() { - - private val rootNode: ArrayNode = mapper.createArrayNode() - - companion object { - private var mapper = ObjectMapper() - } - - override fun visit(fieldName: String, defaultValue: Schema<*>?, fldStruct: Schema<*>, required: Boolean, schemaWriter: SchemaWriter) = throw UnsupportedOperationException("Array visitor supports only collections") - - override fun visit(fieldName: String, defaultValue: String?, fldStruct: Schema<*>, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") - - override fun visit(fieldName: String, defaultValue: Boolean?, fldStruct: Schema<*>, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") - - override fun visit(fieldName: String, defaultValue: Int?, fldStruct: Schema<*>, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") - - override fun visit(fieldName: String, defaultValue: Float?, fldStruct: Schema<*>, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") - - override fun visit(fieldName: String, defaultValue: Double?, fldStruct: Schema<*>, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") - - override fun visit(fieldName: String, defaultValue: BigDecimal?, fldStruct: Schema<*>, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") - - override fun visit(fieldName: String, defaultValue: Long?, fldStruct: Schema<*>, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") - - override fun visitBooleanCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitIntegerCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitStringCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitDoubleCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitFloatCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitLongCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitBigDecimalCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitObjectCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean, schemaWriter: SchemaWriter) { - from.getField(fieldName, required)?.getList()?.map { - if (!it.hasMessageValue()) error("Cannot convert $fieldName=${it.toJson(true)} to json object") - EncodeJsonObjectVisitor(it.messageValue).apply { - schemaWriter.traverse(this, fldStruct.items) - }.getNode() - }?.forEach(rootNode::add) - } - - override fun getUndefinedFields(fields: MutableSet): Nothing? = null - - override fun getResult(): ByteString { - return if (rootNode.isEmpty) { - ByteString.EMPTY - } else { - ByteString.copyFrom(rootNode.toString().toByteArray()) - } - } - - private inline fun ArrayNode.putListFrom(message: Message, name: String, defaultValue: List?, required: Boolean) { - message.getField(name, required)?.getList()?.let { this.putAll(it) } ?: defaultValue?.let(::putAll) - } +import io.swagger.v3.oas.models.media.StringSchema + +class EncodeJsonArrayVisitor(from: Message, visitorSettings: VisitorSettings) : EncodeJsonObjectVisitor(from, visitorSettings) { + override fun visit(fieldName: String, fldStruct: BooleanSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: NumberSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: IntegerSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: StringSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: ComposedSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: Schema<*>, required: Boolean, throwUndefined: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: DateSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + override fun visit(fieldName: String, fldStruct: DateTimeSchema, required: Boolean) = throw UnsupportedOperationException("Array visitor supports only collections") + + override fun getFieldNames() = throw UnsupportedOperationException("Array visitor supports only collections") + override fun getResult(): ByteString = ByteString.copyFrom(rootNode.first().toString().toByteArray()) } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/EncodeJsonObjectVisitor.kt b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/EncodeJsonObjectVisitor.kt index 205395d..87c7021 100644 --- a/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/EncodeJsonObjectVisitor.kt +++ b/src/main/kotlin/com/exactpro/th2/codec/openapi/writer/visitors/json/EncodeJsonObjectVisitor.kt @@ -17,16 +17,17 @@ package com.exactpro.th2.codec.openapi.writer.visitors.json import com.exactpro.th2.codec.openapi.utils.checkEnum +import com.exactpro.th2.codec.openapi.utils.getBoolean +import com.exactpro.th2.codec.openapi.utils.getEndPoint import com.exactpro.th2.codec.openapi.utils.getField import com.exactpro.th2.codec.openapi.utils.putAll import com.exactpro.th2.codec.openapi.writer.SchemaWriter -import com.exactpro.th2.codec.openapi.writer.visitors.SchemaVisitor.EncodeVisitor +import com.exactpro.th2.codec.openapi.writer.visitors.SchemaVisitor +import com.exactpro.th2.codec.openapi.writer.visitors.VisitorSettings import com.exactpro.th2.common.grpc.Message -import com.exactpro.th2.common.message.toJson +import com.exactpro.th2.common.grpc.Value +import com.exactpro.th2.common.grpc.Value.KindCase.NULL_VALUE import com.exactpro.th2.common.value.getBigDecimal -import com.exactpro.th2.common.value.getDouble -import com.exactpro.th2.common.value.getInt -import com.exactpro.th2.common.value.getList import com.exactpro.th2.common.value.getLong import com.exactpro.th2.common.value.getMessage import com.exactpro.th2.common.value.getString @@ -35,126 +36,140 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.google.protobuf.ByteString import io.swagger.v3.oas.models.media.ArraySchema +import io.swagger.v3.oas.models.media.BinarySchema +import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.ByteArraySchema +import io.swagger.v3.oas.models.media.ComposedSchema +import io.swagger.v3.oas.models.media.DateSchema +import io.swagger.v3.oas.models.media.DateTimeSchema +import io.swagger.v3.oas.models.media.EmailSchema +import io.swagger.v3.oas.models.media.FileSchema +import io.swagger.v3.oas.models.media.IntegerSchema +import io.swagger.v3.oas.models.media.MapSchema +import io.swagger.v3.oas.models.media.NumberSchema +import io.swagger.v3.oas.models.media.PasswordSchema import io.swagger.v3.oas.models.media.Schema +import io.swagger.v3.oas.models.media.StringSchema +import io.swagger.v3.oas.models.media.UUIDSchema import java.math.BigDecimal - - -class EncodeJsonObjectVisitor(override val from: Message) : EncodeVisitor() { - - private val rootNode: ObjectNode = mapper.createObjectNode() - - override fun visit(fieldName: String, defaultValue: Schema<*>?, fldStruct: Schema<*>, required: Boolean, schemaWriter: SchemaWriter) { - from.getField(fieldName, required)?.getMessage()?.let { nextMessage -> - val visitor = EncodeJsonObjectVisitor(nextMessage) - schemaWriter.traverse(visitor, fldStruct) +import java.time.OffsetDateTime + +open class EncodeJsonObjectVisitor(override val from: Message, override val settings: VisitorSettings) : SchemaVisitor.EncodeVisitor() { + internal val rootNode: ObjectNode = mapper.createObjectNode() + + override fun visit(fieldName: String, fldStruct: Schema<*>, required: Boolean, throwUndefined: Boolean) { + from.getField(fieldName, required)?.let { field -> + if (field.kindCase == NULL_VALUE) { + rootNode.putObject(fieldName) + return + } + check(field.hasMessageValue()) { "$fieldName is not an message: ${field.kindCase}" } + + val visitor = EncodeJsonObjectVisitor(field.messageValue, settings) + val writer = SchemaWriter(settings.openAPI) + writer.traverse(visitor, fldStruct, throwUndefined) rootNode.set(fieldName, visitor.rootNode) - } + } ?: fldStruct.default?.let { error("Default values isn't supported for objects") } } - override fun visit(fieldName: String, defaultValue: String?, fldStruct: Schema<*>, required: Boolean) { - from.getField(fieldName, required)?.getString()?.let { value -> - fldStruct.checkEnum(value, fieldName) - rootNode.put(fieldName, value) - } ?: defaultValue?.let { - rootNode.put(fieldName, defaultValue) - } + override fun visit(fieldName: String, fldStruct: ArraySchema, required: Boolean, throwUndefined: Boolean) { + val itemSchema = settings.openAPI.getEndPoint(fldStruct.items) + + from.getField(fieldName, required)?.let { field -> + if (field.kindCase == NULL_VALUE) { + rootNode.putArray(fieldName) + return + } + check(field.hasListValue()) { "$fieldName is not an list: ${field.kindCase}" } + + val listOfValues = field.listValue.valuesList + + when (itemSchema) { + is NumberSchema -> rootNode.putArray(fieldName).putAll(listOfValues) + is IntegerSchema -> rootNode.putArray(fieldName).putAll(listOfValues) + is BooleanSchema -> rootNode.putArray(fieldName).putAll(listOfValues) + is StringSchema -> rootNode.putArray(fieldName).putAll(listOfValues) + is DateSchema -> rootNode.putArray(fieldName).run { + listOfValues.forEach { add(settings.dateFormat.parse(it.simpleValue).toString()) } + } + is DateTimeSchema -> rootNode.putArray(fieldName).run { + listOfValues.forEach { add(settings.dateTimeFormat.parse(it.simpleValue).toString()) } + } + is BinarySchema, is ByteArraySchema, is EmailSchema, is FileSchema, is MapSchema, is PasswordSchema, is UUIDSchema -> throw UnsupportedOperationException("${itemSchema::class.simpleName} for json array isn't supported for now") + else -> rootNode.putArray(fieldName).apply { + val writer = SchemaWriter(settings.openAPI) + listOfValues.forEach { + EncodeJsonObjectVisitor(checkNotNull(it.getMessage()) { " Value from list [$fieldName] must be message" }, settings).let { visitor -> + writer.traverse(visitor, itemSchema, throwUndefined) + add(visitor.rootNode) + } + } + } + } + } ?: fldStruct.default?.let { error("Default values isn't supported for arrays") } } - override fun visit(fieldName: String, defaultValue: Boolean?, fldStruct: Schema<*>, required: Boolean) { - from.getField(fieldName, required)?.getString()?.toBoolean()?.let { value -> - fldStruct.checkEnum(value, fieldName) - rootNode.put(fieldName, value) - } ?: defaultValue?.let { - rootNode.put(fieldName, defaultValue) - } + override fun visit(fieldName: String, fldStruct: ComposedSchema, required: Boolean) { + from.getField(fieldName, required)?.let { field -> + when { + field.kindCase.number == 1 -> return + !field.hasMessageValue() -> error("$fieldName is not an message: ${field.kindCase}") + } + val message = field.getMessage()!! + + val visitor = EncodeJsonObjectVisitor(message, settings) + val writer = SchemaWriter(settings.openAPI) + writer.traverse(visitor, fldStruct, false) + rootNode.set(fieldName, visitor.rootNode) + } ?: fldStruct.default?.let { error("Default values isn't supported for composed objects") } } - override fun visit(fieldName: String, defaultValue: Int?, fldStruct: Schema<*>, required: Boolean) { - from.getField(fieldName, required)?.getInt()?.let { value -> - fldStruct.checkEnum(value, fieldName) - rootNode.put(fieldName, value) - } ?: defaultValue?.let { - rootNode.put(fieldName, defaultValue) - } + override fun visit(fieldName: String, fldStruct: NumberSchema, required: Boolean) = visitPrimitive(fieldName, from.getField(fieldName, required), fldStruct, Value::getBigDecimal) { + rootNode.put(fieldName, it) } - override fun visit(fieldName: String, defaultValue: Float?, fldStruct: Schema<*>, required: Boolean) { - from.getField(fieldName, required)?.getString()?.toFloat()?.let { value -> - fldStruct.checkEnum(value, fieldName) - rootNode.put(fieldName, value) - } ?: defaultValue?.let { - rootNode.put(fieldName, defaultValue) - } + override fun visit(fieldName: String, fldStruct: IntegerSchema, required: Boolean) = visitPrimitive(fieldName, from.getField(fieldName, required), fldStruct, Value::getLong) { + rootNode.put(fieldName, it.toLong()) } - override fun visit(fieldName: String, defaultValue: Double?, fldStruct: Schema<*>, required: Boolean) { - from.getField(fieldName, required)?.getDouble()?.let { value -> - fldStruct.checkEnum(value, fieldName) - rootNode.put(fieldName, value) - } ?: defaultValue?.let { - rootNode.put(fieldName, defaultValue) - } + override fun visit(fieldName: String, fldStruct: StringSchema, required: Boolean) = visitPrimitive(fieldName, from.getField(fieldName, required), fldStruct, Value::getString) { + rootNode.put(fieldName, it) } - override fun visit(fieldName: String, defaultValue: Long?, fldStruct: Schema<*>, required: Boolean) { - from.getField(fieldName, required)?.getLong()?.let { value -> - fldStruct.checkEnum(value, fieldName) - rootNode.put(fieldName, value) - } ?: defaultValue?.let { - rootNode.put(fieldName, defaultValue) - } + override fun visit(fieldName: String, fldStruct: BooleanSchema, required: Boolean) = visitPrimitive(fieldName, from.getField(fieldName, required), fldStruct, Value::getBoolean) { + rootNode.put(fieldName, it) } - override fun visit(fieldName: String, defaultValue: BigDecimal?, fldStruct: Schema<*>, required: Boolean) { - from.getField(fieldName, required)?.getBigDecimal()?.let { value -> - fldStruct.checkEnum(value, fieldName) - rootNode.put(fieldName, value) - } ?: defaultValue?.let { - rootNode.put(fieldName, defaultValue) - } + override fun visit(fieldName: String, fldStruct: DateSchema, required: Boolean) = visitPrimitive(fieldName, from.getField(fieldName, required), fldStruct, { value -> settings.dateFormat.parse(value.simpleValue) }) { + rootNode.put(fieldName, it.toString()) } - override fun visitBooleanCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitIntegerCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitStringCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitDoubleCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitFloatCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitLongCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitBigDecimalCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean) = rootNode.putListFrom(from, fieldName, defaultValue, required) - - override fun visitObjectCollection(fieldName: String, defaultValue: List?, fldStruct: ArraySchema, required: Boolean, schemaWriter: SchemaWriter) { - from.getField(fieldName, required)?.getList()?.map { - if (!it.hasMessageValue()) error("Cannot convert $fieldName=${it.toJson(true)} to json object") - EncodeJsonObjectVisitor(it.messageValue).apply { - schemaWriter.traverse(this, fldStruct.items) - }.getNode() - }?.run(rootNode.putArray(fieldName)::addAll) + override fun visit(fieldName: String, fldStruct: DateTimeSchema, required: Boolean) = visitPrimitive(fieldName, from.getField(fieldName, required), fldStruct, { value -> settings.dateTimeFormat.parse(value.simpleValue) as OffsetDateTime }) { + rootNode.put(fieldName, it.toString()) } - override fun getUndefinedFields(fields: MutableSet): Set = this.from.fieldsMap.keys - fields + + override fun getFieldNames(): Collection = from.fieldsMap.keys override fun getResult(): ByteString = ByteString.copyFrom(rootNode.toString().toByteArray()) - fun getNode() = rootNode - private inline fun ObjectNode.putListFrom(message: Message, name: String, defaultValue: List?, required: Boolean) { - message.getField(name, required)?.getList()?.let { values -> - this.putArray(name).putAll(values) - } ?: defaultValue?.let { list -> - this.putArray(name).putAll(list) - } + private inline fun visitPrimitive(fieldName: String, fieldValue: Value?, fldStruct: Schema, convert: (Value) -> T, put: (T) -> Unit) { + fieldValue?.let { value -> + if (value.kindCase.number == 1) { + return + } + val converted = checkNotNull(convert(value)) { "Cannot convert field $fieldName to ${T::class.simpleName}" } + fldStruct.checkEnum(converted, fieldName) + put(converted) + } ?: fldStruct.default?.run(put) } + private companion object { val mapper = ObjectMapper().apply { nodeFactory = JsonNodeFactory.withExactBigDecimals(true) } } -} +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/EmptyBodyTests.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/EmptyBodyTests.kt index 5f700e8..12ab650 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/EmptyBodyTests.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/EmptyBodyTests.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.exactpro.th2.codec.openapi import com.exactpro.th2.codec.openapi.utils.getResourceAsText diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/ParametersTests.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/ParametersTests.kt index 2ecfb3d..379c697 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/ParametersTests.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/ParametersTests.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.exactpro.th2.codec.openapi import com.exactpro.th2.codec.openapi.OpenApiCodec.Companion.HEADERS_FIELD diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/ValidationDictionaryTests.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/ValidationDictionaryTests.kt index 2593112..3b1dea0 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/ValidationDictionaryTests.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/ValidationDictionaryTests.kt @@ -1,4 +1,4 @@ -package com.exactpro.th2.codec.openapi/* +/* * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ package com.exactpro.th2.codec.openapi/* * limitations under the License. */ +package com.exactpro.th2.codec.openapi + import com.exactpro.th2.codec.api.DictionaryAlias import com.exactpro.th2.codec.api.IPipelineCodecContext import com.exactpro.th2.codec.openapi.throwable.DictionaryValidationException @@ -49,11 +51,9 @@ class ValidationDictionaryTests { if (dictionary.name.endsWith(YAML_FORMAT) || dictionary.name.endsWith(JSON_FORMAT)) { val factory = OpenApiCodecFactory().apply { init(object : IPipelineCodecContext { - override fun get(alias: DictionaryAlias): InputStream = TODO("Not yet implemented") - + override fun get(alias: DictionaryAlias): InputStream = error("") override fun get(type: DictionaryType): InputStream = dictionary.inputStream() - - override fun getDictionaryAliases(): Set = TODO("Not yet implemented") + override fun getDictionaryAliases(): Set = error("") }) } diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/decode/JsonArrayDecodeTests.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/decode/JsonArrayDecodeTests.kt index 4551695..86423c7 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/decode/JsonArrayDecodeTests.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/decode/JsonArrayDecodeTests.kt @@ -18,7 +18,7 @@ package com.exactpro.th2.codec.openapi.json.decode import com.exactpro.th2.codec.openapi.OpenApiCodec import com.exactpro.th2.codec.openapi.OpenApiCodecSettings -import com.exactpro.th2.codec.openapi.writer.SchemaWriter.Companion.ARRAY_TYPE +import com.exactpro.th2.codec.openapi.utils.ARRAY_TYPE import com.exactpro.th2.common.assertList import com.exactpro.th2.common.value.toValue import com.exactpro.th2.codec.openapi.utils.getResourceAsText diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/decode/JsonObjectDecodeTests.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/decode/JsonObjectDecodeTests.kt index fd227a7..5e3a599 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/decode/JsonObjectDecodeTests.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/decode/JsonObjectDecodeTests.kt @@ -18,6 +18,7 @@ package com.exactpro.th2.codec.openapi.json.decode import com.exactpro.th2.codec.openapi.OpenApiCodec import com.exactpro.th2.codec.openapi.OpenApiCodecSettings +import com.exactpro.th2.codec.openapi.throwable.DecodeException import com.exactpro.th2.common.message.getString import com.exactpro.th2.codec.openapi.utils.getResourceAsText import io.swagger.parser.OpenAPIParser @@ -25,6 +26,7 @@ import io.swagger.v3.oas.models.OpenAPI import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import com.exactpro.th2.codec.openapi.utils.testDecode +import com.exactpro.th2.common.message.getInt import com.exactpro.th2.common.message.messageType class JsonObjectDecodeTests { @@ -73,6 +75,53 @@ class JsonObjectDecodeTests { Assertions.assertEquals("FAILED", decodedResult.getString("testStatus")) } + @Test + fun `json decode response oneOf`() { + val codec = OpenApiCodec(openAPI, settings) + val firstOneOf = codec.testDecode( + "/test", + "GET", + "300", + "application/json", + """{ + "publicKey" : "1234567", + "testEnabled" : true, + "testStatus" : "FAILED" + }""".trimIndent())!! + + Assertions.assertEquals("TestGet300ApplicationJson", firstOneOf.messageType) + Assertions.assertEquals(3, firstOneOf.fieldsMap.size) + Assertions.assertEquals("1234567", firstOneOf.getString("publicKey")) + Assertions.assertEquals(true, firstOneOf.getString("testEnabled").toBoolean()) + Assertions.assertEquals("FAILED", firstOneOf.getString("testStatus")) + + val secondOneOf = codec.testDecode( + "/test", + "get", + "300", + "application/json", + """{ + "oneOfInteger" : 1234567, + "oneOfEnabled" : true + }""".trimIndent())!! + + Assertions.assertEquals("TestGet300ApplicationJson", secondOneOf.messageType) + Assertions.assertEquals(2, secondOneOf.fieldsMap.size) + Assertions.assertEquals(1234567, secondOneOf.getInt("oneOfInteger")) + Assertions.assertEquals(true, secondOneOf.getString("oneOfEnabled").toBoolean()) + + Assertions.assertThrows(DecodeException::class.java) { + codec.testDecode( + "/test", + "get", + "300", + "application/json", + """{ + "onlyWrongOne" : 1234567, + }""".trimIndent())!! + } + } + private companion object { val settings = OpenApiCodecSettings() val openAPI: OpenAPI = OpenAPIParser().readContents(getResourceAsText("dictionaries/valid/object-json-tests.yml"), null, settings.dictionaryParseOption).openAPI diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/encode/JsonObjectEncodeTests.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/encode/JsonObjectEncodeTests.kt index 84fb222..f29e279 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/encode/JsonObjectEncodeTests.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/encode/JsonObjectEncodeTests.kt @@ -18,6 +18,7 @@ package com.exactpro.th2.codec.openapi.json.encode import com.exactpro.th2.codec.openapi.OpenApiCodec import com.exactpro.th2.codec.openapi.OpenApiCodecSettings +import com.exactpro.th2.codec.openapi.throwable.EncodeException import com.exactpro.th2.common.message.addField import com.fasterxml.jackson.databind.ObjectMapper import com.exactpro.th2.codec.openapi.utils.getResourceAsText @@ -62,6 +63,41 @@ class JsonObjectEncodeTests { } } + @Test + fun `json encode response oneOf`() { + val codec = OpenApiCodec(openAPI, settings) + val firstOneOf = codec.testEncode("/test", "get", "300", "application/json", "json") { + addField("publicKey", "1234567") + addField("testEnabled", true) + addField("testStatus", "FAILED") + } + + mapper.readTree(firstOneOf!!.body.toStringUtf8()).let { json -> + Assertions.assertEquals(3, json.size()) + Assertions.assertEquals("1234567", json.get("publicKey").asText()) + Assertions.assertTrue(json.get("testEnabled").asBoolean()) + Assertions.assertEquals("FAILED", json.get("testStatus").asText()) + Assertions.assertEquals(null, json.get("nullField")?.asText()) + } + + val secondOneOf = codec.testEncode("/test", "get", "300", "application/json", "json") { + addField("oneOfInteger", "1234567") + addField("oneOfEnabled", false) + } + + mapper.readTree(secondOneOf!!.body.toStringUtf8()).let { json -> + Assertions.assertEquals(2, json.size()) + Assertions.assertEquals("1234567", json.get("oneOfInteger").asText()) + Assertions.assertFalse(json.get("oneOfEnabled").asBoolean()) + } + + Assertions.assertThrows(EncodeException::class.java) { + codec.testEncode("/test", "get", "300", "application/json", "json") { + addField("onlyWrongOne", "1234567") + } + } + } + private companion object { val settings = OpenApiCodecSettings() val openAPI: OpenAPI = OpenAPIParser().readContents(getResourceAsText("dictionaries/valid/valid-dictionary.yml"), null, settings.dictionaryParseOption).openAPI diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/decode/JsonArrayTest.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/decode/JsonArrayTest.kt index 6b69b3a..b6f2410 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/decode/JsonArrayTest.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/decode/JsonArrayTest.kt @@ -17,7 +17,6 @@ package com.exactpro.th2.codec.openapi.json.visitor.decode import com.exactpro.th2.codec.openapi.OpenApiCodecSettings -import com.exactpro.th2.codec.openapi.writer.SchemaWriter import com.exactpro.th2.codec.openapi.writer.visitors.json.DecodeJsonArrayVisitor import com.exactpro.th2.common.assertInt import com.exactpro.th2.common.assertList @@ -28,16 +27,21 @@ import com.exactpro.th2.common.value.getList import com.exactpro.th2.common.value.toValue import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode -import com.exactpro.th2.codec.openapi.createArrayTestSchema +import com.exactpro.th2.codec.openapi.utils.createArrayTestSchema import com.exactpro.th2.codec.openapi.utils.getResourceAsText +import com.exactpro.th2.codec.openapi.writer.visitors.VisitorSettings import io.swagger.parser.OpenAPIParser import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.media.ArraySchema -import io.swagger.v3.oas.models.media.Schema +import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.IntegerSchema +import io.swagger.v3.oas.models.media.NumberSchema +import io.swagger.v3.oas.models.media.ObjectSchema import io.swagger.v3.oas.models.media.StringSchema import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -import java.math.BigDecimal +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter class JsonArrayTest { @@ -45,37 +49,25 @@ class JsonArrayTest { @Test fun `not supported decode`() { val node = mapper.createArrayNode() - val visitor = DecodeJsonArrayVisitor(node) + val visitor = DecodeJsonArrayVisitor(node, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? String, StringSchema(), true) + visitor.visit("", StringSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Int, StringSchema(), true) + visitor.visit("", IntegerSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Double, StringSchema(), true) + visitor.visit("", NumberSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Long, StringSchema(), true) + visitor.visit("", BooleanSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Float, StringSchema(), true) - } - - Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Boolean, StringSchema(), true) - } - - Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Schema<*>, StringSchema(), true, SchemaWriter(openAPI)) - } - - Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? BigDecimal, StringSchema(), true) + visitor.visit("", ObjectSchema(), true) } } @@ -123,8 +115,8 @@ class JsonArrayTest { }) } - val result = DecodeJsonArrayVisitor(jsonArrayNode).apply { - visitObjectCollection(fieldName, null, openAPI.components.schemas["ArrayObjectTest"]!! as ArraySchema, true, SchemaWriter(openAPI)) + val result = DecodeJsonArrayVisitor(jsonArrayNode, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, openAPI.components.schemas["ArrayObjectTest"]!! as ArraySchema, true) }.getResult() (result[fieldName]!!).let { listValue -> @@ -150,8 +142,8 @@ class JsonArrayTest { val jsonArrayNode = mapper.createArrayNode().apply { collection.forEach(this::add) } - val visitor = DecodeJsonArrayVisitor(jsonArrayNode) - visitor.visitStringCollection(fieldName, null, createArrayTestSchema("string"), true) + val visitor = DecodeJsonArrayVisitor(jsonArrayNode, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) + visitor.visit(fieldName, createArrayTestSchema("string"), true) visitor.getResult().build().assertList(fieldName, collection.map {it.toValue()}) } @@ -162,8 +154,8 @@ class JsonArrayTest { val jsonArrayNode = mapper.createArrayNode().apply { collection.forEach(this::add) } - val visitor = DecodeJsonArrayVisitor(jsonArrayNode) - visitor.visitBooleanCollection(fieldName, null, createArrayTestSchema("boolean"), true) + val visitor = DecodeJsonArrayVisitor(jsonArrayNode, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) + visitor.visit(fieldName, createArrayTestSchema("boolean"), true) visitor.getResult().build().assertList(fieldName, collection.map {it.toValue()}) } @@ -174,8 +166,8 @@ class JsonArrayTest { val jsonArrayNode = mapper.createArrayNode().apply { collection.forEach(this::add) } - val visitor = DecodeJsonArrayVisitor(jsonArrayNode) - visitor.visitIntegerCollection(fieldName, null, createArrayTestSchema("integer"), true) + val visitor = DecodeJsonArrayVisitor(jsonArrayNode, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) + visitor.visit(fieldName, createArrayTestSchema("integer"), true) visitor.getResult().build().assertList(fieldName, collection.map {it.toValue()}) } @@ -186,8 +178,8 @@ class JsonArrayTest { val jsonArrayNode = mapper.createArrayNode().apply { collection.forEach(this::add) } - val visitor = DecodeJsonArrayVisitor(jsonArrayNode) - visitor.visitDoubleCollection(fieldName, null, createArrayTestSchema("number", "double"), true) + val visitor = DecodeJsonArrayVisitor(jsonArrayNode, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) + visitor.visit(fieldName, createArrayTestSchema("number", "double"), true) visitor.getResult().build().assertList(fieldName, collection.map {it.toValue()}) } @@ -198,8 +190,8 @@ class JsonArrayTest { val jsonArrayNode = mapper.createArrayNode().apply { collection.forEach(this::add) } - val visitor = DecodeJsonArrayVisitor(jsonArrayNode) - visitor.visitFloatCollection(fieldName, null, createArrayTestSchema("number", "float"), true) + val visitor = DecodeJsonArrayVisitor(jsonArrayNode, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) + visitor.visit(fieldName, createArrayTestSchema("number", "float"), true) visitor.getResult().build().assertList(fieldName, collection.map {it.toValue()}) } @@ -210,8 +202,8 @@ class JsonArrayTest { val jsonArrayNode = mapper.createArrayNode().apply { collection.forEach(this::add) } - val visitor = DecodeJsonArrayVisitor(jsonArrayNode) - visitor.visitLongCollection(fieldName, null, createArrayTestSchema("integer", "int64"), true) + val visitor = DecodeJsonArrayVisitor(jsonArrayNode, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) + visitor.visit(fieldName, createArrayTestSchema("integer", "int64"), true) visitor.getResult().build().assertList(fieldName, collection.map {it.toValue()}) } @@ -222,8 +214,8 @@ class JsonArrayTest { val jsonArrayNode = mapper.createArrayNode().apply { collection.forEach(this::add) } - val visitor = DecodeJsonArrayVisitor(jsonArrayNode) - visitor.visitBigDecimalCollection(fieldName, null, createArrayTestSchema("integer", "int64"), true) + val visitor = DecodeJsonArrayVisitor(jsonArrayNode, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) + visitor.visit(fieldName, createArrayTestSchema("integer", "int64"), true) visitor.getResult().build().assertList(fieldName, collection.map {it.toValue()}) } diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/decode/JsonObjectTest.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/decode/JsonObjectTest.kt index 744bde5..d6923e8 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/decode/JsonObjectTest.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/decode/JsonObjectTest.kt @@ -17,7 +17,6 @@ package com.exactpro.th2.codec.openapi.json.visitor.decode import com.exactpro.th2.codec.openapi.OpenApiCodecSettings -import com.exactpro.th2.codec.openapi.writer.SchemaWriter import com.exactpro.th2.codec.openapi.writer.visitors.json.DecodeJsonObjectVisitor import com.exactpro.th2.common.assertDouble import com.exactpro.th2.common.assertInt @@ -29,15 +28,21 @@ import com.exactpro.th2.common.value.getList import com.exactpro.th2.common.value.toValue import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode -import com.exactpro.th2.codec.openapi.createArrayTestSchema -import com.exactpro.th2.codec.openapi.createTestSchema +import com.exactpro.th2.codec.openapi.utils.createArrayTestSchema +import com.exactpro.th2.codec.openapi.utils.createTestSchema import com.exactpro.th2.codec.openapi.utils.getResourceAsText +import com.exactpro.th2.codec.openapi.writer.visitors.VisitorSettings import io.swagger.parser.OpenAPIParser import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.media.ArraySchema -import io.swagger.v3.oas.models.media.Schema +import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.IntegerSchema +import io.swagger.v3.oas.models.media.NumberSchema +import io.swagger.v3.oas.models.media.ObjectSchema +import io.swagger.v3.oas.models.media.StringSchema import org.junit.jupiter.api.Test -import java.math.BigDecimal +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter @Suppress("CAST_NEVER_SUCCEEDS") class JsonObjectTest { @@ -73,8 +78,8 @@ class JsonObjectTest { val json = mapper.createObjectNode().apply { this.set(fieldName, objectValue) } - val result = DecodeJsonObjectVisitor(json).apply { - visit(fieldName, null as? Schema<*>, openAPI.components.schemas["ObjectTest"]!!, true, SchemaWriter(openAPI)) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, openAPI.components.schemas["ObjectTest"]!! as ObjectSchema, true) }.getResult() result[fieldName]!!.messageValue.let { bigMessage -> bigMessage.assertString(stringName, stringValue) @@ -93,8 +98,8 @@ class JsonObjectTest { val json = mapper.createObjectNode().apply { put(fieldName, simpleValue) } - val result = DecodeJsonObjectVisitor(json).apply { - visit(fieldName, null as? String, createTestSchema(simpleValue), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createTestSchema(simpleValue) as StringSchema, true) }.getResult() result.build().assertString(fieldName, simpleValue) } @@ -106,8 +111,8 @@ class JsonObjectTest { val json = mapper.createObjectNode().apply { put(fieldName, simpleValue) } - val result = DecodeJsonObjectVisitor(json).apply { - visit(fieldName, null as? Boolean, createTestSchema(simpleValue), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createTestSchema(simpleValue) as BooleanSchema, true) }.getResult() result.build().assertString(fieldName, simpleValue.toString()) } @@ -119,8 +124,8 @@ class JsonObjectTest { val json = mapper.createObjectNode().apply { put(fieldName, simpleValue) } - val result = DecodeJsonObjectVisitor(json).apply { - visit(fieldName, null as? Int, createTestSchema(simpleValue), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createTestSchema(simpleValue) as IntegerSchema, true) }.getResult() result.build().assertInt(fieldName, simpleValue) } @@ -132,8 +137,8 @@ class JsonObjectTest { val json = mapper.createObjectNode().apply { put(fieldName, simpleValue) } - val result = DecodeJsonObjectVisitor(json).apply { - visit(fieldName, null as? Double, createTestSchema(simpleValue), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createTestSchema(simpleValue) as NumberSchema, true) }.getResult() result.build().assertDouble(fieldName, simpleValue) } @@ -145,8 +150,8 @@ class JsonObjectTest { val json = mapper.createObjectNode().apply { put(fieldName, simpleValue) } - val result = DecodeJsonObjectVisitor(json).apply { - visit(fieldName, null as? Float, createTestSchema(simpleValue), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createTestSchema(simpleValue) as NumberSchema, true) }.getResult() result.build().assertString(fieldName, simpleValue.toString()) } @@ -158,8 +163,8 @@ class JsonObjectTest { val json = mapper.createObjectNode().apply { put(fieldName, simpleValue) } - val result = DecodeJsonObjectVisitor(json).apply { - visit(fieldName, null as? Long, createTestSchema(simpleValue), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createTestSchema(simpleValue) as IntegerSchema, true) }.getResult() result.build().assertString(fieldName, simpleValue.toString()) } @@ -171,8 +176,8 @@ class JsonObjectTest { val json = mapper.createObjectNode().apply { put(fieldName, simpleValue) } - val result = DecodeJsonObjectVisitor(json).apply { - visit(fieldName, null as? BigDecimal, createTestSchema(simpleValue), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createTestSchema(simpleValue) as IntegerSchema, true) }.getResult() result.build().assertString(fieldName, simpleValue.toString()) } @@ -185,8 +190,8 @@ class JsonObjectTest { val arrayNode = putArray(fieldName) collection.forEach(arrayNode::add) } - val result = DecodeJsonObjectVisitor(json).apply { - visitStringCollection(fieldName, null, createArrayTestSchema("string"), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createArrayTestSchema("string"), true) }.getResult() result.build().assertList(fieldName, collection.map { it.toValue() }) } @@ -199,8 +204,8 @@ class JsonObjectTest { val arrayNode = putArray(fieldName) collection.forEach(arrayNode::add) } - val result = DecodeJsonObjectVisitor(json).apply { - visitBooleanCollection(fieldName, null, createArrayTestSchema("string"), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createArrayTestSchema("string"), true) }.getResult() result.build().assertList(fieldName, collection.map { it.toValue() }) } @@ -213,8 +218,8 @@ class JsonObjectTest { val arrayNode = putArray(fieldName) collection.forEach(arrayNode::add) } - val result = DecodeJsonObjectVisitor(json).apply { - visitIntegerCollection(fieldName, null, createArrayTestSchema("integer"), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createArrayTestSchema("integer"), true) }.getResult() result.build().assertList(fieldName, collection.map { it.toValue() }) } @@ -227,8 +232,8 @@ class JsonObjectTest { val arrayNode = putArray(fieldName) collection.forEach(arrayNode::add) } - val result = DecodeJsonObjectVisitor(json).apply { - visitDoubleCollection(fieldName, null, createArrayTestSchema("string"), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createArrayTestSchema("string"), true) }.getResult() result.build().assertList(fieldName, collection.map { it.toValue() }) } @@ -241,8 +246,8 @@ class JsonObjectTest { val arrayNode = putArray(fieldName) collection.forEach(arrayNode::add) } - val result = DecodeJsonObjectVisitor(json).apply { - visitFloatCollection(fieldName, null, createArrayTestSchema("string"), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createArrayTestSchema("string"), true) }.getResult() result.build().assertList(fieldName, collection.map { it.toValue() }) } @@ -255,8 +260,8 @@ class JsonObjectTest { val arrayNode = putArray(fieldName) collection.forEach(arrayNode::add) } - val result = DecodeJsonObjectVisitor(json).apply { - visitLongCollection(fieldName, null, createArrayTestSchema("string"), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createArrayTestSchema("string"), true) }.getResult() result.build().assertList(fieldName, collection.map { it.toValue() }) } @@ -269,8 +274,8 @@ class JsonObjectTest { val arrayNode = putArray(fieldName) collection.forEach(arrayNode::add) } - val result = DecodeJsonObjectVisitor(json).apply { - visitBigDecimalCollection(fieldName, null, createArrayTestSchema("string"), true) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, createArrayTestSchema("string"), true) }.getResult() result.build().assertList(fieldName, collection.map { it.toValue() }) } @@ -323,8 +328,8 @@ class JsonObjectTest { this.set(fieldName, jsonArrayNode) } - val result = DecodeJsonObjectVisitor(json).apply { - visitObjectCollection(fieldName, null, openAPI.components.schemas["ArrayObjectTest"]!! as ArraySchema, true, SchemaWriter(openAPI)) + val result = DecodeJsonObjectVisitor(json, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, openAPI.components.schemas["ArrayObjectTest"]!! as ArraySchema, true) }.getResult() (result[fieldName]!!).let { listValue -> diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/encode/JsonArrayTest.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/encode/JsonArrayTest.kt index d01b7a8..a19bf3b 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/encode/JsonArrayTest.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/encode/JsonArrayTest.kt @@ -16,12 +16,11 @@ package com.exactpro.th2.codec.openapi.json.visitor.encode -import com.exactpro.th2.codec.openapi.assertBoolean -import com.exactpro.th2.codec.openapi.assertFloat -import com.exactpro.th2.codec.openapi.assertInteger -import com.exactpro.th2.codec.openapi.assertString +import com.exactpro.th2.codec.openapi.utils.assertBoolean +import com.exactpro.th2.codec.openapi.utils.assertFloat +import com.exactpro.th2.codec.openapi.utils.assertInteger +import com.exactpro.th2.codec.openapi.utils.assertString import com.exactpro.th2.codec.openapi.OpenApiCodecSettings -import com.exactpro.th2.codec.openapi.writer.SchemaWriter import com.exactpro.th2.codec.openapi.writer.visitors.json.EncodeJsonArrayVisitor import com.exactpro.th2.common.grpc.ListValue import com.exactpro.th2.common.message.addField @@ -29,16 +28,18 @@ import com.exactpro.th2.common.message.message import com.exactpro.th2.common.value.toValue import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode -import com.exactpro.th2.codec.openapi.createArrayTestSchema +import com.exactpro.th2.codec.openapi.utils.createArrayTestSchema import com.exactpro.th2.codec.openapi.utils.getResourceAsText +import com.exactpro.th2.codec.openapi.writer.visitors.VisitorSettings import io.swagger.parser.OpenAPIParser import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.media.ArraySchema -import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.media.StringSchema import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import java.math.BigDecimal +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter class JsonArrayTest { @@ -46,33 +47,33 @@ class JsonArrayTest { @Test fun `not supported encode`() { val message = message().build() - val visitor = EncodeJsonArrayVisitor(message) + val visitor = EncodeJsonArrayVisitor(message, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? String, StringSchema(), true) + visitor.visit("", StringSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Int, StringSchema(), true) + visitor.visit("", StringSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Double, StringSchema(), true) + visitor.visit("", StringSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Long, StringSchema(), true) + visitor.visit("", StringSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Float, StringSchema(), true) + visitor.visit("", StringSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Boolean, StringSchema(), true) + visitor.visit("", StringSchema(), true) } Assertions.assertThrows(UnsupportedOperationException::class.java) { - visitor.visit("", null as? Schema<*>, StringSchema(), true, SchemaWriter(openAPI)) + visitor.visit("", StringSchema(), true) } } @@ -111,8 +112,8 @@ class JsonArrayTest { val message = message().addField(fieldName, listValue).build() - val result = EncodeJsonArrayVisitor(message).apply { - visitObjectCollection(fieldName, null, openAPI.components.schemas["ArrayObjectTest"]!! as ArraySchema, true, SchemaWriter(openAPI)) + val result = EncodeJsonArrayVisitor(message, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, openAPI.components.schemas["ArrayObjectTest"]!! as ArraySchema, true) }.getResult() (mapper.readTree(result.toStringUtf8()) as ArrayNode).let { arrayNode -> @@ -134,9 +135,9 @@ class JsonArrayTest { fun `string array test encode`() { val fieldName = "stringField" val collection = listOf("stringValue1", "stringValue2", "stringValue3", "stringValue4") - val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("string") - visitor.visitStringCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = (mapper.readTree(visitor.getResult().toStringUtf8()) as ArrayNode) collection.forEachIndexed { index, value -> Assertions.assertEquals(value, result.get(index).asText()) @@ -147,9 +148,9 @@ class JsonArrayTest { fun `boolean array test encode`() { val fieldName = "booleanField" val collection = listOf(true, false, false, true) - val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("boolean") - visitor.visitBooleanCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = (mapper.readTree(visitor.getResult().toStringUtf8()) as ArrayNode) collection.forEachIndexed { index, value -> Assertions.assertEquals(value, result.get(index).asBoolean()) @@ -160,9 +161,9 @@ class JsonArrayTest { fun `int array test encode`() { val fieldName = "intField" val collection = listOf(1, 2, 2, 4) - val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("integer") - visitor.visitIntegerCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = (mapper.readTree(visitor.getResult().toStringUtf8()) as ArrayNode) collection.forEachIndexed { index, value -> Assertions.assertEquals(value, result.get(index).asInt()) @@ -173,9 +174,9 @@ class JsonArrayTest { fun `float array test encode`() { val fieldName = "floatField" val collection = listOf(0.1f, 0.2f, 0.2f, 0.4f) - val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("number","float") - visitor.visitFloatCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = (mapper.readTree(visitor.getResult().toStringUtf8()) as ArrayNode) collection.forEachIndexed { index, value -> Assertions.assertEquals(value, result.get(index).asText().toFloat()) @@ -186,9 +187,9 @@ class JsonArrayTest { fun `double array test encode`() { val fieldName = "doubleField" val collection = listOf(0.1, 0.2, 0.2, 0.4) - val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("number", "double") - visitor.visitDoubleCollection(fieldName, null , schema, true) + visitor.visit(fieldName , schema, true) val result = (mapper.readTree(visitor.getResult().toStringUtf8()) as ArrayNode) collection.forEachIndexed { index, value -> Assertions.assertEquals(value, result.get(index).asDouble()) @@ -199,9 +200,9 @@ class JsonArrayTest { fun `long array test encode`() { val fieldName = "longField" val collection = listOf(111111111L, 222222222L, 222222222L, 444444444L) - val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("integer", "int64") - visitor.visitLongCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = (mapper.readTree(visitor.getResult().toStringUtf8()) as ArrayNode) collection.forEachIndexed { index, value -> Assertions.assertEquals(value, result.get(index).asLong()) @@ -212,9 +213,9 @@ class JsonArrayTest { fun `big decimal array test encode`() { val fieldName = "decimalField" val collection = listOf(BigDecimal(100044000000), BigDecimal(100000030000), BigDecimal(100000001000), BigDecimal(100000022000)) - val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonArrayVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("number", "-") - visitor.visitLongCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = (mapper.readTree(visitor.getResult().toStringUtf8()) as ArrayNode) collection.forEachIndexed { index, value -> Assertions.assertEquals(value, result.get(index).asText().toBigDecimal()) diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/encode/JsonObjectTest.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/encode/JsonObjectTest.kt index 02afb98..c7a5637 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/encode/JsonObjectTest.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/json/visitor/encode/JsonObjectTest.kt @@ -16,12 +16,11 @@ package com.exactpro.th2.codec.openapi.json.visitor.encode -import com.exactpro.th2.codec.openapi.assertBoolean -import com.exactpro.th2.codec.openapi.assertFloat -import com.exactpro.th2.codec.openapi.assertInteger -import com.exactpro.th2.codec.openapi.assertString +import com.exactpro.th2.codec.openapi.utils.assertBoolean +import com.exactpro.th2.codec.openapi.utils.assertFloat +import com.exactpro.th2.codec.openapi.utils.assertInteger +import com.exactpro.th2.codec.openapi.utils.assertString import com.exactpro.th2.codec.openapi.OpenApiCodecSettings -import com.exactpro.th2.codec.openapi.writer.SchemaWriter import com.exactpro.th2.codec.openapi.writer.visitors.json.EncodeJsonObjectVisitor import com.exactpro.th2.common.grpc.ListValue import com.exactpro.th2.common.message.addField @@ -29,16 +28,23 @@ import com.exactpro.th2.common.message.message import com.exactpro.th2.common.value.toValue import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode -import com.exactpro.th2.codec.openapi.createArrayTestSchema -import com.exactpro.th2.codec.openapi.createTestSchema +import com.exactpro.th2.codec.openapi.utils.createArrayTestSchema +import com.exactpro.th2.codec.openapi.utils.createTestSchema import com.exactpro.th2.codec.openapi.utils.getResourceAsText +import com.exactpro.th2.codec.openapi.writer.visitors.VisitorSettings import io.swagger.parser.OpenAPIParser import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.media.ArraySchema -import io.swagger.v3.oas.models.media.Schema +import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.IntegerSchema +import io.swagger.v3.oas.models.media.NumberSchema +import io.swagger.v3.oas.models.media.ObjectSchema +import io.swagger.v3.oas.models.media.StringSchema import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import java.math.BigDecimal +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter @Suppress("CAST_NEVER_SUCCEEDS") class JsonObjectTest { @@ -70,8 +76,8 @@ class JsonObjectTest { addField(includedObject, includedObjectValue) }).build() - val result = EncodeJsonObjectVisitor(message).apply { - visit(fieldName, null as? Schema<*>, openAPI.components.schemas["ObjectTest"]!!, true, SchemaWriter(openAPI)) + val result = EncodeJsonObjectVisitor(message, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, openAPI.components.schemas["ObjectTest"]!! as ObjectSchema, true) }.getResult().toStringUtf8() mapper.readTree(result).get(fieldName).let { objectNode -> @@ -88,9 +94,9 @@ class JsonObjectTest { fun `string test encode`() { val fieldName = "stringField" val simpleValue = "stringValue" - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createTestSchema(simpleValue) - visitor.visit(fieldName, null as? String, schema, true) + visitor.visit(fieldName, schema as StringSchema, true) val result = mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName)?.asText() Assertions.assertEquals(simpleValue, result) } @@ -99,9 +105,9 @@ class JsonObjectTest { fun `boolean test encode`() { val fieldName = "booleanField" val simpleValue = true - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createTestSchema(simpleValue) - visitor.visit(fieldName, null as? Boolean, schema, true) + visitor.visit(fieldName, schema as BooleanSchema, true) val result = mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName)?.asBoolean() Assertions.assertEquals(simpleValue, result) } @@ -110,9 +116,9 @@ class JsonObjectTest { fun `int test encode`() { val fieldName = "intField" val simpleValue = 123 - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createTestSchema(simpleValue) - visitor.visit(fieldName, null as? Int, schema, true) + visitor.visit(fieldName, schema as IntegerSchema, true) val result = mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName)?.asInt() Assertions.assertEquals(simpleValue, result) } @@ -121,9 +127,9 @@ class JsonObjectTest { fun `float test encode`() { val fieldName = "floatField" val simpleValue = 123.1f - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createTestSchema(simpleValue) - visitor.visit(fieldName, null as? Float, schema, true) + visitor.visit(fieldName, schema as NumberSchema, true) val result = mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName)?.asText()?.toFloat() Assertions.assertEquals(simpleValue, result) } @@ -132,9 +138,9 @@ class JsonObjectTest { fun `double test encode`() { val fieldName = "doubleField" val simpleValue = 123.1 - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createTestSchema(simpleValue) - visitor.visit(fieldName, null as? Double, schema, true) + visitor.visit(fieldName, schema as NumberSchema, true) val result = mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName)?.asDouble() Assertions.assertEquals(simpleValue, result) } @@ -143,9 +149,9 @@ class JsonObjectTest { fun `long test encode`() { val fieldName = "longField" val simpleValue = 123123L - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createTestSchema(simpleValue) - visitor.visit(fieldName, null as? Long, schema, true) + visitor.visit(fieldName, schema as IntegerSchema, true) val result = mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName)?.asLong() Assertions.assertEquals(simpleValue, result) } @@ -154,9 +160,9 @@ class JsonObjectTest { fun `big decimal test encode`() { val fieldName = "decimalField" val simpleValue = BigDecimal(100000000000) - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, simpleValue).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createTestSchema(simpleValue) - visitor.visit(fieldName, null as? BigDecimal, schema, true) + visitor.visit(fieldName, schema as NumberSchema, true) val result = mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName)?.asText()?.toBigDecimal() Assertions.assertEquals(simpleValue, result) } @@ -165,9 +171,9 @@ class JsonObjectTest { fun `string array test encode`() { val fieldName = "stringField" val collection = listOf("stringValue1", "stringValue2", "stringValue3", "stringValue4") - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("string") - visitor.visitStringCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = requireNotNull(mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName) as? ArrayNode) Assertions.assertEquals(4, result.size()) collection.forEachIndexed { index, value -> @@ -179,9 +185,9 @@ class JsonObjectTest { fun `boolean array test encode`() { val fieldName = "booleanField" val collection = listOf(true, false, false, true) - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("boolean") - visitor.visitBooleanCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = requireNotNull(mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName) as? ArrayNode) Assertions.assertEquals(4, result.size()) collection.forEachIndexed { index, value -> @@ -193,9 +199,9 @@ class JsonObjectTest { fun `int array test encode`() { val fieldName = "intField" val collection = listOf(1, 2, 2, 4) - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("integer") - visitor.visitIntegerCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = requireNotNull(mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName) as? ArrayNode) Assertions.assertEquals(4, result.size()) collection.forEachIndexed { index, value -> @@ -207,9 +213,9 @@ class JsonObjectTest { fun `float array test encode`() { val fieldName = "floatField" val collection = listOf(0.1f, 0.2f, 0.2f, 0.4f) - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("number","float") - visitor.visitFloatCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = requireNotNull(mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName) as? ArrayNode) Assertions.assertEquals(4, result.size()) collection.forEachIndexed { index, value -> @@ -221,9 +227,9 @@ class JsonObjectTest { fun `double array test encode`() { val fieldName = "doubleField" val collection = listOf(0.1, 0.2, 0.2, 0.4) - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("number", "double") - visitor.visitDoubleCollection(fieldName, null , schema, true) + visitor.visit(fieldName, schema, true) val result = requireNotNull(mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName) as? ArrayNode) Assertions.assertEquals(4, result.size()) collection.forEachIndexed { index, value -> @@ -235,9 +241,9 @@ class JsonObjectTest { fun `long array test encode`() { val fieldName = "longField" val collection = listOf(111111111L, 222222222L, 222222222L, 444444444L) - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("integer", "int64") - visitor.visitLongCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = requireNotNull(mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName) as? ArrayNode) Assertions.assertEquals(4, result.size()) collection.forEachIndexed { index, value -> @@ -249,9 +255,9 @@ class JsonObjectTest { fun `big decimal array test encode`() { val fieldName = "decimalField" val collection = listOf(BigDecimal(100000000010), BigDecimal(100000002000), BigDecimal(100300000000), BigDecimal(100004000000)) - val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build()) + val visitor = EncodeJsonObjectVisitor(message().addField(fieldName, collection).build(), VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)) val schema = createArrayTestSchema("number", "-") - visitor.visitBigDecimalCollection(fieldName, null, schema, true) + visitor.visit(fieldName, schema, true) val result = requireNotNull(mapper.readTree(visitor.getResult().toStringUtf8()).get(fieldName) as? ArrayNode) Assertions.assertEquals(4, result.size()) collection.forEachIndexed { index, value -> @@ -294,8 +300,8 @@ class JsonObjectTest { val message = message().addField(fieldName, listValue).build() - val result = EncodeJsonObjectVisitor(message).apply { - visitObjectCollection(fieldName, null, openAPI.components.schemas["ArrayObjectTest"]!! as ArraySchema, true, SchemaWriter(openAPI)) + val result = EncodeJsonObjectVisitor(message, VisitorSettings(openAPI, SimpleDateFormat(), DateTimeFormatter.ISO_DATE_TIME)).apply { + visit(fieldName, openAPI.components.schemas["ArrayObjectTest"]!! as ArraySchema, true) }.getResult().toStringUtf8() mapper.readTree(result).let { objectNode -> diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/JsonTestUtils.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/JsonTestUtils.kt index 946fce8..6543215 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/JsonTestUtils.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/JsonTestUtils.kt @@ -1,4 +1,4 @@ -package com.exactpro.th2.codec.openapi/* +/* * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ package com.exactpro.th2.codec.openapi/* * limitations under the License. */ +package com.exactpro.th2.codec.openapi.utils + import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeType diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/SchemaTestUtils.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/SchemaTestUtils.kt index d34ef13..5ed98ce 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/SchemaTestUtils.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/SchemaTestUtils.kt @@ -1,4 +1,4 @@ -package com.exactpro.th2.codec.openapi/* +/* * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,11 @@ package com.exactpro.th2.codec.openapi/* * limitations under the License. */ +package com.exactpro.th2.codec.openapi.utils + import io.swagger.v3.oas.models.media.ArraySchema import io.swagger.v3.oas.models.media.BooleanSchema +import io.swagger.v3.oas.models.media.IntegerSchema import io.swagger.v3.oas.models.media.NumberSchema import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.media.StringSchema @@ -39,8 +42,8 @@ inline fun createTestSchema(value: T?, fillEnum: List? = null) } } Int::class -> { - return NumberSchema().apply { - type = "number" + return IntegerSchema().apply { + type = "int32" example = value fillEnum?.forEach { enum.add((it as Int).toBigDecimal()) @@ -48,8 +51,8 @@ inline fun createTestSchema(value: T?, fillEnum: List? = null) } } Long::class -> { - return NumberSchema().apply { - type = "number" + return IntegerSchema().apply { + type = "int64" example = value fillEnum?.forEach { enum.add((it as Long).toBigDecimal()) diff --git a/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/TestUtils.kt b/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/TestUtils.kt index 0d5eef4..53e32b2 100644 --- a/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/TestUtils.kt +++ b/src/test/kotlin/com/exactpro/th2/codec/openapi/utils/TestUtils.kt @@ -1,4 +1,4 @@ -package com.exactpro.th2.codec.openapi.utils/* +/* * Copyright 2021-2022 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ package com.exactpro.th2.codec.openapi.utils/* * limitations under the License. */ +package com.exactpro.th2.codec.openapi.utils + import com.exactpro.th2.codec.openapi.OpenApiCodec import com.exactpro.th2.common.assertEqualMessages import com.exactpro.th2.common.assertString @@ -157,42 +159,41 @@ fun OpenApiCodec.testEncode(path: String, method: String, code: String?, type: S val resultGroup = encode(messageGroup.build()) - Assertions.assertTrue(resultGroup.messagesList[0].hasMessage()) + Assertions.assertEquals(1, resultGroup.messagesList.size) - val header = resultGroup.messagesList[0].message + if (fillMessage == null) { + Assertions.assertTrue(resultGroup.messagesList[0].hasMessage()) + val header = resultGroup.messagesList[0].message - val headerType = if (code == null) REQUEST_MESSAGE_TYPE else RESPONSE_MESSAGE_TYPE + val headerType = if (code == null) REQUEST_MESSAGE_TYPE else RESPONSE_MESSAGE_TYPE - header.run { - Assertions.assertEquals(rawParentID, parentEventId) {"Decode process shouldn't lose parent event id inside of header"} - Assertions.assertEquals(headerType, this.messageType) - messageToEncode.metadata.propertiesMap.forEach { - Assertions.assertEquals(it.value, this.metadata.propertiesMap[it.key]) {"Property ${it.key} must be the same as in input message"} - } - when (headerType) { - REQUEST_MESSAGE_TYPE -> { - assertString(OpenApiCodec.URI_FIELD, path) - assertString(OpenApiCodec.METHOD_FIELD, method) - type?.let { - this.getList(OpenApiCodec.HEADERS_FIELD)!![0].messageValue.assertString("value", type) - } ?: Assertions.assertNull(this.getList(OpenApiCodec.HEADERS_FIELD)?.find { it.messageValue.getString("name") == FORMAT_HEADER_NAME }) + header.run { + Assertions.assertEquals(rawParentID, parentEventId) {"Decode process shouldn't lose parent event id inside of header"} + Assertions.assertEquals(headerType, this.messageType) + messageToEncode.metadata.propertiesMap.forEach { + Assertions.assertEquals(it.value, this.metadata.propertiesMap[it.key]) {"Property ${it.key} must be the same as in input message"} } - RESPONSE_MESSAGE_TYPE -> { - assertString(OpenApiCodec.CODE_FIELD, code) - type?.let { - this.getList(OpenApiCodec.HEADERS_FIELD)!![0].messageValue.assertString("value", type) - } ?: Assertions.assertNull(this.getList(OpenApiCodec.HEADERS_FIELD)?.find { it.messageValue.getString("name") == FORMAT_HEADER_NAME }) + when (headerType) { + REQUEST_MESSAGE_TYPE -> { + assertString(OpenApiCodec.URI_FIELD, path) + assertString(OpenApiCodec.METHOD_FIELD, method) + type?.let { + this.getList(OpenApiCodec.HEADERS_FIELD)!![0].messageValue.assertString("value", type) + } ?: Assertions.assertNull(this.getList(OpenApiCodec.HEADERS_FIELD)?.find { it.messageValue.getString("name") == FORMAT_HEADER_NAME }) + } + RESPONSE_MESSAGE_TYPE -> { + assertString(OpenApiCodec.CODE_FIELD, code) + type?.let { + this.getList(OpenApiCodec.HEADERS_FIELD)!![0].messageValue.assertString("value", type) + } ?: Assertions.assertNull(this.getList(OpenApiCodec.HEADERS_FIELD)?.find { it.messageValue.getString("name") == FORMAT_HEADER_NAME }) + } + else -> error("Wrong type of header message: $headerType") } - else -> error("Wrong type of header message: $headerType") } - } - - val body = resultGroup.messagesList.getOrNull(1)?.rawMessage + } else { + val body = resultGroup.messagesList.getOrNull(0)?.rawMessage - if (fillMessage != null) { Assertions.assertNotNull(body) {"Encode must have body in result of filling incoming message"} - Assertions.assertEquals(2, resultGroup.messagesList.size) - Assertions.assertTrue(resultGroup.messagesList[1].hasRawMessage()) body!!.let { rawMessage -> messageToEncode.metadata.propertiesMap.forEach { @@ -205,11 +206,10 @@ fun OpenApiCodec.testEncode(path: String, method: String, code: String?, type: S Assertions.assertEquals(method.lowercase(), rawMessage.metadata.propertiesMap[OpenApiCodec.METHOD_PROPERTY]?.lowercase()) Assertions.assertEquals(path, rawMessage.metadata.propertiesMap[OpenApiCodec.URI_PROPERTY]) } - } else { - Assertions.assertEquals(1, resultGroup.messagesList.size) + return body } - return body + return null } fun OpenApiCodec.encode(message: Message) = encode(MessageGroup.newBuilder().addMessages(AnyMessage.newBuilder().setMessage(message).build()).build()) diff --git a/src/test/resources/dictionaries/valid/object-json-tests.yml b/src/test/resources/dictionaries/valid/object-json-tests.yml index 56af9b5..aa7a770 100644 --- a/src/test/resources/dictionaries/valid/object-json-tests.yml +++ b/src/test/resources/dictionaries/valid/object-json-tests.yml @@ -19,6 +19,14 @@ paths: schema: $ref: "#/components/schemas/TestObject" responses: + "300": + description: One Of Test + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/TestObject" + - $ref: "#/components/schemas/TestObjectOneOf" "200": description: OK content: @@ -28,6 +36,20 @@ paths: components: schemas: + TestObjectOneOf: + type: object + description: | + Defines the test object. + required: + - oneOfEnabled + - oneOfInteger + properties: + oneOfInteger: + type: integer + example: 123 + oneOfEnabled: + type: boolean + example: false TestStatus: type: string description: | diff --git a/src/test/resources/dictionaries/valid/valid-dictionary.yml b/src/test/resources/dictionaries/valid/valid-dictionary.yml index 9c543da..c2d3494 100644 --- a/src/test/resources/dictionaries/valid/valid-dictionary.yml +++ b/src/test/resources/dictionaries/valid/valid-dictionary.yml @@ -19,6 +19,14 @@ paths: schema: $ref: "#/components/schemas/TestObject" responses: + "300": + description: One Of Test + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/TestObject" + - $ref: "#/components/schemas/TestObjectOneOf" "200": description: OK content: @@ -180,6 +188,20 @@ components: example: false testStatus: $ref: "#/components/schemas/TestStatus" + TestObjectOneOf: + type: object + description: | + Defines the test object. + required: + - oneOfEnabled + - oneOfInteger + properties: + oneOfInteger: + type: integer + example: 123 + oneOfEnabled: + type: boolean + example: false TestStatus: type: string description: |