diff --git a/gradle.properties b/gradle.properties index 45e4fbf10..5210439f2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,9 @@ org.gradle.caching=true group=io.ballerina -version=1.8.0-SNAPSHOT +version=1.8.1-SNAPSHOT #dependency -ballerinaLangVersion=2201.8.0-20230816-121900-c1174ddd +ballerinaLangVersion=2201.8.0 testngVersion=7.6.1 slf4jVersion=1.7.30 org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 @@ -15,41 +15,41 @@ swaggerParserVersion=2.1.16 puppycrawlCheckstyleVersion = 10.12.1 # Stdlib Level 01 -stdlibIoVersion=1.5.0 +stdlibIoVersion=1.6.0 stdlibRegexVersion=1.4.3 -stdlibTimeVersion=2.3.0 -stdlibUrlVersion=2.3.0 -stdlibXmldataVersion=2.6.0 +stdlibTimeVersion=2.4.0 +stdlibUrlVersion=2.4.0 +stdlibXmldataVersion=2.7.0 # Stdlib Level 02 -stdlibConstraintVersion=1.3.0 -stdlibCryptoVersion=2.4.0 -stdlibLogVersion=2.8.1-20230718-085900-36c385c -stdlibOsVersion=1.7.0 -stdlibTaskVersion=2.4.0 +stdlibConstraintVersion=1.4.0 +stdlibCryptoVersion=2.5.0 +stdlibLogVersion=2.9.0 +stdlibOsVersion=1.8.0 +stdlibTaskVersion=2.5.0 # Stdlib Level 03 -stdlibCacheVersion=3.6.0 -stdlibFileVersion=1.8.0 -stdlibMimeVersion=2.8.0 -stdlibUuidVersion=1.6.0 +stdlibCacheVersion=3.7.0 +stdlibFileVersion=1.9.0 +stdlibMimeVersion=2.9.0 +stdlibUuidVersion=1.7.0 # Stdlib Level 04 -stdlibAuthVersion=2.9.0 -stdlibJwtVersion=2.9.0 -stdlibOAuth2Version=2.9.0 +stdlibAuthVersion=2.10.0 +stdlibJwtVersion=2.10.0 +stdlibOAuth2Version=2.10.0 # Stdlib Level 05 -stdlibHttpVersion=2.10.0-20230809-150400-0c9cad9 +stdlibHttpVersion=2.10.0 # Stdlib Level 06 -stdlibGrpcVersion=1.9.1-20230809-211700-feffbef -stdlibWebsocketVersion=2.9.1-20230809-174700-6942521 -stdlibWebsubVersion=2.9.1-20230809-211400-c6c75d2 +stdlibGrpcVersion=1.10.0 +stdlibWebsocketVersion=2.10.0 +stdlibWebsubVersion=2.10.0 # Stdlib Level 07 -stdlibGraphqlVersion=1.10.0-20230809-214800-d775b0d +stdlibGraphqlVersion=1.10.0 # Ballerinax Observer -observeVersion=1.1.0 -observeInternalVersion=1.1.0 +observeVersion=1.2.0 +observeInternalVersion=1.2.0 diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/Constants.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/Constants.java index 88f63b674..310ccc379 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/Constants.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/Constants.java @@ -189,7 +189,7 @@ public String toString() { httpCodeMap.put("NotImplemented", "501"); httpCodeMap.put("BadGateway", "502"); httpCodeMap.put("ServiceUnavailable", "503"); - httpCodeMap.put("GatewayTimeOut", "504"); + httpCodeMap.put("GatewayTimeout", "504"); httpCodeMap.put("HttpVersionNotSupported", "505"); httpCodeMap.put("VariantAlsoNegotiates", "506"); httpCodeMap.put("InsufficientStorage", "507"); 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 a09dbeca0..e7bacf184 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 @@ -64,7 +64,10 @@ public enum DiagnosticMessages { OAS_CONVERTOR_115("OAS_CONVERTOR_115", "Given Ballerina file does not contain any HTTP service.", DiagnosticSeverity.ERROR), OAS_CONVERTOR_116("OAS_CONVERTOR_116", "Failed to parser the Number value due to: %s ", - DiagnosticSeverity.ERROR); + DiagnosticSeverity.ERROR), + OAS_CONVERTOR_117("OAS_CONVERTOR_117", "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 2478629d7..e12b1dfba 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; private final ModuleMemberVisitor moduleMemberVisitor; @@ -88,7 +98,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<>(); this.moduleMemberVisitor = moduleMemberVisitor; } @@ -142,8 +152,7 @@ public void handlePayloadAnnotation(RequiredParameterNode payloadNode, Map */ - private void handleSinglePayloadType(RequiredParameterNode payloadNode, Map schema, - RequestBody bodyParameter, String customMediaPrefix) { - - String consumes = payloadNode.typeName().toString().trim(); - String mediaTypeString; - switch (consumes) { - case Constants.JSON: - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_JSON : - APPLICATION_PREFIX + customMediaPrefix + JSON_POSTFIX; - addConsumes(operationAdaptor, bodyParameter, mediaTypeString); - break; - case Constants.XML: - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_XML : - APPLICATION_PREFIX + customMediaPrefix + XML_POSTFIX; - addConsumes(operationAdaptor, bodyParameter, mediaTypeString); + private void handlePayloadType(TypeDescriptorNode payloadNode, Map schema, + RequestBody bodyParameter) { + SyntaxKind kind = payloadNode.kind(); + String mediaTypeString = getMediaTypeForSyntaxKind(payloadNode); + if (!isTypeDeclarationExtractable(kind, mediaTypeString)) { + //Warning message for unsupported request payload type in Ballerina resource. + IncompatibleResourceDiagnostic error = new IncompatibleResourceDiagnostic( + DiagnosticMessages.OAS_CONVERTOR_117, payloadNode.location(), payloadNode.toSourceCode().trim()); + diagnostics.add(error); + return; + } + 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 Constants.STRING: + case STRING_TYPE_DESC: mediaTypeString = 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 : - APPLICATION_PREFIX + customMediaPrefix + OCTECT_STREAM_POSTFIX; - addConsumes(operationAdaptor, bodyParameter, mediaTypeString); + addConsumes(operationAdaptor, bodyParameter, mediaTypeString, kind); break; - case Constants.MAP_STRING: - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_JSON : - APPLICATION_PREFIX + customMediaPrefix + JSON_POSTFIX; + case MAP_TYPE_DESC: Schema objectSchema = new ObjectSchema(); - objectSchema.additionalProperties(new StringSchema()); + 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(); - mediaType.setSchema(objectSchema); - bodyParameter.setContent(new Content().addMediaType(mediaTypeString, 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: - Node node = payloadNode.typeName(); - mediaTypeString = customMediaPrefix == null ? MediaType.APPLICATION_JSON : + //Warning message for unsupported request payload type in Ballerina resource. + IncompatibleResourceDiagnostic error = new IncompatibleResourceDiagnostic( + DiagnosticMessages.OAS_CONVERTOR_117, payloadNode.location(), + payloadNode.toSourceCode().trim()); + diagnostics.add(error); + break; + } + } + + private static boolean isTypeDeclarationExtractable(SyntaxKind kind, String mediaTypeString) { + return mediaTypeString != null || kind == SyntaxKind.UNION_TYPE_DESC || + kind == SyntaxKind.QUALIFIED_NAME_REFERENCE || kind == SyntaxKind.OPTIONAL_TYPE_DESC || + kind == SyntaxKind.ARRAY_TYPE_DESC; + } + + /** + * 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; - 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); + case XML_TYPE_DESC: + return customMediaPrefix == null ? MediaType.APPLICATION_XML : + APPLICATION_PREFIX + customMediaPrefix + XML_POSTFIX; + case STRING_TYPE_DESC: + return customMediaPrefix == null ? MediaType.TEXT_PLAIN : + TEXT_PREFIX + customMediaPrefix + TEXT_POSTFIX; + case BYTE_TYPE_DESC: + return customMediaPrefix == null ? MediaType.APPLICATION_OCTET_STREAM : + APPLICATION_PREFIX + customMediaPrefix + OCTECT_STREAM_POSTFIX; + 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; + } + 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; + 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: + // Default return the null and the warning will be added in the handlePayloadType function. break; } + return null; } private TypeSymbol getReferenceTypeSymbol(Optional symbol) { @@ -226,6 +351,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; @@ -236,19 +362,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); } } @@ -260,7 +403,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); @@ -282,8 +425,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, moduleMemberVisitor); componentMapper.createComponentSchema(schema, typeSymbol); @@ -292,23 +435,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 8153cb89a..44d0a92ca 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; @@ -217,8 +218,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/main/java/io/ballerina/openapi/cmd/BallerinaCodeGenerator.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/BallerinaCodeGenerator.java index 61c195de5..cbbcca05e 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/BallerinaCodeGenerator.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/BallerinaCodeGenerator.java @@ -62,6 +62,7 @@ import static io.ballerina.openapi.cmd.CmdConstants.GenType.GEN_CLIENT; import static io.ballerina.openapi.cmd.CmdConstants.GenType.GEN_SERVICE; import static io.ballerina.openapi.cmd.CmdConstants.OAS_PATH_SEPARATOR; +import static io.ballerina.openapi.cmd.CmdConstants.SUPPORTED_OPENAPI_VERSIONS; import static io.ballerina.openapi.cmd.CmdConstants.TEST_DIR; import static io.ballerina.openapi.cmd.CmdConstants.TEST_FILE_NAME; import static io.ballerina.openapi.cmd.CmdConstants.TYPE_FILE_NAME; @@ -103,7 +104,7 @@ public void generateClientAndService(String definitionPath, String serviceName, // absence of the operationId in operation. Therefor we enable client flag true as default code generation. // if resource is enabled, we avoid checking operationId. OpenAPI openAPIDef = GeneratorUtils.normalizeOpenAPI(openAPIPath, !isResource); - + checkOpenAPIVersion(openAPIDef); // Generate service String concatTitle = serviceName.toLowerCase(Locale.ENGLISH); String srcFile = concatTitle + "_service.bal"; @@ -338,6 +339,7 @@ private List generateClientFiles(Path openAPI, Filter filter, boolea List sourceFiles = new ArrayList<>(); // Normalize OpenAPI definition OpenAPI openAPIDef = GeneratorUtils.normalizeOpenAPI(openAPI, !isResource); + checkOpenAPIVersion(openAPIDef); // Generate ballerina service and resources. OASClientConfig.Builder clientMetaDataBuilder = new OASClientConfig.Builder(); OASClientConfig oasClientConfig = clientMetaDataBuilder @@ -409,6 +411,8 @@ public List generateBallerinaService(Path openAPI, String serviceNam openAPI); } + checkOpenAPIVersion(openAPIDef); + if (openAPIDef.getInfo().getTitle().isBlank() && (serviceName == null || serviceName.isBlank())) { openAPIDef.getInfo().setTitle(UNTITLED_SERVICE); } else { @@ -469,4 +473,11 @@ public void setLicenseHeader(String licenseHeader) { public void setIncludeTestFiles(boolean includeTestFiles) { this.includeTestFiles = includeTestFiles; } + + private void checkOpenAPIVersion(OpenAPI openAPIDef) { + if (!SUPPORTED_OPENAPI_VERSIONS.contains(openAPIDef.getOpenapi())) { + outStream.printf("WARNING: The tool has not been tested with OpenAPI version %s. " + + "The generated code may potentially contain errors.%n", openAPIDef.getOpenapi()); + } + } } diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java index d3bdd4224..04cc65396 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -169,7 +170,7 @@ public String getValue() { httpCodeMap.put("501", "NotImplemented"); httpCodeMap.put("502", "BadGateway"); httpCodeMap.put("503", "ServiceUnavailable"); - httpCodeMap.put("504", "GatewayTimeOut"); + httpCodeMap.put("504", "GatewayTimeout"); httpCodeMap.put("505", "HttpVersionNotSupported"); HTTP_CODES_DES = Collections.unmodifiableMap(httpCodeMap); } @@ -201,5 +202,6 @@ public String getValue() { // OS specific line separator public static final String LINE_SEPARATOR = System.lineSeparator(); public static final String DOUBLE_LINE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR; - + public static final List SUPPORTED_OPENAPI_VERSIONS = + List.of("2.0", "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0"); } diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/client/BallerinaDiagnosticTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/client/BallerinaDiagnosticTests.java index bcd5c06bb..95cd3b862 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/client/BallerinaDiagnosticTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/client/BallerinaDiagnosticTests.java @@ -151,6 +151,7 @@ public Object[][] singleFileProviderForDiagnosticCheck() { {"duplicated_response.yaml"}, {"complex_oneOf_schema.yaml"}, {"request_body_ref.yaml"}, + {"single_allOf.yaml"}, {"vendor_specific_mime_types.yaml"}, {"requestBody_reference_has_inline_object_content_type.yaml"}, {"ballerinax_connector_tests/ably.yaml"}, diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/client/PathParameterTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/client/PathParameterTests.java index 0f3eb02fc..65ea5a6e5 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/client/PathParameterTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/client/PathParameterTests.java @@ -164,7 +164,8 @@ public void unionPathParameter() throws IOException, BallerinaOpenApiException { @Test(description = "When path parameter has given allOf data type in ballerina", expectedExceptions = BallerinaOpenApiException.class, - expectedExceptionsMessageRegExp = "Ballerina does not support object type path .*") + expectedExceptionsMessageRegExp = "Path parameter: 'id' is invalid. " + + "Ballerina does not support object type path parameters.") public void allOfPathParameter() throws IOException, BallerinaOpenApiException { Path definitionPath = RESDIR.resolve("swagger/allOf_path_parameter.yaml"); OpenAPI openAPI = GeneratorUtils.normalizeOpenAPI(definitionPath, true); diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/common/TestUtils.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/common/TestUtils.java index ccc1d97e4..5db762c11 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/common/TestUtils.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/common/TestUtils.java @@ -99,13 +99,6 @@ public static List getDiagnostics(SyntaxTree syntaxTree) throws Form return semanticModel.diagnostics(); } - public static List getDiagnosticsForGenericService(SyntaxTree serviceSyntaxTree) - throws FormatterException, IOException { - writeFile(servicePath, Formatter.format(serviceSyntaxTree).toSourceCode()); - SemanticModel semanticModel = getSemanticModel(servicePath); - return semanticModel.diagnostics(); - } - public static List getDiagnosticsForService(SyntaxTree serviceSyntaxTree, OpenAPI openAPI, BallerinaServiceGenerator ballerinaServiceGenerator) throws FormatterException, IOException, BallerinaOpenApiException { 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/java/io/ballerina/openapi/generators/schema/AllOfDataTypeTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/schema/AllOfDataTypeTests.java index 1b9590d96..46165238c 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/schema/AllOfDataTypeTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/schema/AllOfDataTypeTests.java @@ -114,6 +114,16 @@ public void generateNestedAllOfSchema() throws IOException, BallerinaOpenApiExce TestUtils.compareGeneratedSyntaxTreewithExpectedSyntaxTree("schema/ballerina/nested_all_of.bal", syntaxTree); } + @Test(description = "Generate type definition from allOf schema with valid single item") + public void generateAllOfwithValidSingleItem() throws IOException, BallerinaOpenApiException { + Path definitionPath = RES_DIR.resolve("swagger/single_item_allOf.yaml"); + OpenAPI openAPI = GeneratorUtils.normalizeOpenAPI(definitionPath, true); + BallerinaTypesGenerator ballerinaSchemaGenerator = new BallerinaTypesGenerator(openAPI); + syntaxTree = ballerinaSchemaGenerator.generateSyntaxTree(); + TestUtils.compareGeneratedSyntaxTreewithExpectedSyntaxTree( + "schema/ballerina/single_item_allOf.bal", syntaxTree); + } + @Test(description = "Tests record generation for nested OneOf schema inside AllOf schema", expectedExceptions = BallerinaOpenApiException.class, expectedExceptionsMessageRegExp = diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/service/ParameterGeneratorTest.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/service/ParameterGeneratorTest.java index f196e0489..503bc270a 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/service/ParameterGeneratorTest.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/service/ParameterGeneratorTest.java @@ -262,5 +262,18 @@ public void testsRefParamsInOpenAPIV31() throws IOException, BallerinaOpenApiExc CommonTestFunctions.compareGeneratedSyntaxTreewithExpectedSyntaxTree( "parameter_with_ref_v31.bal", syntaxTree); } -} + @Test(description = "Tests for path segments has parameters with extension") + public void testsForPathSegmentHasExtensionType() throws IOException, BallerinaOpenApiException { + Path definitionPath = RES_DIR.resolve("swagger/multiPathParamWithExtensionType.yaml"); + OpenAPI openAPI = GeneratorUtils.getOpenAPIFromOpenAPIV3Parser(definitionPath); + OASServiceMetadata oasServiceMetadata = new OASServiceMetadata.Builder() + .withOpenAPI(openAPI) + .withFilters(filter) + .build(); + BallerinaServiceGenerator ballerinaServiceGenerator = new BallerinaServiceGenerator(oasServiceMetadata); + syntaxTree = ballerinaServiceGenerator.generateSyntaxTree(); + CommonTestFunctions.compareGeneratedSyntaxTreewithExpectedSyntaxTree( + "multiPathParamWithExtensionType.bal", syntaxTree); + } +} diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/service/ServiceDiagnosticTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/service/ServiceDiagnosticTests.java index 374524eb6..a69be9844 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/service/ServiceDiagnosticTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/service/ServiceDiagnosticTests.java @@ -41,7 +41,6 @@ import java.util.ArrayList; import java.util.List; -import static io.ballerina.openapi.generators.common.TestUtils.getDiagnosticsForGenericService; import static io.ballerina.openapi.generators.common.TestUtils.getDiagnosticsForService; import static io.ballerina.openapi.generators.common.TestUtils.normalizeOpenAPI; @@ -92,7 +91,7 @@ public void checkDiagnosticIssuesInGenericServiceGen(String yamlFile) throws IOE .build(); BallerinaServiceGenerator ballerinaServiceGenerator = new BallerinaServiceGenerator(oasServiceMetadata); syntaxTree = ballerinaServiceGenerator.generateSyntaxTree(); - List diagnostics = getDiagnosticsForGenericService(syntaxTree); + List diagnostics = getDiagnosticsForService(syntaxTree, openAPI, ballerinaServiceGenerator); boolean hasErrors = diagnostics.stream() .anyMatch(d -> DiagnosticSeverity.ERROR.equals(d.diagnosticInfo().severity())); Assert.assertFalse(hasErrors); @@ -137,6 +136,7 @@ public Object[][] singleFileProviderForDiagnosticCheck() { {"complex_oneOf_schema.yaml"}, {"request_body_ref.yaml"}, {"vendor_specific_mime_types.yaml"}, + {"single_allOf.yaml"}, // TODO: Uncomment when fixed https://github.com/ballerina-platform/openapi-tools/issues/1415 // {"ballerinax_connector_tests/ably.yaml"}, {"ballerinax_connector_tests/azure.iot.yaml"}, 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) { + // } +} diff --git a/openapi-cli/src/test/resources/generators/diagnostic_files/single_allOf.yaml b/openapi-cli/src/test/resources/generators/diagnostic_files/single_allOf.yaml new file mode 100644 index 000000000..de56207c6 --- /dev/null +++ b/openapi-cli/src/test/resources/generators/diagnostic_files/single_allOf.yaml @@ -0,0 +1,54 @@ +openapi: 3.0.1 +info: + title: Sample API + description: API description in Markdown. + version: 1.0.0 +servers: + - url: https://api.example.com +paths: + /users/{id}: + get: + summary: Returns a list of users. + operationId: getUserById + description: Optional extended description in Markdown. + parameters: + - name: id + in: path + description: ID of user to fetch + required: true + schema: + allOf: + - $ref: "#/components/schemas/Name" + - description: "Name details" + responses: + "200": + description: OK +components: + schemas: + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + email: + type: string + name: + type: string + address: + allOf: + - $ref: "#/components/schemas/Address" + - description: "abc" + xml: + name: User + Address: + type: object + properties: + street: + type: string + city: + type: string + Name: + type: string diff --git a/openapi-cli/src/test/resources/generators/schema/ballerina/single_item_allOf.bal b/openapi-cli/src/test/resources/generators/schema/ballerina/single_item_allOf.bal new file mode 100644 index 000000000..f1ae70a86 --- /dev/null +++ b/openapi-cli/src/test/resources/generators/schema/ballerina/single_item_allOf.bal @@ -0,0 +1,19 @@ +public type User record { + int id?; + string username?; + string email?; + string name?; + Address address?; + anydata remarks?; +}; + +public type Description anydata; + +public type Address record { + string street?; + string city?; +}; + +public type Id Name; + +public type Name string; \ No newline at end of file diff --git a/openapi-cli/src/test/resources/generators/schema/swagger/single_item_allOf.yaml b/openapi-cli/src/test/resources/generators/schema/swagger/single_item_allOf.yaml new file mode 100644 index 000000000..b1c8a675d --- /dev/null +++ b/openapi-cli/src/test/resources/generators/schema/swagger/single_item_allOf.yaml @@ -0,0 +1,65 @@ +openapi: 3.0.1 +info: + title: Sample API + description: API description in Markdown. + version: 1.0.0 +servers: + - url: https://api.example.com +paths: + /users/{id}: + get: + summary: Returns a list of users. + operationId: getUserById + description: Optional extended description in Markdown. + parameters: + - name: id + in: path + description: ID of user to fetch + required: true + schema: + allOf: + - $ref: "#/components/schemas/Name" + - description: "Name details" + - name: description + in: query + required: true + schema: + allOf: + - description: "Explain the user" + - description: "Name details" + responses: + "200": + description: OK +components: + schemas: + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + email: + type: string + name: + type: string + address: + allOf: + - $ref: "#/components/schemas/Address" + - description: "abc" + remarks: + allOf: + - description: "User status" + - description: "User data" + xml: + name: User + Address: + type: object + properties: + street: + type: string + city: + type: string + Name: + type: string diff --git a/openapi-cli/src/test/resources/generators/service/ballerina/multiPathParamWithExtensionType.bal b/openapi-cli/src/test/resources/generators/service/ballerina/multiPathParamWithExtensionType.bal new file mode 100644 index 000000000..00ed9830b --- /dev/null +++ b/openapi-cli/src/test/resources/generators/service/ballerina/multiPathParamWithExtensionType.bal @@ -0,0 +1,29 @@ +import ballerina/http; + +listener http:Listener ep0 = new (80, config = {host: "petstore.openapi.io"}); + +service /v1 on ep0 { + # Info for a specific pet + # + # + spreadsheetId - The id of the pet to retrieve + # + sheetidCopyto - The id of the pet to retrieve + # + return - returns can be any of following types + # http:Ok (Expected response to a valid request) + # http:Response (unexpected error) + resource function get v4/spreadsheets/[int spreadsheetId]/sheets/[string sheetidCopyto]() returns http:Ok|http:Response|error { + if !sheetidCopyto.endsWith(":copyTo") { + return error("bad URL"); + } + string sheetId = sheetidCopyto.substring(0, sheetidCopyto.length() - 6); + } + # Get the details of the specified field + # + # + idJson - Field ID + # + return - Successful response + resource function get 'field/[string idJson]() returns http:Ok|error { + if !idJson.endsWith(".json") { + return error("bad URL"); + } + string id = idJson.substring(0, idJson.length() - 4); + } +} diff --git a/openapi-cli/src/test/resources/generators/service/swagger/multiPathParamWithExtensionType.yaml b/openapi-cli/src/test/resources/generators/service/swagger/multiPathParamWithExtensionType.yaml new file mode 100644 index 000000000..12f0b4b28 --- /dev/null +++ b/openapi-cli/src/test/resources/generators/service/swagger/multiPathParamWithExtensionType.yaml @@ -0,0 +1,92 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: OpenApi Petstore + license: + name: MIT +servers: + - url: http://petstore.{host}.io/v1 + description: The production API server + variables: + host: + default: openapi + description: this value is assigned by the service provider + +tags: + - name: pets + description: Pets Tag + - name: list + description: List Tag + +paths: + /v4/spreadsheets/{spreadsheetId}/sheets/{sheetId}:copyTo: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: spreadsheetId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: integer + - name: sheetId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: integer + responses: + '200': + description: Expected response to a valid request + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /field/{id}.json: + get: + tags: + - Field + operationId: getFieldById + description: Get the details of the specified field + parameters: + - description: Field ID + in: path + name: id + schema: + type: number + format: double + required: true + responses: + '200': + description: Successful response +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + type: + type: string + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/GeneratorUtils.java b/openapi-core/src/main/java/io/ballerina/openapi/core/GeneratorUtils.java index 3dcaee768..dc0facb62 100644 --- a/openapi-core/src/main/java/io/ballerina/openapi/core/GeneratorUtils.java +++ b/openapi-core/src/main/java/io/ballerina/openapi/core/GeneratorUtils.java @@ -292,7 +292,7 @@ private static void extractPathParameterDetails(Operation operation, List // TypeDescriptor BuiltinSimpleNameReferenceNode builtSNRNode = createBuiltinSimpleNameReferenceNode( null, - parameter.getSchema() == null ? + parameter.getSchema() == null || hasSpecialCharacter ? createIdentifierToken(STRING) : createIdentifierToken(paramType)); IdentifierToken paramName = createIdentifierToken( diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/FunctionSignatureGenerator.java b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/FunctionSignatureGenerator.java index 06fb30532..04453cc3a 100644 --- a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/FunctionSignatureGenerator.java +++ b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/FunctionSignatureGenerator.java @@ -33,6 +33,7 @@ import io.ballerina.compiler.syntax.tree.ReturnTypeDescriptorNode; 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.Token; import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; @@ -376,8 +377,12 @@ public Node getPathParameters(Parameter parameter, NodeList para if (parameterSchema.get$ref() != null) { type = getValidName(extractReferenceType(parameterSchema.get$ref()), true); Schema schema = openAPI.getComponents().getSchemas().get(type.trim()); - if (isObjectSchema(schema) || (isComposedSchema(schema) && schema.getAllOf() != null)) { - throw new BallerinaOpenApiException("Ballerina does not support object type path parameters."); + TypeDefinitionNode typeDefinitionNode = ballerinaSchemaGenerator.getTypeDefinitionNode + (schema, type, new ArrayList<>()); + if (typeDefinitionNode.typeDescriptor().kind().equals(SyntaxKind.RECORD_TYPE_DESC)) { + throw new BallerinaOpenApiException(String.format( + "Path parameter: '%s' is invalid. Ballerina does not support object type path parameters.", + parameter.getName())); } } else { type = convertOpenAPITypeToBallerina(parameter.getSchema()); diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/schema/ballerinatypegenerators/AllOfRecordTypeGenerator.java b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/schema/ballerinatypegenerators/AllOfRecordTypeGenerator.java index ea015d315..91c4179b0 100644 --- a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/schema/ballerinatypegenerators/AllOfRecordTypeGenerator.java +++ b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/schema/ballerinatypegenerators/AllOfRecordTypeGenerator.java @@ -33,6 +33,7 @@ import io.ballerina.openapi.core.generators.schema.model.RecordMetadata; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.Schema; +import org.apache.commons.lang3.tuple.ImmutablePair; import java.util.ArrayList; import java.util.LinkedList; @@ -91,7 +92,7 @@ public TypeDescriptorNode generateTypeDescriptorNode() throws BallerinaOpenApiEx // This assertion is always `true` because this type generator receive ComposedSchema during the upper level // filtering as input. Has to use this assertion statement instead of `if` condition, because to avoid // unreachable else statement. - List allOfSchemas = schema.getAllOf(); + List> allOfSchemas = schema.getAllOf(); RecordMetadata recordMetadata = getRecordMetadata(); RecordRestDescriptorNode restDescriptorNode = recordMetadata.getRestDescriptorNode(); @@ -100,23 +101,37 @@ public TypeDescriptorNode generateTypeDescriptorNode() throws BallerinaOpenApiEx typeName); return referencedTypeGenerator.generateTypeDescriptorNode(); } else { - List recordFieldList = generateAllOfRecordFields(allOfSchemas); - addAdditionalSchemas(schema); - restDescriptorNode = - restSchemas.size() > 1 ? getRestDescriptorNodeForAllOf(restSchemas) : restDescriptorNode; - - NodeList fieldNodes = AbstractNodeFactory.createNodeList(recordFieldList); - return NodeFactory.createRecordTypeDescriptorNode(createToken(RECORD_KEYWORD), - recordMetadata.isOpenRecord() ? createToken(OPEN_BRACE_TOKEN) : createToken(OPEN_BRACE_PIPE_TOKEN), - fieldNodes, restDescriptorNode, - recordMetadata.isOpenRecord() ? createToken(CLOSE_BRACE_TOKEN) : - createToken(CLOSE_BRACE_PIPE_TOKEN)); + ImmutablePair, List>> recordFlist = generateAllOfRecordFields(allOfSchemas); + List recordFieldList = recordFlist.getLeft(); + List> validSchemas = recordFlist.getRight(); + if (validSchemas.isEmpty()) { + AnyDataTypeGenerator anyDataTypeGenerator = new AnyDataTypeGenerator(schema, typeName); + return anyDataTypeGenerator.generateTypeDescriptorNode(); + } else if (validSchemas.size() == 1) { + TypeGenerator typeGenerator = getTypeGenerator(validSchemas.get(0), typeName, null); + return typeGenerator.generateTypeDescriptorNode(); + } else { + addAdditionalSchemas(schema); + restDescriptorNode = + restSchemas.size() > 1 ? getRestDescriptorNodeForAllOf(restSchemas) : restDescriptorNode; + + NodeList fieldNodes = AbstractNodeFactory.createNodeList(recordFieldList); + return NodeFactory.createRecordTypeDescriptorNode(createToken(RECORD_KEYWORD), + recordMetadata.isOpenRecord() ? + createToken(OPEN_BRACE_TOKEN) : createToken(OPEN_BRACE_PIPE_TOKEN), + fieldNodes, restDescriptorNode, + recordMetadata.isOpenRecord() ? createToken(CLOSE_BRACE_TOKEN) : + createToken(CLOSE_BRACE_PIPE_TOKEN)); + } } } - private List generateAllOfRecordFields(List allOfSchemas) throws BallerinaOpenApiException { + private ImmutablePair, List>> generateAllOfRecordFields(List> allOfSchemas) + throws BallerinaOpenApiException { List recordFieldList = new ArrayList<>(); + List> validSchemas = new ArrayList<>(); + for (Schema allOfSchema : allOfSchemas) { if (allOfSchema.get$ref() != null) { String extractedSchemaName = GeneratorUtils.extractReferenceType(allOfSchema.get$ref()); @@ -137,15 +152,22 @@ private List generateAllOfRecordFields(List allOfSchemas) throws B addAdditionalSchemas(allOfSchema); } else if (GeneratorUtils.isComposedSchema(allOfSchema)) { if (allOfSchema.getAllOf() != null) { - recordFieldList.addAll(generateAllOfRecordFields(allOfSchema.getAllOf())); + ImmutablePair, List>> immutablePair = + generateAllOfRecordFields(allOfSchema.getAllOf()); + List recordAllFields = (List) immutablePair.getLeft(); + recordFieldList.addAll(recordAllFields); } else { // TODO: Needs to improve the error message. Could not access the schema name at this level. throw new BallerinaOpenApiException( "Unsupported nested OneOf or AnyOf schema is found inside a AllOf schema."); } } + if (allOfSchema.getType() != null || allOfSchema.getProperties() != null || allOfSchema.get$ref() != null + || allOfSchema.getAllOf() != null) { + validSchemas.add(allOfSchema); + } } - return recordFieldList; + return ImmutablePair.of(recordFieldList, validSchemas); } /** diff --git a/openapi-validator/src/main/java/io/ballerina/openapi/validator/Constants.java b/openapi-validator/src/main/java/io/ballerina/openapi/validator/Constants.java index 04b27c55d..5d91220d2 100644 --- a/openapi-validator/src/main/java/io/ballerina/openapi/validator/Constants.java +++ b/openapi-validator/src/main/java/io/ballerina/openapi/validator/Constants.java @@ -131,7 +131,7 @@ public class Constants { httpCodes.put("NotImplemented", "501"); httpCodes.put("BadGateway", "502"); httpCodes.put("ServiceUnavailable", "503"); - httpCodes.put("GatewayTimeOut", "504"); + httpCodes.put("GatewayTimeout", "504"); httpCodes.put("HttpVersionNotSupported", "505"); httpCodes.put("VariantAlsoNegotiates", "506"); httpCodes.put("InsufficientStorage", "507"); diff --git a/openapi-validator/src/test/resources/return/response_code.yaml b/openapi-validator/src/test/resources/return/response_code.yaml index 07c756f2a..78bfd446e 100644 --- a/openapi-validator/src/test/resources/return/response_code.yaml +++ b/openapi-validator/src/test/resources/return/response_code.yaml @@ -94,6 +94,8 @@ paths: description: BadGateway "503": description: ServiceUnavailable + "504": + description: GatewayTimeout "505": description: HttpVersionNotSupported components: {}