From ba7660031dfd17f52da45a3796d4b8b798e35a5e Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Thu, 1 Feb 2024 20:38:00 +0530 Subject: [PATCH 01/12] Add support for status code error responses --- .../openapi/service/mapper/Constants.java | 45 +++++++ .../mapper/response/ResponseMapperImpl.java | 110 ++++++++++++------ .../mapper/type/ReferenceTypeMapper.java | 16 +++ .../service/mapper/type/TypeMapper.java | 3 + .../service/mapper/type/TypeMapperImpl.java | 4 + 5 files changed, 141 insertions(+), 37 deletions(-) diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java index 187d39a0c..20e36fee3 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java @@ -180,6 +180,51 @@ public String toString() { // TODO: remove this after fixing https://github.com/ballerina-platform/ballerina-standard-library/issues/4245 HTTP_CODES = Collections.unmodifiableMap(httpCodeMap); } + public static final Map HTTP_STATUS_CODE_ERRORS; + static { + Map httpErrorCodeMap = new HashMap<>(); + httpErrorCodeMap.put("DefaultStatusCodeError", "default"); + httpErrorCodeMap.put("BadRequestError", "400"); + httpErrorCodeMap.put("UnauthorizedError", "401"); + httpErrorCodeMap.put("PaymentRequiredError", "402"); + httpErrorCodeMap.put("ForbiddenError", "403"); + httpErrorCodeMap.put("NotFoundError", "404"); + httpErrorCodeMap.put("MethodNotAllowedError", "405"); + httpErrorCodeMap.put("NotAcceptableError", "406"); + httpErrorCodeMap.put("ProxyAuthenticationRequiredError", "407"); + httpErrorCodeMap.put("RequestTimeOutError", "408"); + httpErrorCodeMap.put("ConflictError", "409"); + httpErrorCodeMap.put("GoneError", "410"); + httpErrorCodeMap.put("LengthRequiredError", "411"); + httpErrorCodeMap.put("PreconditionFailedError", "412"); + httpErrorCodeMap.put("PayloadTooLargeError", "413"); + httpErrorCodeMap.put("UriTooLongError", "414"); + httpErrorCodeMap.put("UnsupportedMediaTypeError", "415"); + httpErrorCodeMap.put("RangeNotSatisfiableError", "416"); + httpErrorCodeMap.put("ExpectationFailedError", "417"); + httpErrorCodeMap.put("MisdirectedRequestError", "421"); + httpErrorCodeMap.put("UnprocessableEntityError", "422"); + httpErrorCodeMap.put("LockedError", "423"); + httpErrorCodeMap.put("FailedDependencyError", "424"); + httpErrorCodeMap.put("TooEarlyError", "425"); + httpErrorCodeMap.put("UpgradeRequiredError", "426"); + httpErrorCodeMap.put("PreconditionRequiredError", "428"); + httpErrorCodeMap.put("TooManyRequestsError", "429"); + httpErrorCodeMap.put("RequestHeaderFieldsTooLargeError", "431"); + httpErrorCodeMap.put("UnavailableDueToLegalReasonsError", "451"); + httpErrorCodeMap.put("InternalServerErrorError", "500"); + httpErrorCodeMap.put("NotImplementedError", "501"); + httpErrorCodeMap.put("BadGatewayError", "502"); + httpErrorCodeMap.put("ServiceUnavailableError", "503"); + httpErrorCodeMap.put("GatewayTimeoutError", "504"); + httpErrorCodeMap.put("HttpVersionNotSupportedError", "505"); + httpErrorCodeMap.put("VariantAlsoNegotiatesError", "506"); + httpErrorCodeMap.put("InsufficientStorageError", "507"); + httpErrorCodeMap.put("LoopDetectedError", "508"); + httpErrorCodeMap.put("NotExtendedError", "510"); + httpErrorCodeMap.put("NetworkAuthenticationRequiredError", "511"); + HTTP_STATUS_CODE_ERRORS = Collections.unmodifiableMap(httpErrorCodeMap); + } public static final Map HTTP_CODE_DESCRIPTIONS; static { Map httpCodeDescriptionMap = new HashMap<>(); diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java index 91036fe9c..07beaf278 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java @@ -19,6 +19,7 @@ import io.ballerina.compiler.api.SemanticModel; import io.ballerina.compiler.api.symbols.ClassSymbol; +import io.ballerina.compiler.api.symbols.ErrorTypeSymbol; import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; import io.ballerina.compiler.api.symbols.RecordFieldSymbol; import io.ballerina.compiler.api.symbols.RecordTypeSymbol; @@ -66,6 +67,7 @@ import static io.ballerina.openapi.service.mapper.Constants.HTTP_CODES; import static io.ballerina.openapi.service.mapper.Constants.HTTP_CODE_DESCRIPTIONS; import static io.ballerina.openapi.service.mapper.Constants.HTTP_PAYLOAD; +import static io.ballerina.openapi.service.mapper.Constants.HTTP_STATUS_CODE_ERRORS; import static io.ballerina.openapi.service.mapper.Constants.MEDIA_TYPE; import static io.ballerina.openapi.service.mapper.Constants.POST; import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.extractAnnotationFieldDetails; @@ -273,17 +275,25 @@ private void addResponseMappingForSimpleType(TypeSymbol returnType, String defau } else if (isSubTypeOfHttpResponse(returnType, semanticModel)) { addResponseMappingForHttpResponse(); } else if (isSubTypeOfHttpStatusCodeResponse(returnType, semanticModel)) { - TypeSymbol bodyType = getBodyTypeFromStatusCodeResponse(returnType, semanticModel); - Map headersFromStatusCodeResponse = getHeadersFromStatusCodeResponse(returnType); - String statusCode = getResponseCode(returnType, defaultStatusCode, semanticModel); + RecordTypeSymbol statusCodeRecordType = getRecordTypeSymbol(returnType); + TypeSymbol bodyType = getBodyTypeFromResponseRecord(statusCodeRecordType, semanticModel); + Map headersFromStatusCodeResponse = getHeadersFromResponseRecord(statusCodeRecordType); + String statusCode = getResponseCodeForStatusCodeResponse(returnType, defaultStatusCode, semanticModel); updateHeaderMap(statusCode, headersFromStatusCodeResponse); - createResponseMapping(bodyType, getResponseCode(returnType, defaultStatusCode, semanticModel)); + createResponseMapping(bodyType, statusCode); + } else if (isSubTypeOfHttpStatusCodeError(returnType, semanticModel)) { + RecordTypeSymbol errorDetailRecordType = getStatusCodeErrorDetailRecordTypeSymbol(returnType); + TypeSymbol bodyType = getBodyTypeFromResponseRecord(errorDetailRecordType, semanticModel); + Map headersFromStatusCodeResponse = getHeadersFromResponseRecord(errorDetailRecordType); + String statusCode = getResponseCodeForStatusCodeError(returnType, semanticModel); + updateHeaderMap(statusCode, headersFromStatusCodeResponse); + createResponseMapping(bodyType, statusCode); } else { ApiResponse apiResponse = new ApiResponse(); String mediaType = getMediaTypeFromType(returnType, mediaTypeSubTypePrefix, allowedMediaTypes, semanticModel); addResponseContent(returnType, apiResponse, mediaType); - String statusCode = getResponseCode(returnType, defaultStatusCode, semanticModel); + String statusCode = getResponseCodeForAnydata(returnType, defaultStatusCode, semanticModel); apiResponse.description(HTTP_CODE_DESCRIPTIONS.get(statusCode)); addApiResponse(apiResponse, statusCode); } @@ -345,19 +355,29 @@ private void extractBasicMembers(UnionTypeSymbol unionTypeSymbol, String default if (isSameMediaType(unionTypeSymbol, semanticModel)) { String mediaType = getMediaTypeFromType(unionTypeSymbol, mediaTypeSubTypePrefix, allowedMediaTypes, semanticModel); - String code = getResponseCode(unionTypeSymbol, defaultCode, semanticModel); + String code = getResponseCodeForAnydata(unionTypeSymbol, defaultCode, semanticModel); updateResponseCodeMap(responses, unionTypeSymbol, code, mediaType); return; } List directMemberTypes = unionTypeSymbol.userSpecifiedMemberTypes(); for (TypeSymbol directMemberType : directMemberTypes) { - String code = getResponseCode(directMemberType, defaultCode, semanticModel); - if (isHttpStatusCodeResponseType(semanticModel, directMemberType)) { - Map headersFromStatusCodeResponse = getHeadersFromStatusCodeResponse(directMemberType); + String code = getResponseCodeForAnydata(directMemberType, defaultCode, semanticModel); + if (isSubTypeOfHttpStatusCodeResponse(directMemberType, semanticModel)) { + code = getResponseCodeForStatusCodeResponse(directMemberType, code, semanticModel); + RecordTypeSymbol statusCodeRecordType = getRecordTypeSymbol(directMemberType); + Map headersFromStatusCodeResponse = getHeadersFromResponseRecord(statusCodeRecordType); + if (!headersFromStatusCodeResponse.isEmpty()) { + updateHeaderMap(code, headersFromStatusCodeResponse); + } + directMemberType = getBodyTypeFromResponseRecord(statusCodeRecordType, semanticModel); + } else if (isSubTypeOfHttpStatusCodeError(directMemberType, semanticModel)) { + code = getResponseCodeForStatusCodeError(directMemberType, semanticModel); + RecordTypeSymbol errorDetailRecordType = getStatusCodeErrorDetailRecordTypeSymbol(directMemberType); + Map headersFromStatusCodeResponse = getHeadersFromResponseRecord(errorDetailRecordType); if (!headersFromStatusCodeResponse.isEmpty()) { updateHeaderMap(code, headersFromStatusCodeResponse); } - directMemberType = getBodyTypeFromStatusCodeResponse(directMemberType, semanticModel); + directMemberType = getBodyTypeFromResponseRecord(errorDetailRecordType, semanticModel); } if (isSameMediaType(directMemberType, semanticModel)) { String mediaType = getMediaTypeFromType(directMemberType, mediaTypeSubTypePrefix, allowedMediaTypes, @@ -384,31 +404,19 @@ private void updateHeaderMap(String code, Map headers) { } } - public static boolean isHttpStatusCodeResponseType(SemanticModel semanticModel, TypeSymbol typeSymbol) { - Optional optionalStatusCodeRecordSymbol = semanticModel.types().getTypeByName("ballerina", "http", - "", "StatusCodeResponse"); - if (optionalStatusCodeRecordSymbol.isPresent() && - optionalStatusCodeRecordSymbol.get() instanceof TypeDefinitionSymbol statusCodeRecordSymbol) { - return typeSymbol.subtypeOf(statusCodeRecordSymbol.typeDescriptor()); - } - return false; - } - - public TypeSymbol getBodyTypeFromStatusCodeResponse(TypeSymbol typeSymbol, SemanticModel semanticModel) { - RecordTypeSymbol statusCodeRecordType = getStatusCodeRecordTypeSymbol(typeSymbol); - if (Objects.nonNull(statusCodeRecordType) && statusCodeRecordType.fieldDescriptors().containsKey("body")) { - return statusCodeRecordType.fieldDescriptors().get("body").typeDescriptor(); + public TypeSymbol getBodyTypeFromResponseRecord(RecordTypeSymbol responseRecordType, SemanticModel semanticModel) { + if (Objects.nonNull(responseRecordType) && responseRecordType.fieldDescriptors().containsKey("body")) { + return responseRecordType.fieldDescriptors().get("body").typeDescriptor(); } return semanticModel.types().ANYDATA; } - public Map getHeadersFromStatusCodeResponse(TypeSymbol typeSymbol) { - RecordTypeSymbol statusCodeRecordType = getStatusCodeRecordTypeSymbol(typeSymbol); - if (Objects.isNull(statusCodeRecordType)) { + public Map getHeadersFromResponseRecord(RecordTypeSymbol responseRecordType) { + if (Objects.isNull(responseRecordType)) { return new HashMap<>(); } - HeadersInfo headersInfo = getHeadersInfo(statusCodeRecordType); + HeadersInfo headersInfo = getHeadersInfo(responseRecordType); if (Objects.isNull(headersInfo)) { return new HashMap<>(); } @@ -420,10 +428,10 @@ public Map getHeadersFromStatusCodeResponse(TypeSymbol typeSymbo return mapRecordFieldToHeaders(recordFieldsMapping); } - private HeadersInfo getHeadersInfo(RecordTypeSymbol statusCodeRecordType) { - if (statusCodeRecordType.fieldDescriptors().containsKey("headers")) { + private HeadersInfo getHeadersInfo(RecordTypeSymbol responseRecordType) { + if (responseRecordType.fieldDescriptors().containsKey("headers")) { TypeSymbol headersType = typeMapper.getReferredType( - statusCodeRecordType.fieldDescriptors().get("headers").typeDescriptor()); + responseRecordType.fieldDescriptors().get("headers").typeDescriptor()); if (Objects.nonNull(headersType) && headersType instanceof TypeReferenceTypeSymbol headersRefType && headersRefType.typeDescriptor() instanceof RecordTypeSymbol recordType) { return new HeadersInfo(recordType, MapperCommonUtils.getTypeName(headersType)); @@ -437,7 +445,7 @@ private HeadersInfo getHeadersInfo(RecordTypeSymbol statusCodeRecordType) { private record HeadersInfo(RecordTypeSymbol headerRecordType, String recordName) { } - private RecordTypeSymbol getStatusCodeRecordTypeSymbol(TypeSymbol typeSymbol) { + private RecordTypeSymbol getRecordTypeSymbol(TypeSymbol typeSymbol) { TypeSymbol statusCodeResType = typeMapper.getReferredType(typeSymbol); RecordTypeSymbol statusCodeRecordType = null; if (statusCodeResType instanceof TypeReferenceTypeSymbol statusCodeResRefType && @@ -449,6 +457,15 @@ private RecordTypeSymbol getStatusCodeRecordTypeSymbol(TypeSymbol typeSymbol) { return statusCodeRecordType; } + private RecordTypeSymbol getStatusCodeErrorDetailRecordTypeSymbol(TypeSymbol typeSymbol) { + IntersectionTypeSymbol errorIntersectionType = typeMapper.getReferredIntersectionType(typeSymbol); + if (Objects.isNull(errorIntersectionType) || + !(errorIntersectionType.effectiveTypeDescriptor() instanceof ErrorTypeSymbol errorTypeSymbol)) { + return null; + } + return getRecordTypeSymbol(errorTypeSymbol.detailTypeDescriptor()); + } + public Map mapRecordFieldToHeaders(Map recordFields) { Map headers = new HashMap<>(); for (Map.Entry entry : recordFields.entrySet()) { @@ -459,14 +476,28 @@ public Map mapRecordFieldToHeaders(Map recordFie return headers; } - private static String getResponseCode(TypeSymbol typeSymbol, String defaultCode, SemanticModel semanticModel) { + private static String getResponseCodeForAnydata(TypeSymbol typeSymbol, String defaultCode, SemanticModel semanticModel) { if (isSubTypeOfNil(typeSymbol, semanticModel)) { return HTTP_202; } else if (isSubTypeOfError(typeSymbol, semanticModel)) { return HTTP_500; } + return defaultCode; + } + + private static String getResponseCodeForStatusCodeError(TypeSymbol typeSymbol, SemanticModel semanticModel) { + for (Map.Entry entry : HTTP_STATUS_CODE_ERRORS.entrySet()) { + if (isSubTypeOfBallerinaModuleType(entry.getKey(), "http.httpscerr", typeSymbol, semanticModel)) { + return entry.getValue(); + } + } + return HTTP_500; + } + + private static String getResponseCodeForStatusCodeResponse(TypeSymbol typeSymbol, String defaultCode, + SemanticModel semanticModel) { for (Map.Entry entry : HTTP_CODES.entrySet()) { - if (isSubTypeOfHttpRecordType(entry.getKey(), typeSymbol, semanticModel)) { + if (isSubTypeOfBallerinaModuleType(entry.getKey(), "http", typeSymbol, semanticModel)) { return entry.getValue(); } } @@ -500,11 +531,16 @@ public static boolean isSubTypeOfHttpResponse(TypeSymbol returnType, SemanticMod } private static boolean isSubTypeOfHttpStatusCodeResponse(TypeSymbol typeSymbol, SemanticModel semanticModel) { - return isSubTypeOfHttpRecordType("StatusCodeResponse", typeSymbol, semanticModel); + return isSubTypeOfBallerinaModuleType("StatusCodeResponse", "http", typeSymbol, semanticModel); } - private static boolean isSubTypeOfHttpRecordType(String type, TypeSymbol typeSymbol, SemanticModel semanticModel) { - Optional optionalRecordSymbol = semanticModel.types().getTypeByName("ballerina", "http", + private static boolean isSubTypeOfHttpStatusCodeError(TypeSymbol typeSymbol, SemanticModel semanticModel) { + return isSubTypeOfBallerinaModuleType("StatusCodeError", "http.httpscerr", typeSymbol, semanticModel); + } + + private static boolean isSubTypeOfBallerinaModuleType(String type, String moduleName, TypeSymbol typeSymbol, + SemanticModel semanticModel) { + Optional optionalRecordSymbol = semanticModel.types().getTypeByName("ballerina", moduleName, "", type); if (optionalRecordSymbol.isPresent() && optionalRecordSymbol.get() instanceof TypeDefinitionSymbol recordSymbol) { diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/ReferenceTypeMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/ReferenceTypeMapper.java index 6b914b4f0..821c0dd96 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/ReferenceTypeMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/ReferenceTypeMapper.java @@ -96,4 +96,20 @@ public static TypeSymbol getReferredType(TypeSymbol typeSymbol) { } return typeSymbol; } + + public static IntersectionTypeSymbol getReferredIntersectionType(TypeSymbol typeSymbol) { + if (typeSymbol.typeKind().equals(TypeDescKind.TYPE_REFERENCE)) { + TypeSymbol referencedType = ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(); + if (referencedType.typeKind().equals(TypeDescKind.TYPE_REFERENCE)) { + return getReferredIntersectionType(referencedType); + } else if (referencedType.typeKind().equals(TypeDescKind.INTERSECTION)) { + return (IntersectionTypeSymbol) referencedType; + } else { + return null; + } + } else if (typeSymbol.typeKind().equals(TypeDescKind.INTERSECTION)) { + return (IntersectionTypeSymbol) typeSymbol; + } + return null; + } } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapper.java index fb9816d1a..d50f8a8b2 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapper.java @@ -17,6 +17,7 @@ */ package io.ballerina.openapi.service.mapper.type; +import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; import io.ballerina.compiler.api.symbols.RecordFieldSymbol; import io.ballerina.compiler.api.symbols.TypeSymbol; import io.swagger.v3.oas.models.media.Schema; @@ -40,6 +41,8 @@ Map getSchemaForRecordFields(Map reco TypeSymbol getReferredType(TypeSymbol typeSymbol); + IntersectionTypeSymbol getReferredIntersectionType(TypeSymbol typeSymbol); + static void setDefaultValue(Schema schema, Object defaultValue) { if (Objects.isNull(defaultValue)) { return; diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapperImpl.java index f2f035624..d2e63b42b 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapperImpl.java @@ -120,4 +120,8 @@ public Map getSchemaForRecordFields(Map Date: Fri, 2 Feb 2024 09:05:16 +0530 Subject: [PATCH 02/12] Organize code base --- .../constraint/ConstraintMapperImpl.java | 6 +- .../mapper/model/ModuleMemberVisitor.java | 7 +- .../mapper/response/ResponseMapperImpl.java | 203 ++++-------------- .../{ => model}/CacheConfigAnnotation.java | 3 +- .../response/model/HeaderRecordInfo.java | 31 +++ .../mapper/response/model/ResponseInfo.java | 37 ++++ .../{ => utils}/CacheHeaderUtils.java | 4 +- .../response/utils/StatusCodeErrorUtils.java | 76 +++++++ .../utils/StatusCodeResponseUtils.java | 63 ++++++ .../response/utils/StatusCodeTypeUtils.java | 118 ++++++++++ .../service/mapper/type/RecordTypeMapper.java | 26 +-- .../src/main/java/module-info.java | 2 + 12 files changed, 397 insertions(+), 179 deletions(-) rename ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/{ => model}/CacheConfigAnnotation.java (98%) create mode 100644 ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/HeaderRecordInfo.java create mode 100644 ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/ResponseInfo.java rename ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/{ => utils}/CacheHeaderUtils.java (98%) create mode 100644 ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java create mode 100644 ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java create mode 100644 ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/constraint/ConstraintMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/constraint/ConstraintMapperImpl.java index 647932dba..db15443db 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/constraint/ConstraintMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/constraint/ConstraintMapperImpl.java @@ -96,10 +96,12 @@ public void setConstraints() { } Map schemas = components.getSchemas(); for (Map.Entry schemaEntry : schemas.entrySet()) { - TypeDefinitionNode typeDefinitionNode = moduleMemberVisitor.getTypeDefinitionNode(schemaEntry.getKey()); - if (Objects.isNull(typeDefinitionNode)) { + Optional typeDefNodeOpt = moduleMemberVisitor.getTypeDefinitionNode( + schemaEntry.getKey()); + if (typeDefNodeOpt.isEmpty()) { continue; } + TypeDefinitionNode typeDefinitionNode = typeDefNodeOpt.get(); if (typeDefinitionNode.metadata().isPresent()) { ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = new ConstraintAnnotation.ConstraintAnnotationBuilder(); diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/ModuleMemberVisitor.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/ModuleMemberVisitor.java index eefcf781f..fb9290b3e 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/ModuleMemberVisitor.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/ModuleMemberVisitor.java @@ -23,6 +23,7 @@ import io.ballerina.openapi.service.mapper.utils.MapperCommonUtils; import java.util.LinkedHashSet; +import java.util.Optional; import java.util.Set; /** @@ -49,12 +50,12 @@ public Set getListenerDeclarationNodes() { return listenerDeclarationNodes; } - public TypeDefinitionNode getTypeDefinitionNode(String typeName) { + public Optional getTypeDefinitionNode(String typeName) { for (TypeDefinitionNode typeDefinitionNode : typeDefinitionNodes) { if (MapperCommonUtils.unescapeIdentifier(typeDefinitionNode.typeName().text()).equals(typeName)) { - return typeDefinitionNode; + return Optional.of(typeDefinitionNode); } } - return null; + return Optional.empty(); } } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java index 07beaf278..0bfacef3c 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java @@ -19,13 +19,9 @@ import io.ballerina.compiler.api.SemanticModel; import io.ballerina.compiler.api.symbols.ClassSymbol; -import io.ballerina.compiler.api.symbols.ErrorTypeSymbol; import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; -import io.ballerina.compiler.api.symbols.RecordFieldSymbol; -import io.ballerina.compiler.api.symbols.RecordTypeSymbol; import io.ballerina.compiler.api.symbols.ResourceMethodSymbol; import io.ballerina.compiler.api.symbols.Symbol; -import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; import io.ballerina.compiler.api.symbols.TypeDescKind; import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; import io.ballerina.compiler.api.symbols.TypeSymbol; @@ -37,9 +33,13 @@ import io.ballerina.compiler.syntax.tree.ReturnTypeDescriptorNode; import io.ballerina.openapi.service.mapper.model.AdditionalData; import io.ballerina.openapi.service.mapper.model.OperationInventory; +import io.ballerina.openapi.service.mapper.response.model.CacheConfigAnnotation; +import io.ballerina.openapi.service.mapper.response.model.ResponseInfo; +import io.ballerina.openapi.service.mapper.response.utils.CacheHeaderUtils; +import io.ballerina.openapi.service.mapper.response.utils.StatusCodeErrorUtils; +import io.ballerina.openapi.service.mapper.response.utils.StatusCodeResponseUtils; import io.ballerina.openapi.service.mapper.type.TypeMapper; import io.ballerina.openapi.service.mapper.type.TypeMapperImpl; -import io.ballerina.openapi.service.mapper.utils.MapperCommonUtils; import io.ballerina.openapi.service.mapper.utils.MediaTypeUtils; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.headers.Header; @@ -51,7 +51,6 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -59,17 +58,18 @@ import static io.ballerina.openapi.service.mapper.Constants.ACCEPTED; import static io.ballerina.openapi.service.mapper.Constants.BAD_REQUEST; +import static io.ballerina.openapi.service.mapper.Constants.DEFAULT; import static io.ballerina.openapi.service.mapper.Constants.HTTP_200; import static io.ballerina.openapi.service.mapper.Constants.HTTP_201; import static io.ballerina.openapi.service.mapper.Constants.HTTP_202; import static io.ballerina.openapi.service.mapper.Constants.HTTP_400; import static io.ballerina.openapi.service.mapper.Constants.HTTP_500; -import static io.ballerina.openapi.service.mapper.Constants.HTTP_CODES; import static io.ballerina.openapi.service.mapper.Constants.HTTP_CODE_DESCRIPTIONS; import static io.ballerina.openapi.service.mapper.Constants.HTTP_PAYLOAD; -import static io.ballerina.openapi.service.mapper.Constants.HTTP_STATUS_CODE_ERRORS; import static io.ballerina.openapi.service.mapper.Constants.MEDIA_TYPE; import static io.ballerina.openapi.service.mapper.Constants.POST; +import static io.ballerina.openapi.service.mapper.response.utils.StatusCodeErrorUtils.isSubTypeOfHttpStatusCodeError; +import static io.ballerina.openapi.service.mapper.response.utils.StatusCodeResponseUtils.isSubTypeOfHttpStatusCodeResponse; import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.extractAnnotationFieldDetails; import static io.ballerina.openapi.service.mapper.utils.MediaTypeUtils.getMediaTypeFromType; import static io.ballerina.openapi.service.mapper.utils.MediaTypeUtils.isSameMediaType; @@ -145,25 +145,25 @@ private void extractAnnotationDetails(FunctionDefinitionNode resource) { } private void createResponseMapping(TypeSymbol returnType, String defaultStatusCode) { - UnionTypeSymbol unionType = getUnionType(returnType, semanticModel); - if (Objects.nonNull(unionType)) { - addResponseMappingForUnion(defaultStatusCode, unionType); + Optional unionTypeOpt = getUnionType(returnType, semanticModel); + if (unionTypeOpt.isPresent()) { + addResponseMappingForUnion(defaultStatusCode, unionTypeOpt.get()); } else { addResponseMappingForSimpleType(returnType, defaultStatusCode); } } - private static UnionTypeSymbol getUnionType(TypeSymbol typeSymbol, SemanticModel semanticModel) { + private static Optional getUnionType(TypeSymbol typeSymbol, SemanticModel semanticModel) { if (Objects.isNull(typeSymbol)) { - return null; + return Optional.empty(); } return switch (typeSymbol.typeKind()) { - case UNION -> (UnionTypeSymbol) typeSymbol; - case TYPE_REFERENCE -> isSameMediaType(typeSymbol, semanticModel) ? null : + case UNION -> Optional.of((UnionTypeSymbol) typeSymbol); + case TYPE_REFERENCE -> isSameMediaType(typeSymbol, semanticModel) ? Optional.empty() : getUnionType(((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(), semanticModel); case INTERSECTION -> getUnionType(((IntersectionTypeSymbol) typeSymbol).effectiveTypeDescriptor(), semanticModel); - default -> null; + default -> Optional.empty(); }; } @@ -266,7 +266,7 @@ private void addResponseMappingForHttpResponse() { ApiResponse apiResponse = new ApiResponse(); apiResponse.setDescription("Any Response"); apiResponse.setContent(new Content().addMediaType("*/*", mediaTypeObj)); - addApiResponse(apiResponse, "default"); + addApiResponse(apiResponse, DEFAULT); } private void addResponseMappingForSimpleType(TypeSymbol returnType, String defaultStatusCode) { @@ -275,30 +275,28 @@ private void addResponseMappingForSimpleType(TypeSymbol returnType, String defau } else if (isSubTypeOfHttpResponse(returnType, semanticModel)) { addResponseMappingForHttpResponse(); } else if (isSubTypeOfHttpStatusCodeResponse(returnType, semanticModel)) { - RecordTypeSymbol statusCodeRecordType = getRecordTypeSymbol(returnType); - TypeSymbol bodyType = getBodyTypeFromResponseRecord(statusCodeRecordType, semanticModel); - Map headersFromStatusCodeResponse = getHeadersFromResponseRecord(statusCodeRecordType); - String statusCode = getResponseCodeForStatusCodeResponse(returnType, defaultStatusCode, semanticModel); - updateHeaderMap(statusCode, headersFromStatusCodeResponse); - createResponseMapping(bodyType, statusCode); + ResponseInfo responseInfo = StatusCodeResponseUtils.extractResponseInfo(returnType, defaultStatusCode, + typeMapper, semanticModel); + updateApiResponseWithResponseInfo(responseInfo); } else if (isSubTypeOfHttpStatusCodeError(returnType, semanticModel)) { - RecordTypeSymbol errorDetailRecordType = getStatusCodeErrorDetailRecordTypeSymbol(returnType); - TypeSymbol bodyType = getBodyTypeFromResponseRecord(errorDetailRecordType, semanticModel); - Map headersFromStatusCodeResponse = getHeadersFromResponseRecord(errorDetailRecordType); - String statusCode = getResponseCodeForStatusCodeError(returnType, semanticModel); - updateHeaderMap(statusCode, headersFromStatusCodeResponse); - createResponseMapping(bodyType, statusCode); + ResponseInfo responseInfo = StatusCodeErrorUtils.extractResponseInfo(returnType, typeMapper, semanticModel); + updateApiResponseWithResponseInfo(responseInfo); } else { ApiResponse apiResponse = new ApiResponse(); String mediaType = getMediaTypeFromType(returnType, mediaTypeSubTypePrefix, allowedMediaTypes, semanticModel); addResponseContent(returnType, apiResponse, mediaType); - String statusCode = getResponseCodeForAnydata(returnType, defaultStatusCode, semanticModel); + String statusCode = getResponseCode(returnType, defaultStatusCode, semanticModel); apiResponse.description(HTTP_CODE_DESCRIPTIONS.get(statusCode)); addApiResponse(apiResponse, statusCode); } } + private void updateApiResponseWithResponseInfo(ResponseInfo responseInfo) { + updateHeaderMap(responseInfo.statusCode(), responseInfo.headers()); + createResponseMapping(responseInfo.bodyType(), responseInfo.statusCode()); + } + public Map> getResponseCodeMap(UnionTypeSymbol typeSymbol, String defaultCode) { Map>> responsesCodeMap = new HashMap<>(); extractBasicMembers(typeSymbol, defaultCode, responsesCodeMap); @@ -355,43 +353,40 @@ private void extractBasicMembers(UnionTypeSymbol unionTypeSymbol, String default if (isSameMediaType(unionTypeSymbol, semanticModel)) { String mediaType = getMediaTypeFromType(unionTypeSymbol, mediaTypeSubTypePrefix, allowedMediaTypes, semanticModel); - String code = getResponseCodeForAnydata(unionTypeSymbol, defaultCode, semanticModel); + String code = getResponseCode(unionTypeSymbol, defaultCode, semanticModel); updateResponseCodeMap(responses, unionTypeSymbol, code, mediaType); return; } List directMemberTypes = unionTypeSymbol.userSpecifiedMemberTypes(); for (TypeSymbol directMemberType : directMemberTypes) { - String code = getResponseCodeForAnydata(directMemberType, defaultCode, semanticModel); + String code = getResponseCode(directMemberType, defaultCode, semanticModel); + ResponseInfo responseInfo = null; if (isSubTypeOfHttpStatusCodeResponse(directMemberType, semanticModel)) { - code = getResponseCodeForStatusCodeResponse(directMemberType, code, semanticModel); - RecordTypeSymbol statusCodeRecordType = getRecordTypeSymbol(directMemberType); - Map headersFromStatusCodeResponse = getHeadersFromResponseRecord(statusCodeRecordType); - if (!headersFromStatusCodeResponse.isEmpty()) { - updateHeaderMap(code, headersFromStatusCodeResponse); - } - directMemberType = getBodyTypeFromResponseRecord(statusCodeRecordType, semanticModel); + responseInfo = StatusCodeResponseUtils.extractResponseInfo(directMemberType, code, + typeMapper, semanticModel); } else if (isSubTypeOfHttpStatusCodeError(directMemberType, semanticModel)) { - code = getResponseCodeForStatusCodeError(directMemberType, semanticModel); - RecordTypeSymbol errorDetailRecordType = getStatusCodeErrorDetailRecordTypeSymbol(directMemberType); - Map headersFromStatusCodeResponse = getHeadersFromResponseRecord(errorDetailRecordType); - if (!headersFromStatusCodeResponse.isEmpty()) { - updateHeaderMap(code, headersFromStatusCodeResponse); + responseInfo = StatusCodeErrorUtils.extractResponseInfo(directMemberType, typeMapper, semanticModel); + } + if (Objects.nonNull(responseInfo)) { + code = responseInfo.statusCode(); + if (!responseInfo.headers().isEmpty()) { + updateHeaderMap(code, responseInfo.headers()); } - directMemberType = getBodyTypeFromResponseRecord(errorDetailRecordType, semanticModel); + directMemberType = responseInfo.bodyType(); } if (isSameMediaType(directMemberType, semanticModel)) { String mediaType = getMediaTypeFromType(directMemberType, mediaTypeSubTypePrefix, allowedMediaTypes, semanticModel); updateResponseCodeMap(responses, directMemberType, code, mediaType); } else { - UnionTypeSymbol unionType = getUnionType(directMemberType, semanticModel); - if (Objects.isNull(unionType)) { + Optional unionTypeOpt = getUnionType(directMemberType, semanticModel); + if (unionTypeOpt.isEmpty()) { String mediaType = getMediaTypeFromType(directMemberType, mediaTypeSubTypePrefix, allowedMediaTypes, semanticModel); updateResponseCodeMap(responses, directMemberType, code, mediaType); continue; } - extractBasicMembers(unionType, code, responses); + extractBasicMembers(unionTypeOpt.get(), code, responses); } } } @@ -404,79 +399,7 @@ private void updateHeaderMap(String code, Map headers) { } } - public TypeSymbol getBodyTypeFromResponseRecord(RecordTypeSymbol responseRecordType, SemanticModel semanticModel) { - if (Objects.nonNull(responseRecordType) && responseRecordType.fieldDescriptors().containsKey("body")) { - return responseRecordType.fieldDescriptors().get("body").typeDescriptor(); - } - return semanticModel.types().ANYDATA; - } - - public Map getHeadersFromResponseRecord(RecordTypeSymbol responseRecordType) { - if (Objects.isNull(responseRecordType)) { - return new HashMap<>(); - } - - HeadersInfo headersInfo = getHeadersInfo(responseRecordType); - if (Objects.isNull(headersInfo)) { - return new HashMap<>(); - } - - Map recordFieldMap = new HashMap<>(headersInfo.headerRecordType(). - fieldDescriptors()); - Map recordFieldsMapping = typeMapper.getSchemaForRecordFields(recordFieldMap, new HashSet<>(), - headersInfo.recordName(), false); - return mapRecordFieldToHeaders(recordFieldsMapping); - } - - private HeadersInfo getHeadersInfo(RecordTypeSymbol responseRecordType) { - if (responseRecordType.fieldDescriptors().containsKey("headers")) { - TypeSymbol headersType = typeMapper.getReferredType( - responseRecordType.fieldDescriptors().get("headers").typeDescriptor()); - if (Objects.nonNull(headersType) && headersType instanceof TypeReferenceTypeSymbol headersRefType && - headersRefType.typeDescriptor() instanceof RecordTypeSymbol recordType) { - return new HeadersInfo(recordType, MapperCommonUtils.getTypeName(headersType)); - } else if (Objects.nonNull(headersType) && headersType instanceof RecordTypeSymbol recordType) { - return new HeadersInfo(recordType, MapperCommonUtils.getTypeName(recordType)); - } - } - return null; - } - - private record HeadersInfo(RecordTypeSymbol headerRecordType, String recordName) { - } - - private RecordTypeSymbol getRecordTypeSymbol(TypeSymbol typeSymbol) { - TypeSymbol statusCodeResType = typeMapper.getReferredType(typeSymbol); - RecordTypeSymbol statusCodeRecordType = null; - if (statusCodeResType instanceof TypeReferenceTypeSymbol statusCodeResRefType && - statusCodeResRefType.typeDescriptor() instanceof RecordTypeSymbol recordTypeSymbol) { - statusCodeRecordType = recordTypeSymbol; - } else if (statusCodeResType instanceof RecordTypeSymbol recordTypeSymbol) { - statusCodeRecordType = recordTypeSymbol; - } - return statusCodeRecordType; - } - - private RecordTypeSymbol getStatusCodeErrorDetailRecordTypeSymbol(TypeSymbol typeSymbol) { - IntersectionTypeSymbol errorIntersectionType = typeMapper.getReferredIntersectionType(typeSymbol); - if (Objects.isNull(errorIntersectionType) || - !(errorIntersectionType.effectiveTypeDescriptor() instanceof ErrorTypeSymbol errorTypeSymbol)) { - return null; - } - return getRecordTypeSymbol(errorTypeSymbol.detailTypeDescriptor()); - } - - public Map mapRecordFieldToHeaders(Map recordFields) { - Map headers = new HashMap<>(); - for (Map.Entry entry : recordFields.entrySet()) { - Header header = new Header(); - header.setSchema(entry.getValue()); - headers.put(entry.getKey(), header); - } - return headers; - } - - private static String getResponseCodeForAnydata(TypeSymbol typeSymbol, String defaultCode, SemanticModel semanticModel) { + private static String getResponseCode(TypeSymbol typeSymbol, String defaultCode, SemanticModel semanticModel) { if (isSubTypeOfNil(typeSymbol, semanticModel)) { return HTTP_202; } else if (isSubTypeOfError(typeSymbol, semanticModel)) { @@ -485,25 +408,6 @@ private static String getResponseCodeForAnydata(TypeSymbol typeSymbol, String de return defaultCode; } - private static String getResponseCodeForStatusCodeError(TypeSymbol typeSymbol, SemanticModel semanticModel) { - for (Map.Entry entry : HTTP_STATUS_CODE_ERRORS.entrySet()) { - if (isSubTypeOfBallerinaModuleType(entry.getKey(), "http.httpscerr", typeSymbol, semanticModel)) { - return entry.getValue(); - } - } - return HTTP_500; - } - - private static String getResponseCodeForStatusCodeResponse(TypeSymbol typeSymbol, String defaultCode, - SemanticModel semanticModel) { - for (Map.Entry entry : HTTP_CODES.entrySet()) { - if (isSubTypeOfBallerinaModuleType(entry.getKey(), "http", typeSymbol, semanticModel)) { - return entry.getValue(); - } - } - return defaultCode; - } - private static boolean isSuccessStatusCode(String statusCode) { return statusCode.startsWith("2"); } @@ -529,23 +433,4 @@ public static boolean isSubTypeOfHttpResponse(TypeSymbol returnType, SemanticMod } return false; } - - private static boolean isSubTypeOfHttpStatusCodeResponse(TypeSymbol typeSymbol, SemanticModel semanticModel) { - return isSubTypeOfBallerinaModuleType("StatusCodeResponse", "http", typeSymbol, semanticModel); - } - - private static boolean isSubTypeOfHttpStatusCodeError(TypeSymbol typeSymbol, SemanticModel semanticModel) { - return isSubTypeOfBallerinaModuleType("StatusCodeError", "http.httpscerr", typeSymbol, semanticModel); - } - - private static boolean isSubTypeOfBallerinaModuleType(String type, String moduleName, TypeSymbol typeSymbol, - SemanticModel semanticModel) { - Optional optionalRecordSymbol = semanticModel.types().getTypeByName("ballerina", moduleName, - "", type); - if (optionalRecordSymbol.isPresent() && - optionalRecordSymbol.get() instanceof TypeDefinitionSymbol recordSymbol) { - return typeSymbol.subtypeOf(recordSymbol.typeDescriptor()); - } - return false; - } } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/CacheConfigAnnotation.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/CacheConfigAnnotation.java similarity index 98% rename from ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/CacheConfigAnnotation.java rename to ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/CacheConfigAnnotation.java index 15d03973c..049fa1bdf 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/CacheConfigAnnotation.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/CacheConfigAnnotation.java @@ -15,10 +15,11 @@ * specific language governing permissions and limitations * under the License. */ -package io.ballerina.openapi.service.mapper.response; +package io.ballerina.openapi.service.mapper.response.model; import java.util.ArrayList; import java.util.List; + /** * This class uses to store all the cache configuration details. * diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/HeaderRecordInfo.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/HeaderRecordInfo.java new file mode 100644 index 000000000..130f849df --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/HeaderRecordInfo.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.openapi.service.mapper.response.model; + +import io.ballerina.compiler.api.symbols.RecordTypeSymbol; + +/** + * This {@link HeaderRecordInfo} record stores the response header record information. + * @param recordType - The record type of the response header. + * @param recordName - The name of the record. + * + * @since 1.9.0 + */ +public record HeaderRecordInfo(RecordTypeSymbol recordType, + String recordName) { +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/ResponseInfo.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/ResponseInfo.java new file mode 100644 index 000000000..6975d4824 --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/model/ResponseInfo.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.openapi.service.mapper.response.model; + +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.swagger.v3.oas.models.headers.Header; + +import java.util.Map; + +/** + * This {@link ResponseInfo} record stores the response information for Ballerina HTTP status code responses or + * status code errors. + * @param statusCode - The status code of the response. + * @param bodyType - The type of the response body. + * @param headers - The headers of the response. + * + * @since 1.9.0 + */ +public record ResponseInfo(String statusCode, + TypeSymbol bodyType, + Map headers) { +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/CacheHeaderUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/CacheHeaderUtils.java similarity index 98% rename from ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/CacheHeaderUtils.java rename to ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/CacheHeaderUtils.java index a072b18be..d6b4b2943 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/CacheHeaderUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/CacheHeaderUtils.java @@ -15,7 +15,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.ballerina.openapi.service.mapper.response; +package io.ballerina.openapi.service.mapper.response.utils; import io.ballerina.compiler.syntax.tree.ExpressionNode; import io.ballerina.compiler.syntax.tree.ListConstructorExpressionNode; @@ -24,6 +24,7 @@ import io.ballerina.compiler.syntax.tree.SeparatedNodeList; import io.ballerina.compiler.syntax.tree.SpecificFieldNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.openapi.service.mapper.response.model.CacheConfigAnnotation; import io.swagger.v3.oas.models.headers.Header; import io.swagger.v3.oas.models.media.StringSchema; @@ -57,7 +58,6 @@ public final class CacheHeaderUtils { private CacheHeaderUtils() { - } public static CacheConfigAnnotation setCacheConfigValues(SeparatedNodeList fields) { diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java new file mode 100644 index 000000000..96376f410 --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.openapi.service.mapper.response.utils; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.ErrorTypeSymbol; +import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; +import io.ballerina.compiler.api.symbols.RecordTypeSymbol; +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.openapi.service.mapper.response.model.ResponseInfo; +import io.ballerina.openapi.service.mapper.type.TypeMapper; +import io.swagger.v3.oas.models.headers.Header; + +import java.util.Map; +import java.util.Objects; + +import static io.ballerina.openapi.service.mapper.Constants.HTTP_500; +import static io.ballerina.openapi.service.mapper.Constants.HTTP_STATUS_CODE_ERRORS; + +/** + * This {@link StatusCodeErrorUtils} class provides functionalities for mapping the Ballerina HTTP status code + * error to OpenAPI response. + * + * @since 1.9.0 + */ +public final class StatusCodeErrorUtils extends StatusCodeTypeUtils { + + private StatusCodeErrorUtils() { + } + + public static boolean isSubTypeOfHttpStatusCodeError(TypeSymbol typeSymbol, SemanticModel semanticModel) { + return isSubTypeOfBallerinaModuleType("StatusCodeError", "http.httpscerr", typeSymbol, semanticModel); + } + + public static ResponseInfo extractResponseInfo(TypeSymbol statusCodeResponseType, TypeMapper typeMapper, + SemanticModel semanticModel) { + RecordTypeSymbol errorDetailRecordType = getErrorDetailTypeSymbol(statusCodeResponseType, typeMapper); + TypeSymbol bodyType = getBodyType(errorDetailRecordType, semanticModel); + Map headers = getHeaders(errorDetailRecordType, typeMapper); + String statusCode = getResponseCode(errorDetailRecordType, semanticModel); + return new ResponseInfo(statusCode, bodyType, headers); + } + + private static RecordTypeSymbol getErrorDetailTypeSymbol(TypeSymbol typeSymbol, TypeMapper typeMapper) { + IntersectionTypeSymbol errorIntersectionType = typeMapper.getReferredIntersectionType(typeSymbol); + if (Objects.isNull(errorIntersectionType) || + !(errorIntersectionType.effectiveTypeDescriptor() instanceof ErrorTypeSymbol errorTypeSymbol)) { + return null; + } + return getRecordTypeSymbol(errorTypeSymbol.detailTypeDescriptor(), typeMapper); + } + + private static String getResponseCode(TypeSymbol typeSymbol, SemanticModel semanticModel) { + for (Map.Entry entry : HTTP_STATUS_CODE_ERRORS.entrySet()) { + if (isSubTypeOfBallerinaModuleType(entry.getKey(), "http.httpscerr", typeSymbol, semanticModel)) { + return entry.getValue(); + } + } + return HTTP_500; + } +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java new file mode 100644 index 000000000..27e6578fe --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.openapi.service.mapper.response.utils; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.RecordTypeSymbol; +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.openapi.service.mapper.response.model.ResponseInfo; +import io.ballerina.openapi.service.mapper.type.TypeMapper; +import io.swagger.v3.oas.models.headers.Header; + +import java.util.Map; + +import static io.ballerina.openapi.service.mapper.Constants.HTTP_CODES; + +/** + * This {@link StatusCodeResponseUtils} class provides functionalities for mapping the Ballerina HTTP status code + * response to OpenAPI response. + * + * @since 1.9.0 + */ +public final class StatusCodeResponseUtils extends StatusCodeTypeUtils { + + private StatusCodeResponseUtils() { + } + + public static boolean isSubTypeOfHttpStatusCodeResponse(TypeSymbol typeSymbol, SemanticModel semanticModel) { + return isSubTypeOfBallerinaModuleType("StatusCodeResponse", "http", typeSymbol, semanticModel); + } + + public static ResponseInfo extractResponseInfo(TypeSymbol statusCodeResponseType, String defaultStatusCode, + TypeMapper typeMapper, SemanticModel semanticModel) { + RecordTypeSymbol statusCodeRecordType = getRecordTypeSymbol(statusCodeResponseType, typeMapper); + TypeSymbol bodyType = getBodyType(statusCodeRecordType, semanticModel); + Map headers = getHeaders(statusCodeRecordType, typeMapper); + String statusCode = getResponseCode(statusCodeRecordType, defaultStatusCode, semanticModel); + return new ResponseInfo(statusCode, bodyType, headers); + } + + private static String getResponseCode(TypeSymbol typeSymbol, String defaultCode, SemanticModel semanticModel) { + for (Map.Entry entry : HTTP_CODES.entrySet()) { + if (isSubTypeOfBallerinaModuleType(entry.getKey(), "http", typeSymbol, semanticModel)) { + return entry.getValue(); + } + } + return defaultCode; + } +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java new file mode 100644 index 000000000..ff4c57132 --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you 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 io.ballerina.openapi.service.mapper.response.utils; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.RecordFieldSymbol; +import io.ballerina.compiler.api.symbols.RecordTypeSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; +import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.openapi.service.mapper.response.model.HeaderRecordInfo; +import io.ballerina.openapi.service.mapper.type.TypeMapper; +import io.ballerina.openapi.service.mapper.utils.MapperCommonUtils; +import io.swagger.v3.oas.models.headers.Header; +import io.swagger.v3.oas.models.media.Schema; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * This {@link StatusCodeTypeUtils} class provides common functionalities for mapping the Ballerina HTTP status code + * response and error to OpenAPI response. + * + * @since 1.9.0 + */ +public abstract class StatusCodeTypeUtils { + + static boolean isSubTypeOfBallerinaModuleType(String type, String moduleName, TypeSymbol typeSymbol, + SemanticModel semanticModel) { + Optional optionalRecordSymbol = semanticModel.types().getTypeByName("ballerina", moduleName, + "", type); + if (optionalRecordSymbol.isPresent() && + optionalRecordSymbol.get() instanceof TypeDefinitionSymbol recordSymbol) { + return typeSymbol.subtypeOf(recordSymbol.typeDescriptor()); + } + return false; + } + + static RecordTypeSymbol getRecordTypeSymbol(TypeSymbol typeSymbol, TypeMapper typeMapper) { + TypeSymbol statusCodeResType = typeMapper.getReferredType(typeSymbol); + RecordTypeSymbol statusCodeRecordType = null; + if (statusCodeResType instanceof TypeReferenceTypeSymbol statusCodeResRefType && + statusCodeResRefType.typeDescriptor() instanceof RecordTypeSymbol recordTypeSymbol) { + statusCodeRecordType = recordTypeSymbol; + } else if (statusCodeResType instanceof RecordTypeSymbol recordTypeSymbol) { + statusCodeRecordType = recordTypeSymbol; + } + return statusCodeRecordType; + } + + static TypeSymbol getBodyType(RecordTypeSymbol responseRecordType, SemanticModel semanticModel) { + if (Objects.nonNull(responseRecordType) && responseRecordType.fieldDescriptors().containsKey("body")) { + return responseRecordType.fieldDescriptors().get("body").typeDescriptor(); + } + return semanticModel.types().ANYDATA; + } + + static Map getHeaders(RecordTypeSymbol responseRecordType, + TypeMapper typeMapper) { + if (Objects.isNull(responseRecordType)) { + return new HashMap<>(); + } + + HeaderRecordInfo headersInfo = getHeadersInfo(responseRecordType, typeMapper); + if (Objects.isNull(headersInfo)) { + return new HashMap<>(); + } + + Map recordFieldMap = new HashMap<>(headersInfo.recordType(). + fieldDescriptors()); + Map recordFieldsMapping = typeMapper.getSchemaForRecordFields(recordFieldMap, new HashSet<>(), + headersInfo.recordName(), false); + return mapRecordFieldToHeaders(recordFieldsMapping); + } + + private static HeaderRecordInfo getHeadersInfo(RecordTypeSymbol responseRecordType, TypeMapper typeMapper) { + if (responseRecordType.fieldDescriptors().containsKey("headers")) { + TypeSymbol headersType = typeMapper.getReferredType( + responseRecordType.fieldDescriptors().get("headers").typeDescriptor()); + if (Objects.nonNull(headersType) && headersType instanceof TypeReferenceTypeSymbol headersRefType && + headersRefType.typeDescriptor() instanceof RecordTypeSymbol recordType) { + return new HeaderRecordInfo(recordType, MapperCommonUtils.getTypeName(headersType)); + } else if (Objects.nonNull(headersType) && headersType instanceof RecordTypeSymbol recordType) { + return new HeaderRecordInfo(recordType, MapperCommonUtils.getTypeName(recordType)); + } + } + return null; + } + + private static Map mapRecordFieldToHeaders(Map recordFields) { + Map headers = new HashMap<>(); + for (Map.Entry entry : recordFields.entrySet()) { + Header header = new Header(); + header.setSchema(entry.getValue()); + headers.put(entry.getKey(), header); + } + return headers; + } +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java index 76cdfaf6a..29947fd53 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java @@ -146,10 +146,10 @@ public static Map mapRecordFields(Map recordFieldSchema = recordFieldSchema.description(recordFieldDescription); } if (recordFieldSymbol.hasDefaultValue()) { - Object recordFieldDefaultValue = getRecordFieldDefaultValue(recordName, recordFieldName, + Optional recordFieldDefaultValueOpt = getRecordFieldDefaultValue(recordName, recordFieldName, additionalData.moduleMemberVisitor()); - if (Objects.nonNull(recordFieldDefaultValue)) { - TypeMapper.setDefaultValue(recordFieldSchema, recordFieldDefaultValue); + if (recordFieldDefaultValueOpt.isPresent()) { + TypeMapper.setDefaultValue(recordFieldSchema, recordFieldDefaultValueOpt.get()); } else { DiagnosticMessages message = DiagnosticMessages.OAS_CONVERTOR_124; IncompatibleResourceDiagnostic error = new IncompatibleResourceDiagnostic(message, @@ -162,29 +162,31 @@ public static Map mapRecordFields(Map return properties; } - public static Object getRecordFieldDefaultValue(String recordName, String fieldName, + public static Optional getRecordFieldDefaultValue(String recordName, String fieldName, ModuleMemberVisitor moduleMemberVisitor) { - TypeDefinitionNode recordDefNode = moduleMemberVisitor.getTypeDefinitionNode(recordName); - if (Objects.isNull(recordDefNode) || !(recordDefNode.typeDescriptor() instanceof RecordTypeDescriptorNode)) { - return null; + Optional recordDefNodeOpt = moduleMemberVisitor.getTypeDefinitionNode(recordName); + if (recordDefNodeOpt.isPresent() && + recordDefNodeOpt.get().typeDescriptor() instanceof RecordTypeDescriptorNode recordDefNode) { + return getRecordFieldDefaultValue(fieldName, recordDefNode); } - return getRecordFieldDefaultValue(fieldName, (RecordTypeDescriptorNode) recordDefNode.typeDescriptor()); + return Optional.empty(); } - private static Object getRecordFieldDefaultValue(String fieldName, RecordTypeDescriptorNode recordDefNode) { + private static Optional getRecordFieldDefaultValue(String fieldName, + RecordTypeDescriptorNode recordDefNode) { NodeList recordFields = recordDefNode.fields(); RecordFieldWithDefaultValueNode defaultValueNode = recordFields.stream() .filter(field -> field instanceof RecordFieldWithDefaultValueNode) .map(field -> (RecordFieldWithDefaultValueNode) field) .filter(field -> field.fieldName().toString().trim().equals(fieldName)).findFirst().orElse(null); if (Objects.isNull(defaultValueNode)) { - return null; + return Optional.empty(); } ExpressionNode defaultValueExpression = defaultValueNode.expression(); if (MapperCommonUtils.isNotSimpleValueLiteralKind(defaultValueExpression.kind())) { - return null; + return Optional.empty(); } - return MapperCommonUtils.parseBalSimpleLiteral(defaultValueExpression.toString().trim()); + return Optional.of(MapperCommonUtils.parseBalSimpleLiteral(defaultValueExpression.toString().trim())); } public static RecordTypeInfo getDirectRecordType(TypeSymbol typeSymbol, String recordName) { diff --git a/ballerina-to-openapi/src/main/java/module-info.java b/ballerina-to-openapi/src/main/java/module-info.java index 004a96ab8..3abc45aae 100644 --- a/ballerina-to-openapi/src/main/java/module-info.java +++ b/ballerina-to-openapi/src/main/java/module-info.java @@ -40,5 +40,7 @@ exports io.ballerina.openapi.service.mapper.utils; exports io.ballerina.openapi.service.mapper.parameter; exports io.ballerina.openapi.service.mapper.response; + exports io.ballerina.openapi.service.mapper.response.model; + exports io.ballerina.openapi.service.mapper.response.utils; } From 86c7f52029cd2ae3aa75ab1c41000e5f96f745ad Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Fri, 2 Feb 2024 09:47:26 +0530 Subject: [PATCH 03/12] Fix the status code inferring logic --- .../service/mapper/response/utils/StatusCodeErrorUtils.java | 6 +++--- .../mapper/response/utils/StatusCodeResponseUtils.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java index 96376f410..2e6cc4f86 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java @@ -47,12 +47,12 @@ public static boolean isSubTypeOfHttpStatusCodeError(TypeSymbol typeSymbol, Sema return isSubTypeOfBallerinaModuleType("StatusCodeError", "http.httpscerr", typeSymbol, semanticModel); } - public static ResponseInfo extractResponseInfo(TypeSymbol statusCodeResponseType, TypeMapper typeMapper, + public static ResponseInfo extractResponseInfo(TypeSymbol statusCodeErrorType, TypeMapper typeMapper, SemanticModel semanticModel) { - RecordTypeSymbol errorDetailRecordType = getErrorDetailTypeSymbol(statusCodeResponseType, typeMapper); + RecordTypeSymbol errorDetailRecordType = getErrorDetailTypeSymbol(statusCodeErrorType, typeMapper); TypeSymbol bodyType = getBodyType(errorDetailRecordType, semanticModel); Map headers = getHeaders(errorDetailRecordType, typeMapper); - String statusCode = getResponseCode(errorDetailRecordType, semanticModel); + String statusCode = getResponseCode(statusCodeErrorType, semanticModel); return new ResponseInfo(statusCode, bodyType, headers); } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java index 27e6578fe..ab1ded14e 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java @@ -48,7 +48,7 @@ public static ResponseInfo extractResponseInfo(TypeSymbol statusCodeResponseType RecordTypeSymbol statusCodeRecordType = getRecordTypeSymbol(statusCodeResponseType, typeMapper); TypeSymbol bodyType = getBodyType(statusCodeRecordType, semanticModel); Map headers = getHeaders(statusCodeRecordType, typeMapper); - String statusCode = getResponseCode(statusCodeRecordType, defaultStatusCode, semanticModel); + String statusCode = getResponseCode(statusCodeResponseType, defaultStatusCode, semanticModel); return new ResponseInfo(statusCode, bodyType, headers); } From 741694311f200f7808b51b4e1b34983402d0f170 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Fri, 2 Feb 2024 12:35:53 +0530 Subject: [PATCH 04/12] Fix response overwrite due to data binding --- .../mapper/response/ResponseMapperImpl.java | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java index 0bfacef3c..722aab680 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java @@ -43,6 +43,7 @@ import io.ballerina.openapi.service.mapper.utils.MediaTypeUtils; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.headers.Header; +import io.swagger.v3.oas.models.media.ComposedSchema; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; @@ -56,8 +57,6 @@ import java.util.Objects; import java.util.Optional; -import static io.ballerina.openapi.service.mapper.Constants.ACCEPTED; -import static io.ballerina.openapi.service.mapper.Constants.BAD_REQUEST; import static io.ballerina.openapi.service.mapper.Constants.DEFAULT; import static io.ballerina.openapi.service.mapper.Constants.HTTP_200; import static io.ballerina.openapi.service.mapper.Constants.HTTP_201; @@ -242,19 +241,26 @@ private void updateApiResponseContentWithMediaType(ApiResponse apiResponse, Stri if (Objects.isNull(content)) { content = new Content(); } - content.addMediaType(mediaType, mediaTypeObj); + if (content.containsKey(mediaType) && Objects.nonNull(content.get(mediaType).getSchema())) { + MediaType existingMediaType = content.get(mediaType); + if (Objects.nonNull(mediaTypeObj.getSchema())) { + Schema updatedSchema = new ComposedSchema().oneOf(List.of(existingMediaType.getSchema(), + mediaTypeObj.getSchema())); + existingMediaType.setSchema(updatedSchema); + } + } else { + content.addMediaType(mediaType, mediaTypeObj); + } apiResponse.setContent(content); } private void addResponseMappingForNil() { - ApiResponse apiResponse = new ApiResponse(); - apiResponse.description(ACCEPTED); + ApiResponse apiResponse = getApiResponse(HTTP_202); addApiResponse(apiResponse, HTTP_202); } private void addResponseMappingForDataBindingFailures() { - ApiResponse apiResponse = new ApiResponse(); - apiResponse.description(BAD_REQUEST); + ApiResponse apiResponse = getApiResponse(HTTP_400); TypeSymbol errorType = semanticModel.types().ERROR; addResponseContent(errorType, apiResponse, "application/json"); addApiResponse(apiResponse, HTTP_400); @@ -263,12 +269,21 @@ private void addResponseMappingForDataBindingFailures() { private void addResponseMappingForHttpResponse() { MediaType mediaTypeObj = new MediaType(); mediaTypeObj.setSchema(new Schema().description("Any type of entity body")); - ApiResponse apiResponse = new ApiResponse(); + ApiResponse apiResponse = getApiResponse(DEFAULT); apiResponse.setDescription("Any Response"); apiResponse.setContent(new Content().addMediaType("*/*", mediaTypeObj)); addApiResponse(apiResponse, DEFAULT); } + private ApiResponse getApiResponse(String statusCode) { + if (apiResponses.containsKey(statusCode)) { + return apiResponses.get(statusCode); + } + ApiResponse apiResponse = new ApiResponse(); + apiResponse.setDescription(HTTP_CODE_DESCRIPTIONS.get(statusCode)); + return apiResponse; + } + private void addResponseMappingForSimpleType(TypeSymbol returnType, String defaultStatusCode) { if (isSubTypeOfNil(returnType, semanticModel)) { addResponseMappingForNil(); From d3f915affabecd23aba69ab1683fe41e9407af98 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Fri, 2 Feb 2024 12:36:00 +0530 Subject: [PATCH 05/12] Add test cases --- .../generators/openapi/ResponseTests.java | 20 ++ .../response/status_code_errors_01.yaml | 170 +++++++++++++++++ .../response/status_code_errors_02.yaml | 162 +++++++++++++++++ .../response/status_code_errors_01.bal | 171 ++++++++++++++++++ .../response/status_code_errors_02.bal | 55 ++++++ 5 files changed, 578 insertions(+) create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_01.yaml create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_02.yaml create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_01.bal create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_02.bal diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java index 66b0de8c8..70b9789fe 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java @@ -393,6 +393,26 @@ public void testNilUnionReturnType() throws IOException { compareWithGeneratedFile(ballerinaFilePath, "response/nil_union_return_type.yaml"); } + @Test(description = "When the resource has all the http status code error return") + public void testHttpStatusCodeErrorReturnType1() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("response/status_code_errors_01.bal"); + OASContractGenerator openApiConverterUtils = new OASContractGenerator(); + openApiConverterUtils.generateOAS3DefinitionsAllService(ballerinaFilePath, this.tempDir, null + , false); + Assert.assertTrue(openApiConverterUtils.getErrors().isEmpty()); + compareWithGeneratedFile(ballerinaFilePath, "response/status_code_errors_01.yaml"); + } + + @Test(description = "When the resource has http status code error return with common detail") + public void testHttpStatusCodeErrorReturnType2() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("response/status_code_errors_02.bal"); + OASContractGenerator openApiConverterUtils = new OASContractGenerator(); + openApiConverterUtils.generateOAS3DefinitionsAllService(ballerinaFilePath, this.tempDir, null + , false); + Assert.assertTrue(openApiConverterUtils.getErrors().isEmpty()); + compareWithGeneratedFile(ballerinaFilePath, "response/status_code_errors_02.yaml"); + } + @AfterMethod public void cleanUp() { TestUtils.deleteDirectory(this.tempDir); diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_01.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_01.yaml new file mode 100644 index 000000000..200778ab8 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_01.yaml @@ -0,0 +1,170 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /statusCodeError: + get: + operationId: getStatuscodeerror + parameters: + - name: statusCode + in: query + required: true + schema: + type: integer + format: int64 + responses: + "451": + description: UnavailableDueToLegalReasons + "431": + description: RequestHeaderFieldsTooLarge + default: + description: Default response + "410": + description: Gone + "411": + description: LengthRequired + "510": + description: NotExtended + "412": + description: PreconditionFailed + "511": + description: NetworkAuthenticationRequired + "413": + description: PayloadTooLarge + "415": + description: UnsupportedMediaType + "416": + description: RangeNotSatisfiable + "417": + description: ExpectationFailed + "421": + description: MisdirectedRequest + "400": + description: BadRequest + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + "422": + description: UnprocessableEntity + "401": + description: Unauthorized + "500": + description: InternalServerError + content: + application/json: + schema: {} + "423": + description: Locked + "402": + description: PaymentRequired + "424": + description: FailedDependency + "501": + description: NotImplemented + "403": + description: Forbidden + "502": + description: BadGateway + "404": + description: NotFound + "426": + description: UpgradeRequired + "503": + description: ServiceUnavailable + "405": + description: MethodNotAllowed + "504": + description: GatewayTimeout + "406": + description: NotAcceptable + "428": + description: PreconditionRequired + "407": + description: ProxyAuthenticationRequired + "429": + description: TooManyRequests + "506": + description: VariantAlsoNegotiates + "507": + description: InsufficientStorage + "409": + description: Conflict + "508": + description: LoopDetected + post: + operationId: postStatuscodeerror + parameters: + - name: statusCode + in: query + required: true + schema: + type: integer + format: int64 + - name: header2 + in: header + required: true + schema: + type: array + items: + type: string + - name: header1 + in: header + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: {} + required: true + responses: + default: + description: Default response + "500": + description: InternalServerError + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ErrorPayload' + - {} + "400": + description: BadRequest + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' +components: + schemas: + ErrorPayload: + required: + - message + - method + - path + - reason + - status + - timestamp + type: object + properties: + timestamp: + type: string + status: + type: integer + format: int64 + reason: + type: string + message: + type: string + path: + type: string + method: + type: string diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_02.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_02.yaml new file mode 100644 index 000000000..de6f285a9 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_02.yaml @@ -0,0 +1,162 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "9000" +paths: + /users/{id}: + get: + operationId: getUsersId + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/User' + "202": + description: Accepted + "404": + description: NotFound + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInfo' + "400": + description: BadRequest + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + /users: + post: + operationId: postUsers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserWithoutId' + required: true + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/User' + "400": + description: BadRequest + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ErrorInfo' + - $ref: '#/components/schemas/ErrorPayload' + "202": + description: Accepted + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInfo' + /test1: + get: + operationId: getTest1 + responses: + default: + description: Any Response + content: + '*/*': + schema: + description: Any type of entity body + "202": + description: Accepted + /test2: + get: + operationId: getTest2 + responses: + default: + description: Any Response + content: + '*/*': + schema: + description: Any type of entity body + application/json: + schema: + $ref: '#/components/schemas/ErrorInfo' + "202": + description: Accepted +components: + schemas: + ErrorInfo: + required: + - message + - timeStamp + type: object + properties: + timeStamp: + type: string + message: + type: string + additionalProperties: false + ErrorPayload: + required: + - message + - method + - path + - reason + - status + - timestamp + type: object + properties: + timestamp: + type: string + status: + type: integer + format: int64 + reason: + type: string + message: + type: string + path: + type: string + method: + type: string + User: + type: object + allOf: + - $ref: '#/components/schemas/UserWithoutId' + - required: + - id + type: object + properties: + id: + type: integer + format: int64 + additionalProperties: false + UserWithoutId: + required: + - age + - name + type: object + properties: + name: + type: string + age: + type: integer + format: int64 + additionalProperties: false diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_01.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_01.bal new file mode 100644 index 000000000..39e6ffd2b --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_01.bal @@ -0,0 +1,171 @@ +import ballerina/http; +import ballerina/http.httpscerr; + +function getStatusCodeError(int statusCode) returns + httpscerr:BadRequestError|httpscerr:UnauthorizedError|httpscerr:PaymentRequiredError| + httpscerr:ForbiddenError|httpscerr:NotFoundError|httpscerr:MethodNotAllowedError| + httpscerr:NotAcceptableError|httpscerr:ProxyAuthenticationRequiredError|httpscerr:RequestTimeoutError| + httpscerr:ConflictError|httpscerr:GoneError|httpscerr:LengthRequiredError|httpscerr:PreconditionFailedError| + httpscerr:PayloadTooLargeError|httpscerr:URITooLongError|httpscerr:UnsupportedMediaTypeError| + httpscerr:RangeNotSatisfiableError|httpscerr:ExpectationFailedError|httpscerr:MisdirectedRequestError| + httpscerr:UnprocessableEntityError|httpscerr:LockedError|httpscerr:FailedDependencyError| + httpscerr:UpgradeRequiredError|httpscerr:PreconditionRequiredError|httpscerr:TooManyRequestsError| + httpscerr:RequestHeaderFieldsTooLargeError|httpscerr:UnavailableDueToLegalReasonsError| + httpscerr:InternalServerErrorError|httpscerr:NotImplementedError|httpscerr:BadGatewayError| + httpscerr:ServiceUnavailableError|httpscerr:GatewayTimeoutError|httpscerr:HTTPVersionNotSupportedError| + httpscerr:VariantAlsoNegotiatesError|httpscerr:InsufficientStorageError|httpscerr:LoopDetectedError| + httpscerr:NotExtendedError|httpscerr:NetworkAuthenticationRequiredError|httpscerr:DefaultStatusCodeError +{ + match statusCode { + 400 => { + return error httpscerr:BadRequestError("Bad request error"); + } + 401 => { + return error httpscerr:UnauthorizedError("Unauthorized error"); + } + 402 => { + return error httpscerr:PaymentRequiredError("Payment required error"); + } + 403 => { + return error httpscerr:ForbiddenError("Forbidden error"); + } + 404 => { + return error httpscerr:NotFoundError("Not found error"); + } + 405 => { + return error httpscerr:MethodNotAllowedError("Method not allowed error"); + } + 406 => { + return error httpscerr:NotAcceptableError("Not acceptable error"); + } + 407 => { + return error httpscerr:ProxyAuthenticationRequiredError("Proxy authentication required error"); + } + 408 => { + return error httpscerr:RequestTimeoutError("Request timeout error"); + } + 409 => { + return error httpscerr:ConflictError("Conflict error"); + } + 410 => { + return error httpscerr:GoneError("Gone error"); + } + 411 => { + return error httpscerr:LengthRequiredError("Length required error"); + } + 412 => { + return error httpscerr:PreconditionFailedError("Precondition failed error"); + } + 413 => { + return error httpscerr:PayloadTooLargeError("Payload too large error"); + } + 414 => { + return error httpscerr:URITooLongError("URI too long error"); + } + 415 => { + return error httpscerr:UnsupportedMediaTypeError("Unsupported media type error"); + } + 416 => { + return error httpscerr:RangeNotSatisfiableError("Range not satisfiable error"); + } + 417 => { + return error httpscerr:ExpectationFailedError("Expectation failed error"); + } + 421 => { + return error httpscerr:MisdirectedRequestError("Misdirected request error"); + } + 422 => { + return error httpscerr:UnprocessableEntityError("Unprocessable entity error"); + } + 423 => { + return error httpscerr:LockedError("Locked error"); + } + 424 => { + return error httpscerr:FailedDependencyError("Failed dependency error"); + } + 426 => { + return error httpscerr:UpgradeRequiredError("Upgrade required error"); + } + 428 => { + return error httpscerr:PreconditionRequiredError("Precondition required error"); + } + 429 => { + return error httpscerr:TooManyRequestsError("Too many requests error"); + } + 431 => { + return error httpscerr:RequestHeaderFieldsTooLargeError("Request header fields too large error"); + } + 451 => { + return error httpscerr:UnavailableDueToLegalReasonsError("Unavailable for legal reasons error"); + } + 500 => { + return error httpscerr:InternalServerErrorError("Internal server error error"); + } + 501 => { + return error httpscerr:NotImplementedError("Not implemented error"); + } + 502 => { + return error httpscerr:BadGatewayError("Bad gateway error"); + } + 503 => { + return error httpscerr:ServiceUnavailableError("Service unavailable error"); + } + 504 => { + return error httpscerr:GatewayTimeoutError("Gateway timeout error"); + } + 505 => { + return error httpscerr:HTTPVersionNotSupportedError("HTTP version not supported error"); + } + 506 => { + return error httpscerr:VariantAlsoNegotiatesError("Variant also negotiates error"); + } + 507 => { + return error httpscerr:InsufficientStorageError("Insufficient storage error"); + } + 508 => { + return error httpscerr:LoopDetectedError("Loop detected error"); + } + 510 => { + return error httpscerr:NotExtendedError("Not extended error"); + } + 511 => { + return error httpscerr:NetworkAuthenticationRequiredError("Network authentication required error"); + } + _ => { + return error httpscerr:DefaultStatusCodeError("Default error", statusCode = statusCode); + } + } +} + +type CustomHeaders record {| + string header1; + string[] header2; +|}; + +service /payloadV on new http:Listener(9090) { + + resource function get statusCodeError(int statusCode) returns + httpscerr:BadRequestError|httpscerr:UnauthorizedError|httpscerr:PaymentRequiredError| + httpscerr:ForbiddenError|httpscerr:NotFoundError|httpscerr:MethodNotAllowedError| + httpscerr:NotAcceptableError|httpscerr:ProxyAuthenticationRequiredError|httpscerr:RequestTimeoutError| + httpscerr:ConflictError|httpscerr:GoneError|httpscerr:LengthRequiredError|httpscerr:PreconditionFailedError| + httpscerr:PayloadTooLargeError|httpscerr:URITooLongError|httpscerr:UnsupportedMediaTypeError| + httpscerr:RangeNotSatisfiableError|httpscerr:ExpectationFailedError|httpscerr:MisdirectedRequestError| + httpscerr:UnprocessableEntityError|httpscerr:LockedError|httpscerr:FailedDependencyError| + httpscerr:UpgradeRequiredError|httpscerr:PreconditionRequiredError|httpscerr:TooManyRequestsError| + httpscerr:RequestHeaderFieldsTooLargeError|httpscerr:UnavailableDueToLegalReasonsError| + httpscerr:InternalServerErrorError|httpscerr:NotImplementedError|httpscerr:BadGatewayError| + httpscerr:ServiceUnavailableError|httpscerr:GatewayTimeoutError|httpscerr:HTTPVersionNotSupportedError| + httpscerr:VariantAlsoNegotiatesError|httpscerr:InsufficientStorageError|httpscerr:LoopDetectedError| + httpscerr:NotExtendedError|httpscerr:NetworkAuthenticationRequiredError|httpscerr:DefaultStatusCodeError + { + return getStatusCodeError(statusCode); + } + + resource function post statusCodeError(@http:Payload anydata payload, int statusCode, + @http:Header CustomHeaders headers) returns httpscerr:DefaultStatusCodeError|error|httpscerr:InternalServerErrorError { + + return error httpscerr:DefaultStatusCodeError("Default error", statusCode = statusCode, + body = payload, headers = headers); + } +} diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_02.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_02.bal new file mode 100644 index 000000000..5921ea5c8 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_02.bal @@ -0,0 +1,55 @@ +import ballerina/http; +import ballerina/http.httpscerr; + +type Error distinct error; + +type ErrorInfo record {| + string timeStamp; + string message; +|}; + +type ErrorDetails record {| + *httpscerr:ErrorDetail; + ErrorInfo body; +|}; + +type UserNotFoundError Error & httpscerr:NotFoundError; + +type UserNameAlreadyExistError Error & httpscerr:ConflictError; + +type BadUserError Error & httpscerr:BadRequestError; + +type DefaultError Error & httpscerr:DefaultStatusCodeError; + +type User record {| + readonly int id; + *UserWithoutId; +|}; + +type UserWithoutId record {| + string name; + int age; +|}; + +service /payloadV on new http:Listener(9000) { + + resource function get users/[int id]() returns User|UserNotFoundError? { + return; + } + + resource function post users(@http:Payload readonly & UserWithoutId user) + returns User|UserNameAlreadyExistError|BadUserError? { + return; + } + + resource function get test1() returns http:Response|httpscerr:DefaultStatusCodeError? { + return; + } + + resource function get test2() returns http:Response|DefaultError? { + return; + } + + resource function 'default [string... path]() returns httpscerr:NotFoundError? { + } +} From 43557d1af3ed08ce567179164b1518192112ce4f Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Thu, 8 Feb 2024 15:20:30 +0530 Subject: [PATCH 06/12] Update ballerina lang timestamped version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index dc71129ce..ccf738a12 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ group=io.ballerina version=1.9.0-SNAPSHOT #dependency -ballerinaLangVersion=2201.8.0 +ballerinaLangVersion=2201.9.0-20240208-103300-0823dc95 testngVersion=7.6.1 slf4jVersion=1.7.30 org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 From 80043d53db76cd22dfc4e1edfb59c06ee57c785d Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Thu, 8 Feb 2024 16:01:09 +0530 Subject: [PATCH 07/12] Add additional test case --- .../generators/openapi/ResponseTests.java | 10 + .../response/status_code_errors_03.yaml | 173 ++++++++++++++++++ .../response/status_code_errors_03.bal | 65 +++++++ 3 files changed, 248 insertions(+) create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_03.yaml create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_03.bal diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java index 70b9789fe..6759a81ac 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java @@ -413,6 +413,16 @@ public void testHttpStatusCodeErrorReturnType2() throws IOException { compareWithGeneratedFile(ballerinaFilePath, "response/status_code_errors_02.yaml"); } + @Test(description = "When the resource has http status code error return with different detail types") + public void testHttpStatusCodeErrorReturnType3() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("response/status_code_errors_03.bal"); + OASContractGenerator openApiConverterUtils = new OASContractGenerator(); + openApiConverterUtils.generateOAS3DefinitionsAllService(ballerinaFilePath, this.tempDir, null + , false); + Assert.assertTrue(openApiConverterUtils.getErrors().isEmpty()); + compareWithGeneratedFile(ballerinaFilePath, "response/status_code_errors_03.yaml"); + } + @AfterMethod public void cleanUp() { TestUtils.deleteDirectory(this.tempDir); diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_03.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_03.yaml new file mode 100644 index 000000000..e5dc5ace8 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_03.yaml @@ -0,0 +1,173 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "9000" +paths: + /users/{id}: + get: + operationId: getUsersId + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/User' + "202": + description: Accepted + "404": + description: NotFound + content: + application/json: + schema: + $ref: '#/components/schemas/UserApiErrorInfo' + "400": + description: BadRequest + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + /users: + post: + operationId: postUsers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserWithoutId' + required: true + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/User' + "400": + description: BadRequest + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/UserApiErrorInfo' + - $ref: '#/components/schemas/ErrorPayload' + "202": + description: Accepted + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/UserApiErrorInfo' + /test1: + get: + operationId: getTest1 + responses: + default: + description: Any Response + content: + '*/*': + schema: + description: Any type of entity body + "202": + description: Accepted + /test2: + get: + operationId: getTest2 + responses: + default: + description: Any Response + content: + '*/*': + schema: + description: Any type of entity body + application/json: + schema: + $ref: '#/components/schemas/DefaultErrorInfo' + "202": + description: Accepted +components: + schemas: + DefaultErrorInfo: + required: + - message + - timeStamp + type: object + properties: + timeStamp: + type: string + message: + type: string + additionalProperties: false + ErrorPayload: + required: + - message + - method + - path + - reason + - status + - timestamp + type: object + properties: + timestamp: + type: string + status: + type: integer + format: int64 + reason: + type: string + message: + type: string + path: + type: string + method: + type: string + User: + type: object + allOf: + - $ref: '#/components/schemas/UserWithoutId' + - required: + - id + type: object + properties: + id: + type: integer + format: int64 + additionalProperties: false + UserApiErrorInfo: + type: object + allOf: + - $ref: '#/components/schemas/DefaultErrorInfo' + - required: + - userId + type: object + properties: + userId: + type: string + additionalProperties: false + UserWithoutId: + required: + - age + - name + type: object + properties: + name: + type: string + age: + type: integer + format: int64 + additionalProperties: false diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_03.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_03.bal new file mode 100644 index 000000000..0aaadf594 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/response/status_code_errors_03.bal @@ -0,0 +1,65 @@ +import ballerina/http; +import ballerina/http.httpscerr; + +type Error distinct error; + +type DefaultErrorInfo record {| + string timeStamp; + string message; +|}; + +type DefaultErrorDetails record {| + *httpscerr:ErrorDetail; + DefaultErrorInfo body; +|}; + +type UserApiErrorInfo record {| + *DefaultErrorInfo; + string userId; +|}; + +type UserApiErrorDetails record {| + *httpscerr:ErrorDetail; + UserApiErrorInfo body; +|}; + +type UserNotFoundError Error & httpscerr:NotFoundError & error; + +type UserNameAlreadyExistError Error & httpscerr:ConflictError & error; + +type BadUserError Error & httpscerr:BadRequestError & error; + +type DefaultError Error & httpscerr:DefaultStatusCodeError & error; + +type User record {| + readonly int id; + *UserWithoutId; +|}; + +type UserWithoutId record {| + string name; + int age; +|}; + +service /payloadV on new http:Listener(9000) { + + resource function get users/[int id]() returns User|UserNotFoundError? { + return; + } + + resource function post users(@http:Payload readonly & UserWithoutId user) + returns User|UserNameAlreadyExistError|BadUserError? { + return; + } + + resource function get test1() returns http:Response|httpscerr:DefaultStatusCodeError? { + return; + } + + resource function get test2() returns http:Response|DefaultError? { + return; + } + + resource function 'default [string... path]() returns httpscerr:NotFoundError? { + } +} From 3f51c34c8402f24939f994edef0b4249659262aa Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Thu, 8 Feb 2024 16:18:18 +0530 Subject: [PATCH 08/12] Fix assertion for no warnings --- .../io/ballerina/openapi/generators/openapi/ResponseTests.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java index 6759a81ac..c61e20215 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ResponseTests.java @@ -399,7 +399,6 @@ public void testHttpStatusCodeErrorReturnType1() throws IOException { OASContractGenerator openApiConverterUtils = new OASContractGenerator(); openApiConverterUtils.generateOAS3DefinitionsAllService(ballerinaFilePath, this.tempDir, null , false); - Assert.assertTrue(openApiConverterUtils.getErrors().isEmpty()); compareWithGeneratedFile(ballerinaFilePath, "response/status_code_errors_01.yaml"); } @@ -409,7 +408,6 @@ public void testHttpStatusCodeErrorReturnType2() throws IOException { OASContractGenerator openApiConverterUtils = new OASContractGenerator(); openApiConverterUtils.generateOAS3DefinitionsAllService(ballerinaFilePath, this.tempDir, null , false); - Assert.assertTrue(openApiConverterUtils.getErrors().isEmpty()); compareWithGeneratedFile(ballerinaFilePath, "response/status_code_errors_02.yaml"); } @@ -419,7 +417,6 @@ public void testHttpStatusCodeErrorReturnType3() throws IOException { OASContractGenerator openApiConverterUtils = new OASContractGenerator(); openApiConverterUtils.generateOAS3DefinitionsAllService(ballerinaFilePath, this.tempDir, null , false); - Assert.assertTrue(openApiConverterUtils.getErrors().isEmpty()); compareWithGeneratedFile(ballerinaFilePath, "response/status_code_errors_03.yaml"); } From 4ae8d8652f0245bcf25ebb15db8f268844a02fa8 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 13 Feb 2024 09:04:14 +0530 Subject: [PATCH 09/12] Add suggestions from review --- .../openapi/service/mapper/Constants.java | 4 ++-- .../mapper/response/ResponseMapperImpl.java | 4 +++- .../response/utils/StatusCodeErrorUtils.java | 15 ++++++++++----- .../response/utils/StatusCodeResponseUtils.java | 10 +++++++--- .../response/utils/StatusCodeTypeUtils.java | 9 ++++----- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java index 20e36fee3..2cd9c7ef9 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java @@ -198,7 +198,7 @@ public String toString() { httpErrorCodeMap.put("LengthRequiredError", "411"); httpErrorCodeMap.put("PreconditionFailedError", "412"); httpErrorCodeMap.put("PayloadTooLargeError", "413"); - httpErrorCodeMap.put("UriTooLongError", "414"); + httpErrorCodeMap.put("URITooLongError", "414"); httpErrorCodeMap.put("UnsupportedMediaTypeError", "415"); httpErrorCodeMap.put("RangeNotSatisfiableError", "416"); httpErrorCodeMap.put("ExpectationFailedError", "417"); @@ -217,7 +217,7 @@ public String toString() { httpErrorCodeMap.put("BadGatewayError", "502"); httpErrorCodeMap.put("ServiceUnavailableError", "503"); httpErrorCodeMap.put("GatewayTimeoutError", "504"); - httpErrorCodeMap.put("HttpVersionNotSupportedError", "505"); + httpErrorCodeMap.put("HTTPVersionNotSupportedError", "505"); httpErrorCodeMap.put("VariantAlsoNegotiatesError", "506"); httpErrorCodeMap.put("InsufficientStorageError", "507"); httpErrorCodeMap.put("LoopDetectedError", "508"); diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java index 722aab680..5c9f5264f 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapperImpl.java @@ -339,7 +339,9 @@ private void updateResponseCodeMap(Map>> re String responseCode, String mediaType) { if (responsesMap.containsKey(responseCode)) { if (responsesMap.get(responseCode).containsKey(mediaType)) { - responsesMap.get(responseCode).get(mediaType).add(typesymbol); + if (!responsesMap.get(responseCode).get(mediaType).contains(typesymbol)) { + responsesMap.get(responseCode).get(mediaType).add(typesymbol); + } } else { List typeSymbols = new ArrayList<>(); typeSymbols.add(typesymbol); diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java index 2e6cc4f86..9bb801c5a 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeErrorUtils.java @@ -26,8 +26,10 @@ import io.ballerina.openapi.service.mapper.type.TypeMapper; import io.swagger.v3.oas.models.headers.Header; +import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; import static io.ballerina.openapi.service.mapper.Constants.HTTP_500; import static io.ballerina.openapi.service.mapper.Constants.HTTP_STATUS_CODE_ERRORS; @@ -49,18 +51,21 @@ public static boolean isSubTypeOfHttpStatusCodeError(TypeSymbol typeSymbol, Sema public static ResponseInfo extractResponseInfo(TypeSymbol statusCodeErrorType, TypeMapper typeMapper, SemanticModel semanticModel) { - RecordTypeSymbol errorDetailRecordType = getErrorDetailTypeSymbol(statusCodeErrorType, typeMapper); - TypeSymbol bodyType = getBodyType(errorDetailRecordType, semanticModel); - Map headers = getHeaders(errorDetailRecordType, typeMapper); String statusCode = getResponseCode(statusCodeErrorType, semanticModel); + Optional errorDetailRecordType = getErrorDetailTypeSymbol(statusCodeErrorType, typeMapper); + if (errorDetailRecordType.isEmpty()) { + return new ResponseInfo(statusCode, semanticModel.types().ANYDATA, new HashMap<>()); + } + TypeSymbol bodyType = getBodyType(errorDetailRecordType.get(), semanticModel); + Map headers = getHeaders(errorDetailRecordType.get(), typeMapper); return new ResponseInfo(statusCode, bodyType, headers); } - private static RecordTypeSymbol getErrorDetailTypeSymbol(TypeSymbol typeSymbol, TypeMapper typeMapper) { + private static Optional getErrorDetailTypeSymbol(TypeSymbol typeSymbol, TypeMapper typeMapper) { IntersectionTypeSymbol errorIntersectionType = typeMapper.getReferredIntersectionType(typeSymbol); if (Objects.isNull(errorIntersectionType) || !(errorIntersectionType.effectiveTypeDescriptor() instanceof ErrorTypeSymbol errorTypeSymbol)) { - return null; + return Optional.empty(); } return getRecordTypeSymbol(errorTypeSymbol.detailTypeDescriptor(), typeMapper); } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java index ab1ded14e..13bf44a5b 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeResponseUtils.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.models.headers.Header; import java.util.Map; +import java.util.Optional; import static io.ballerina.openapi.service.mapper.Constants.HTTP_CODES; @@ -45,10 +46,13 @@ public static boolean isSubTypeOfHttpStatusCodeResponse(TypeSymbol typeSymbol, S public static ResponseInfo extractResponseInfo(TypeSymbol statusCodeResponseType, String defaultStatusCode, TypeMapper typeMapper, SemanticModel semanticModel) { - RecordTypeSymbol statusCodeRecordType = getRecordTypeSymbol(statusCodeResponseType, typeMapper); - TypeSymbol bodyType = getBodyType(statusCodeRecordType, semanticModel); - Map headers = getHeaders(statusCodeRecordType, typeMapper); + Optional statusCodeRecordType = getRecordTypeSymbol(statusCodeResponseType, typeMapper); String statusCode = getResponseCode(statusCodeResponseType, defaultStatusCode, semanticModel); + if (statusCodeRecordType.isEmpty()) { + return new ResponseInfo(statusCode, semanticModel.types().ANYDATA, Map.of()); + } + TypeSymbol bodyType = getBodyType(statusCodeRecordType.get(), semanticModel); + Map headers = getHeaders(statusCodeRecordType.get(), typeMapper); return new ResponseInfo(statusCode, bodyType, headers); } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java index ff4c57132..cb55083ae 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java @@ -55,16 +55,15 @@ static boolean isSubTypeOfBallerinaModuleType(String type, String moduleName, Ty return false; } - static RecordTypeSymbol getRecordTypeSymbol(TypeSymbol typeSymbol, TypeMapper typeMapper) { + static Optional getRecordTypeSymbol(TypeSymbol typeSymbol, TypeMapper typeMapper) { TypeSymbol statusCodeResType = typeMapper.getReferredType(typeSymbol); - RecordTypeSymbol statusCodeRecordType = null; if (statusCodeResType instanceof TypeReferenceTypeSymbol statusCodeResRefType && statusCodeResRefType.typeDescriptor() instanceof RecordTypeSymbol recordTypeSymbol) { - statusCodeRecordType = recordTypeSymbol; + return Optional.of(recordTypeSymbol); } else if (statusCodeResType instanceof RecordTypeSymbol recordTypeSymbol) { - statusCodeRecordType = recordTypeSymbol; + return Optional.of(recordTypeSymbol); } - return statusCodeRecordType; + return Optional.empty(); } static TypeSymbol getBodyType(RecordTypeSymbol responseRecordType, SemanticModel semanticModel) { From 097d045d2ef4c0257f71f08e3e7cf75574e3ad4e Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 13 Feb 2024 09:04:26 +0530 Subject: [PATCH 10/12] Fix test case --- .../expected_gen/response/readonly.yaml | 4 +--- .../expected_gen/response/status_code_errors_01.yaml | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/readonly.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/readonly.yaml index d3235c80a..30a51200d 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/readonly.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/readonly.yaml @@ -298,9 +298,7 @@ paths: additionalProperties: false text/plain: schema: - oneOf: - - type: string - - type: string + type: string components: schemas: ErrorPayload: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_01.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_01.yaml index 200778ab8..7b5f8d0be 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_01.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/status_code_errors_01.yaml @@ -39,6 +39,8 @@ paths: description: NetworkAuthenticationRequired "413": description: PayloadTooLarge + "414": + description: UriTooLong "415": description: UnsupportedMediaType "416": @@ -59,9 +61,6 @@ paths: description: Unauthorized "500": description: InternalServerError - content: - application/json: - schema: {} "423": description: Locked "402": @@ -88,6 +87,8 @@ paths: description: NotAcceptable "428": description: PreconditionRequired + "505": + description: HttpVersionNotSupported "407": description: ProxyAuthenticationRequired "429": From 063c4d1bdf5c4dbbb69229b2cf4d5e9f719e04f7 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 13 Feb 2024 12:54:43 +0530 Subject: [PATCH 11/12] Fix adding default values for response headers --- .../response/utils/StatusCodeTypeUtils.java | 15 ++++++++++----- .../service/mapper/type/RecordTypeMapper.java | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java index cb55083ae..83e20d44a 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/utils/StatusCodeTypeUtils.java @@ -35,6 +35,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; /** * This {@link StatusCodeTypeUtils} class provides common functionalities for mapping the Ballerina HTTP status code @@ -73,8 +74,7 @@ static TypeSymbol getBodyType(RecordTypeSymbol responseRecordType, SemanticModel return semanticModel.types().ANYDATA; } - static Map getHeaders(RecordTypeSymbol responseRecordType, - TypeMapper typeMapper) { + static Map getHeaders(RecordTypeSymbol responseRecordType, TypeMapper typeMapper) { if (Objects.isNull(responseRecordType)) { return new HashMap<>(); } @@ -86,9 +86,10 @@ static Map getHeaders(RecordTypeSymbol responseRecordType, Map recordFieldMap = new HashMap<>(headersInfo.recordType(). fieldDescriptors()); - Map recordFieldsMapping = typeMapper.getSchemaForRecordFields(recordFieldMap, new HashSet<>(), + Set requiredFields = new HashSet<>(); + Map recordFieldsMapping = typeMapper.getSchemaForRecordFields(recordFieldMap, requiredFields, headersInfo.recordName(), false); - return mapRecordFieldToHeaders(recordFieldsMapping); + return mapRecordFieldToHeaders(recordFieldsMapping, requiredFields); } private static HeaderRecordInfo getHeadersInfo(RecordTypeSymbol responseRecordType, TypeMapper typeMapper) { @@ -105,12 +106,16 @@ private static HeaderRecordInfo getHeadersInfo(RecordTypeSymbol responseRecordTy return null; } - private static Map mapRecordFieldToHeaders(Map recordFields) { + private static Map mapRecordFieldToHeaders(Map recordFields, + Set requiredFields) { Map headers = new HashMap<>(); for (Map.Entry entry : recordFields.entrySet()) { Header header = new Header(); header.setSchema(entry.getValue()); headers.put(entry.getKey(), header); + if (requiredFields.contains(entry.getKey())) { + header.setRequired(true); + } } return headers; } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java index 29947fd53..52e0d2deb 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java @@ -178,7 +178,8 @@ private static Optional getRecordFieldDefaultValue(String fieldName, RecordFieldWithDefaultValueNode defaultValueNode = recordFields.stream() .filter(field -> field instanceof RecordFieldWithDefaultValueNode) .map(field -> (RecordFieldWithDefaultValueNode) field) - .filter(field -> field.fieldName().toString().trim().equals(fieldName)).findFirst().orElse(null); + .filter(field -> MapperCommonUtils.unescapeIdentifier(field.fieldName().toString().trim()). + equals(fieldName)).findFirst().orElse(null); if (Objects.isNull(defaultValueNode)) { return Optional.empty(); } From 71eb56eaa15de625cbd9a7cb802f692079794b00 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 13 Feb 2024 14:54:36 +0530 Subject: [PATCH 12/12] Fix test cases --- .../ballerina-to-openapi/expected_gen/post_method.yaml | 2 ++ .../expected_gen/response/rs_with_headers.yaml | 4 ++++ .../expected_gen/response/typeInclusion_01.yaml | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/post_method.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/post_method.yaml index e02c0c708..b5df3c8f7 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/post_method.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/post_method.yaml @@ -79,9 +79,11 @@ paths: description: Ok headers: header2: + required: true schema: type: string header1: + required: true schema: type: string content: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/rs_with_headers.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/rs_with_headers.yaml index 8ea4e8a0d..3addeb0f6 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/rs_with_headers.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/rs_with_headers.yaml @@ -18,18 +18,22 @@ paths: description: Ok headers: x-rate-limit-enable: + required: true schema: type: boolean x-rate-limit-remaining: + required: true schema: type: integer format: int64 x-rate-limit-types: + required: true schema: type: array items: type: string x-rate-limit-id: + required: true schema: type: string content: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/typeInclusion_01.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/typeInclusion_01.yaml index d6ad9643c..f35215aee 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/typeInclusion_01.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/typeInclusion_01.yaml @@ -63,20 +63,24 @@ paths: description: Created headers: payment-type: + required: true schema: type: string simple-payment-tokens: + required: true schema: type: array items: type: string simple-payment-type: + required: true schema: type: string location: schema: type: string payment-tokens: + required: true schema: type: array items: @@ -120,20 +124,24 @@ paths: description: Created headers: payment-type: + required: true schema: type: string simple-payment-tokens: + required: true schema: type: array items: type: string simple-payment-type: + required: true schema: type: string location: schema: type: string payment-tokens: + required: true schema: type: array items: