diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java index aa5a3c2b4..4bb67ca96 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java @@ -63,7 +63,10 @@ public enum DiagnosticMessages { "for Ballerina type '%s'. ", DiagnosticSeverity.WARNING), OAS_CONVERTOR_115("OAS_CONVERTOR_115", "Given Ballerina file does not contain any HTTP service.", - DiagnosticSeverity.ERROR); + DiagnosticSeverity.ERROR), + OAS_CONVERTOR_116("OAS_CONVERTOR_116", "Generated OpenAPI definition does not contain `%s` request" + + " body information, as it's not supported by the OpenAPI tool.", + DiagnosticSeverity.WARNING); private final String code; private final String description; diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java index 27f724bbe..9308f66e3 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java @@ -19,24 +19,32 @@ package io.ballerina.openapi.converter.service; import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.TypeDescKind; +import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; import io.ballerina.compiler.api.symbols.TypeSymbol; import io.ballerina.compiler.syntax.tree.AnnotationNode; import io.ballerina.compiler.syntax.tree.ArrayTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.MapTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; -import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.OptionalTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; import io.ballerina.compiler.syntax.tree.RequiredParameterNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.UnionTypeDescriptorNode; import io.ballerina.openapi.converter.Constants; +import io.ballerina.openapi.converter.diagnostic.DiagnosticMessages; +import io.ballerina.openapi.converter.diagnostic.IncompatibleResourceDiagnostic; import io.ballerina.openapi.converter.diagnostic.OpenAPIConverterDiagnostic; import io.ballerina.openapi.converter.utils.ConverterCommonUtils; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.ComposedSchema; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; @@ -52,6 +60,7 @@ import javax.ws.rs.core.MediaType; import static io.ballerina.openapi.converter.Constants.APPLICATION_PREFIX; +import static io.ballerina.openapi.converter.Constants.DELETE; import static io.ballerina.openapi.converter.Constants.HTTP_PAYLOAD; import static io.ballerina.openapi.converter.Constants.JSON_POSTFIX; import static io.ballerina.openapi.converter.Constants.MEDIA_TYPE; @@ -60,6 +69,7 @@ import static io.ballerina.openapi.converter.Constants.TEXT_PREFIX; import static io.ballerina.openapi.converter.Constants.XML_POSTFIX; import static io.ballerina.openapi.converter.utils.ConverterCommonUtils.extractAnnotationFieldDetails; +import static io.ballerina.openapi.converter.utils.ConverterCommonUtils.getOpenApiSchema; /** * OpenAPIRequestBodyMapper provides functionality for converting ballerina payload to OAS request body model. @@ -70,7 +80,7 @@ public class OpenAPIRequestBodyMapper { private final Components components; private final OperationAdaptor operationAdaptor; private final SemanticModel semanticModel; - private final String customMediaType; + private final String customMediaPrefix; private final List diagnostics; /** @@ -86,7 +96,7 @@ public OpenAPIRequestBodyMapper(Components components, OperationAdaptor operatio this.components = components; this.operationAdaptor = operationAdaptor; this.semanticModel = semanticModel; - this.customMediaType = customMediaType; + this.customMediaPrefix = customMediaType; this.diagnostics = new ArrayList<>(); } @@ -140,8 +150,7 @@ public void handlePayloadAnnotation(RequiredParameterNode payloadNode, Map */ - private void handleSinglePayloadType(RequiredParameterNode payloadNode, Map schema, - RequestBody bodyParameter, String customMediaPrefix) { + private void handlePayloadType(TypeDescriptorNode payloadNode, Map schema, + RequestBody bodyParameter) { + SyntaxKind kind = payloadNode.kind(); + String mediaTypeString = getMediaTypeForSyntaxKind(payloadNode); + if (mediaTypeString != null || kind == SyntaxKind.UNION_TYPE_DESC || + kind == SyntaxKind.QUALIFIED_NAME_REFERENCE || kind == SyntaxKind.OPTIONAL_TYPE_DESC || + kind == SyntaxKind.ARRAY_TYPE_DESC) { + switch (kind) { + case INT_TYPE_DESC: + case FLOAT_TYPE_DESC: + case DECIMAL_TYPE_DESC: + case BOOLEAN_TYPE_DESC: + case JSON_TYPE_DESC: + case XML_TYPE_DESC: + case BYTE_TYPE_DESC: + addConsumes(operationAdaptor, bodyParameter, mediaTypeString, kind); + break; + case STRING_TYPE_DESC: + mediaTypeString = customMediaPrefix == null ? MediaType.TEXT_PLAIN : + TEXT_PREFIX + customMediaPrefix + TEXT_POSTFIX; + addConsumes(operationAdaptor, bodyParameter, mediaTypeString, kind); + break; + case MAP_TYPE_DESC: + Schema objectSchema = new ObjectSchema(); + MapTypeDescriptorNode mapTypeDescriptorNode = (MapTypeDescriptorNode) payloadNode; + SyntaxKind mapMemberType = (mapTypeDescriptorNode.mapTypeParamsNode()).typeNode().kind(); + Schema openApiSchema = getOpenApiSchema(mapMemberType); + objectSchema.additionalProperties(openApiSchema); + io.swagger.v3.oas.models.media.MediaType mediaType = new io.swagger.v3.oas.models.media.MediaType(); + if (bodyParameter.getContent() != null) { + Content content = bodyParameter.getContent(); + if (content.containsKey(mediaTypeString)) { + ComposedSchema oneOfSchema = new ComposedSchema(); + oneOfSchema.addOneOfItem(content.get(mediaTypeString).getSchema()); + oneOfSchema.addOneOfItem(objectSchema); + mediaType.setSchema(oneOfSchema); + content.addMediaType(mediaTypeString, mediaType); + } else { + mediaType.setSchema(objectSchema); + content.addMediaType(mediaTypeString, mediaType); + } + } else { + mediaType.setSchema(objectSchema); + bodyParameter.setContent(new Content().addMediaType(mediaTypeString, mediaType)); + } + operationAdaptor.getOperation().setRequestBody(bodyParameter); + break; + case OPTIONAL_TYPE_DESC: + OptionalTypeDescriptorNode optionalTypeDescriptorNode = (OptionalTypeDescriptorNode) payloadNode; + handlePayloadType((TypeDescriptorNode) optionalTypeDescriptorNode.typeDescriptor(), + schema, bodyParameter); + break; + case UNION_TYPE_DESC: + UnionTypeDescriptorNode unionTypeDescriptorNode = (UnionTypeDescriptorNode) payloadNode; + TypeDescriptorNode leftTypeDesc = unionTypeDescriptorNode.leftTypeDesc(); + TypeDescriptorNode rightTypeDesc = unionTypeDescriptorNode.rightTypeDesc(); + handlePayloadType(leftTypeDesc, schema, bodyParameter); + handlePayloadType(rightTypeDesc, schema, bodyParameter); + break; + case SIMPLE_NAME_REFERENCE: + SimpleNameReferenceNode record = (SimpleNameReferenceNode) payloadNode; + TypeSymbol typeSymbol = getReferenceTypeSymbol(semanticModel.symbol(record)); + String recordName = record.name().toString().trim(); + handleReferencePayload(typeSymbol, mediaTypeString, recordName, schema, bodyParameter); + break; + case QUALIFIED_NAME_REFERENCE: + QualifiedNameReferenceNode separateRecord = (QualifiedNameReferenceNode) payloadNode; + typeSymbol = getReferenceTypeSymbol(semanticModel.symbol(separateRecord)); + recordName = ((QualifiedNameReferenceNode) payloadNode).identifier().text(); + TypeDescKind typeDesc = ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor().typeKind(); + String mimeType = getMediaTypeForTypeDecKind(typeDesc); + + handleReferencePayload(typeSymbol, mimeType, recordName, schema, bodyParameter); + break; + case ARRAY_TYPE_DESC: + ArrayTypeDescriptorNode arrayNode = (ArrayTypeDescriptorNode) payloadNode; + mediaTypeString = getMediaTypeForSyntaxKind(arrayNode.memberTypeDesc()); + handleArrayTypePayload(schema, arrayNode, mediaTypeString, bodyParameter); + break; + default: + //Warning message for unsupported request payload type in Ballerina resource. + IncompatibleResourceDiagnostic error = new IncompatibleResourceDiagnostic( + DiagnosticMessages.OAS_CONVERTOR_116, payloadNode.location(), + String.valueOf(payloadNode.kind())); + diagnostics.add(error); + break; + } + } else { + //Warning message for unsupported request payload type in Ballerina resource. + IncompatibleResourceDiagnostic error = new IncompatibleResourceDiagnostic( + DiagnosticMessages.OAS_CONVERTOR_116, payloadNode.location(), payloadNode.toSourceCode().trim()); + diagnostics.add(error); + } + } - String consumes = payloadNode.typeName().toString().trim(); - String mediaTypeString; - switch (consumes) { - case Constants.JSON: - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_JSON : + /** + * This function is used to select the media type according to the syntax kind. + * + * @param payloadNode - payload type descriptor node + */ + private String getMediaTypeForSyntaxKind(TypeDescriptorNode payloadNode) { + SyntaxKind kind = payloadNode.kind(); + switch (kind) { + case RECORD_TYPE_DESC: + case MAP_TYPE_DESC: + case INT_TYPE_DESC: + case FLOAT_TYPE_DESC: + case DECIMAL_TYPE_DESC: + case BOOLEAN_TYPE_DESC: + case JSON_TYPE_DESC: + return customMediaPrefix == null ? MediaType.APPLICATION_JSON : APPLICATION_PREFIX + customMediaPrefix + JSON_POSTFIX; - addConsumes(operationAdaptor, bodyParameter, mediaTypeString); - break; - case Constants.XML: - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_XML : + case XML_TYPE_DESC: + return customMediaPrefix == null ? MediaType.APPLICATION_XML : APPLICATION_PREFIX + customMediaPrefix + XML_POSTFIX; - addConsumes(operationAdaptor, bodyParameter, mediaTypeString); - break; - case Constants.STRING: - mediaTypeString = customMediaPrefix == null ? MediaType.TEXT_PLAIN : + case STRING_TYPE_DESC: + return customMediaPrefix == null ? MediaType.TEXT_PLAIN : TEXT_PREFIX + customMediaPrefix + TEXT_POSTFIX; - addConsumes(operationAdaptor, bodyParameter, mediaTypeString); - break; - case Constants.BYTE_ARRAY: - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_OCTET_STREAM : + case BYTE_TYPE_DESC: + return customMediaPrefix == null ? MediaType.APPLICATION_OCTET_STREAM : APPLICATION_PREFIX + customMediaPrefix + OCTECT_STREAM_POSTFIX; - addConsumes(operationAdaptor, bodyParameter, mediaTypeString); + case SIMPLE_NAME_REFERENCE: + SimpleNameReferenceNode record = (SimpleNameReferenceNode) payloadNode; + TypeSymbol typeSymbol = getReferenceTypeSymbol(semanticModel.symbol(record)); + if (typeSymbol instanceof TypeReferenceTypeSymbol) { + typeSymbol = ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(); + } + if (typeSymbol instanceof IntersectionTypeSymbol) { + typeSymbol = ((IntersectionTypeSymbol) typeSymbol).effectiveTypeDescriptor(); + } + return getMediaTypeForTypeDecKind(typeSymbol.typeKind()); + default: + // Default the warning will be added in the handlePayloadType function. break; - case Constants.MAP_STRING: - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_JSON : + } + return null; + } + + /** + * This function is used to select the media type according to the type descriptor kind. + */ + private String getMediaTypeForTypeDecKind(TypeDescKind type) { + switch (type) { + case RECORD: + case MAP: + case INT: + case INT_SIGNED32: + case INT_SIGNED16: + case FLOAT: + case DECIMAL: + case BOOLEAN: + case JSON: + return customMediaPrefix == null ? MediaType.APPLICATION_JSON : APPLICATION_PREFIX + customMediaPrefix + JSON_POSTFIX; - Schema objectSchema = new ObjectSchema(); - objectSchema.additionalProperties(new StringSchema()); - io.swagger.v3.oas.models.media.MediaType mediaType = new io.swagger.v3.oas.models.media.MediaType(); - mediaType.setSchema(objectSchema); - bodyParameter.setContent(new Content().addMediaType(mediaTypeString, mediaType)); - operationAdaptor.getOperation().setRequestBody(bodyParameter); - break; + case XML: + return customMediaPrefix == null ? MediaType.APPLICATION_XML : + APPLICATION_PREFIX + customMediaPrefix + XML_POSTFIX; + case STRING: + return customMediaPrefix == null ? MediaType.TEXT_PLAIN : + TEXT_PREFIX + customMediaPrefix + TEXT_POSTFIX; + case BYTE: + return customMediaPrefix == null ? MediaType.APPLICATION_OCTET_STREAM : + APPLICATION_PREFIX + customMediaPrefix + OCTECT_STREAM_POSTFIX; default: - Node node = payloadNode.typeName(); - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_JSON : - APPLICATION_PREFIX + customMediaPrefix + JSON_POSTFIX; - if (node.kind() == SyntaxKind.SIMPLE_NAME_REFERENCE) { - SimpleNameReferenceNode record = (SimpleNameReferenceNode) node; - // Creating request body - required. - TypeSymbol typeSymbol = getReferenceTypeSymbol(semanticModel.symbol(record)); - String recordName = record.name().toString().trim(); - handleReferencePayload(typeSymbol, recordName, schema, mediaTypeString, bodyParameter); - } else if (node instanceof ArrayTypeDescriptorNode) { - handleArrayTypePayload(schema, (ArrayTypeDescriptorNode) node, mediaTypeString, bodyParameter); - } else if (node.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE) { - QualifiedNameReferenceNode separateRecord = (QualifiedNameReferenceNode) node; - TypeSymbol typeSymbol = getReferenceTypeSymbol(semanticModel.symbol(separateRecord)); - String recordName = ((QualifiedNameReferenceNode) payloadNode.typeName()).identifier().text(); - handleReferencePayload(typeSymbol, recordName, schema, mediaTypeString, bodyParameter); - } + // Default return the null and the warning will be added in the handlePayloadType function. break; } + return null; } private TypeSymbol getReferenceTypeSymbol(Optional symbol) { @@ -224,6 +345,7 @@ private void handleArrayTypePayload(Map schema, ArrayTypeDescrip ArraySchema arraySchema = new ArraySchema(); TypeDescriptorNode typeDescriptorNode = arrayNode.memberTypeDesc(); // Nested array not allowed + io.swagger.v3.oas.models.media.MediaType media = new io.swagger.v3.oas.models.media.MediaType(); if (typeDescriptorNode.kind().equals(SyntaxKind.SIMPLE_NAME_REFERENCE)) { //handle record for components SimpleNameReferenceNode referenceNode = (SimpleNameReferenceNode) typeDescriptorNode; @@ -234,19 +356,36 @@ private void handleArrayTypePayload(Map schema, ArrayTypeDescrip Schema itemSchema = new Schema(); arraySchema.setItems(itemSchema.$ref(ConverterCommonUtils.unescapeIdentifier( referenceNode.name().text().trim()))); - io.swagger.v3.oas.models.media.MediaType media = new io.swagger.v3.oas.models.media.MediaType(); media.setSchema(arraySchema); - if (requestBody.getContent() != null) { - Content content = requestBody.getContent(); - content.addMediaType(mimeType, media); + } else if (typeDescriptorNode.kind() == SyntaxKind.BYTE_TYPE_DESC) { + StringSchema byteSchema = new StringSchema(); + byteSchema.setFormat("byte"); + media.setSchema(byteSchema); + } else { + Schema itemSchema = getOpenApiSchema(typeDescriptorNode.kind()); + arraySchema.setItems(itemSchema); + media.setSchema(arraySchema); + } + + if (requestBody.getContent() != null) { + Content content = requestBody.getContent(); + if (content.containsKey(mimeType)) { + ComposedSchema oneOfSchema = new ComposedSchema(); + oneOfSchema.addOneOfItem(content.get(mimeType).getSchema()); + oneOfSchema.addOneOfItem(arraySchema); + media.setSchema(oneOfSchema); } else { - requestBody.setContent(new Content().addMediaType(mimeType, media)); - } - // Adding conditional check for http delete operation as it cannot have body - // parameter. - if (!operationAdaptor.getHttpOperation().equalsIgnoreCase("delete")) { - operationAdaptor.getOperation().setRequestBody(requestBody); + content.addMediaType(mimeType, media); } + content.addMediaType(mimeType, media); + requestBody.setContent(content); + } else { + requestBody.setContent(new Content().addMediaType(mimeType, media)); + } + // Adding conditional check for http delete operation as it cannot have body + // parameter. + if (!operationAdaptor.getHttpOperation().equalsIgnoreCase("delete")) { + operationAdaptor.getOperation().setRequestBody(requestBody); } } @@ -258,7 +397,7 @@ private void createRequestBody(RequestBody bodyParameter, RequiredParameterNode SimpleNameReferenceNode record = (SimpleNameReferenceNode) payloadNode.typeName(); TypeSymbol typeSymbol = getReferenceTypeSymbol(semanticModel.symbol(record)); String recordName = record.name().toString().trim(); - handleReferencePayload(typeSymbol, recordName, schema, mimeType, requestBody); + handleReferencePayload(typeSymbol, mimeType, recordName, schema, requestBody); } else if (payloadNode.typeName() instanceof ArrayTypeDescriptorNode) { ArrayTypeDescriptorNode arrayTypeDescriptorNode = (ArrayTypeDescriptorNode) payloadNode.typeName(); handleArrayTypePayload(schema, arrayTypeDescriptorNode, mimeType, requestBody); @@ -280,8 +419,8 @@ private void createRequestBody(RequestBody bodyParameter, RequiredParameterNode /** * Handle record type request payload. */ - private void handleReferencePayload(TypeSymbol typeSymbol, String recordName, - Map schema, String mediaType, RequestBody bodyParameter) { + private void handleReferencePayload(TypeSymbol typeSymbol, String mediaType, String recordName, + Map schema, RequestBody bodyParameter) { //handle record for components OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components); componentMapper.createComponentSchema(schema, typeSymbol); @@ -290,23 +429,52 @@ private void handleReferencePayload(TypeSymbol typeSymbol, String recordName, media.setSchema(new Schema().$ref(ConverterCommonUtils.unescapeIdentifier(recordName))); if (bodyParameter.getContent() != null) { Content content = bodyParameter.getContent(); - content.addMediaType(mediaType, media); + if (content.containsKey(mediaType)) { + io.swagger.v3.oas.models.media.MediaType mediaSchema = content.get(mediaType); + Schema currentSchema = mediaSchema.getSchema(); + ComposedSchema oneOf = new ComposedSchema(); + oneOf.addOneOfItem(currentSchema); + oneOf.addOneOfItem(new Schema().$ref(ConverterCommonUtils.unescapeIdentifier(recordName))); + mediaSchema.setSchema(oneOf); + content.addMediaType(mediaType, mediaSchema); + } else { + content.addMediaType(mediaType, media); + } + bodyParameter.setContent(content); } else { bodyParameter.setContent(new Content().addMediaType(mediaType, media)); } // Adding conditional check for http delete operation as it cannot have body // parameter. - if (!operationAdaptor.getHttpOperation().equalsIgnoreCase("delete")) { + if (!operationAdaptor.getHttpOperation().equalsIgnoreCase(DELETE)) { operationAdaptor.getOperation().setRequestBody(bodyParameter); } } - private void addConsumes(OperationAdaptor operationAdaptor, RequestBody bodyParameter, String applicationType) { - String type = applicationType.split("/")[1]; + private void addConsumes(OperationAdaptor operationAdaptor, RequestBody bodyParameter, String applicationType, + SyntaxKind kindType) { + String type = kindType.name().split("_")[0].toLowerCase(Locale.ENGLISH); Schema schema = ConverterCommonUtils.getOpenApiSchema(type); io.swagger.v3.oas.models.media.MediaType mediaType = new io.swagger.v3.oas.models.media.MediaType(); - mediaType.setSchema(schema); - bodyParameter.setContent(new Content().addMediaType(applicationType, mediaType)); - operationAdaptor.getOperation().setRequestBody(bodyParameter); + if (bodyParameter.getContent() != null) { + Content content = bodyParameter.getContent(); + if (content.containsKey(applicationType)) { + io.swagger.v3.oas.models.media.MediaType mediaSchema = content.get(applicationType); + Schema currentSchema = mediaSchema.getSchema(); + ComposedSchema oneOf = new ComposedSchema(); + oneOf.addOneOfItem(currentSchema); + oneOf.addOneOfItem(schema); + mediaSchema.setSchema(oneOf); + content.addMediaType(applicationType, mediaSchema); + } else { + mediaType.setSchema(schema); + content.addMediaType(applicationType, mediaType); + } + bodyParameter.setContent(content); + } else { + mediaType.setSchema(schema); + bodyParameter.setContent(new Content().addMediaType(applicationType, mediaType)); + operationAdaptor.getOperation().setRequestBody(bodyParameter); + } } } diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResourceMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResourceMapper.java index ae33ba95a..517bfcb47 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResourceMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResourceMapper.java @@ -34,6 +34,7 @@ import io.ballerina.openapi.converter.diagnostic.IncompatibleResourceDiagnostic; import io.ballerina.openapi.converter.diagnostic.OpenAPIConverterDiagnostic; import io.ballerina.openapi.converter.utils.ConverterCommonUtils; +import io.ballerina.tools.diagnostics.DiagnosticSeverity; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -215,8 +216,12 @@ private Optional convertResourceToOperation(FunctionDefinition openAPIParameterMapper.getResourceInputs(components, semanticModel); if (openAPIParameterMapper.getErrors().size() > 1 || (openAPIParameterMapper.getErrors().size() == 1 && !openAPIParameterMapper.getErrors().get(0).getCode().equals("OAS_CONVERTOR_113"))) { - errors.addAll(openAPIParameterMapper.getErrors()); - return Optional.empty(); + boolean isErrorIncluded = openAPIParameterMapper.getErrors().stream().anyMatch(d -> + DiagnosticSeverity.ERROR.equals(d.getDiagnosticSeverity())); + if (isErrorIncluded) { + errors.addAll(openAPIParameterMapper.getErrors()); + return Optional.empty(); + } } errors.addAll(openAPIParameterMapper.getErrors()); diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/RequestBodyTest.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/RequestBodyTest.java index c7306a645..77806e859 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/RequestBodyTest.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/RequestBodyTest.java @@ -264,6 +264,12 @@ public void testForServiceConfigOnlyWithCors() { compareWithGeneratedFile(ballerinaFilePath, "service_config_with_cors.yaml"); } + @Test(description = "Generate OpenAPI spec for request body with union type") + public void testForUnionTypeRequestBody() { + Path ballerinaFilePath = RES_DIR.resolve("request_body/union_type.bal"); + compareWithGeneratedFile(ballerinaFilePath, "union_type.yaml"); + } + @AfterMethod public void cleanUp() { deleteDirectory(this.tempDir); diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/request_body/union_type.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/request_body/union_type.yaml new file mode 100644 index 000000000..72172bab0 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/request_body/union_type.yaml @@ -0,0 +1,182 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "0" +paths: + /path: + post: + operationId: postPath + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ABC' + responses: + "202": + description: Accepted + /path1: + post: + operationId: postPath1 + responses: + "202": + description: Accepted + /path2: + post: + operationId: postPath2 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ABC' + application/xml: + schema: {} + responses: + "202": + description: Accepted + /path3: + post: + operationId: postPath3 + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ABC' + - $ref: '#/components/schemas/Remote' + application/xml: + schema: {} + responses: + "202": + description: Accepted + /path4: + post: + operationId: postPath4 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ABC' + text/plain: + schema: + type: string + application/xml: + schema: {} + responses: + "202": + description: Accepted + /path5: + post: + operationId: postPath5 + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ABC' + - type: integer + format: int64 + responses: + "202": + description: Accepted + /path6: + post: + operationId: postPath6 + requestBody: + content: + application/json: + schema: + type: integer + format: int64 + text/plain: + schema: + type: string + responses: + "202": + description: Accepted + /path7: + post: + operationId: postPath7 + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + additionalProperties: {} + - type: array + items: + type: integer + format: int64 + responses: + "202": + description: Accepted + /path8: + post: + operationId: postPath8 + requestBody: + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int64 + text/plain: + schema: + type: string + responses: + "202": + description: Accepted + /path9: + post: + operationId: postPath9 + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + additionalProperties: + type: integer + format: int64 + - type: object + additionalProperties: + type: string + responses: + "202": + description: Accepted +components: + schemas: + ABC: + required: + - id + - name + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Remote: + required: + - host + - ip + - port + type: object + properties: + host: + type: string + port: + type: integer + format: int64 + ip: + type: string + description: Presents a read-only view of the remote address. diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/request_body/union_type.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/request_body/union_type.bal new file mode 100644 index 000000000..565da847d --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/request_body/union_type.bal @@ -0,0 +1,40 @@ +import ballerina/http; + +public type ABC record { + int id; + string name; +}; +type UnionType ABC|xml; + +service /payloadV on new http:Listener(0) { + resource function post path(ABC? payload) { + } + //This needs to be fixed with separated PR + resource function post path1(UnionType payload) { + } + resource function post path2(ABC|xml payload) { + } + resource function post path3(ABC|http:Remote|xml? payload) { + } + resource function post path4(@http:Payload ABC|string|xml payload) { + } + //oneOF- scenarios for application/json + resource function post path5(@http:Payload ABC|int payload) { + } + resource function post path6(@http:Payload int|string payload) { + } + resource function post path7(@http:Payload map|int[] payload) { + } + resource function post path8(@http:Payload map|string payload) { + } + resource function post path9(@http:Payload map|map payload) { + } + + // //negative - skip + // resource function post path10(ABC|string payload) { + // } + // resource function post path11(int|string payload) { + // } + // resource function post path12(map|string payload) { + // } +}