diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ResourceMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ResourceMapperImpl.java index 5455bbccc..529c7930a 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ResourceMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ResourceMapperImpl.java @@ -174,26 +174,19 @@ private void addPathItem(String httpMethod, Paths path, Operation operation, Str * * @return Operation Adaptor object of given resource */ - private Optional convertResourceToOperation(FunctionDefinitionNode resource, String httpMethod, - String generateRelativePath, + private Optional convertResourceToOperation(FunctionDefinitionNode resourceFunction, + String httpMethod, String generateRelativePath, Components components) { OperationInventory operationInventory = new OperationInventory(); operationInventory.setHttpOperation(httpMethod); operationInventory.setPath(generateRelativePath); - /* Set operation id */ - String resName = (resource.functionName().text() + "_" + - generateRelativePath).replaceAll("\\{///}", "_"); - - if (generateRelativePath.equals("/")) { - resName = resource.functionName().text(); - } - operationInventory.setOperationId(getOperationId(resName)); + operationInventory.setOperationId(getOperationId(resourceFunction)); // Set operation summary // Map API documentation - Map apiDocs = listAPIDocumentations(resource, operationInventory); + Map apiDocs = listAPIDocumentations(resourceFunction, operationInventory); //Add path parameters if in path and query parameters - ParameterMapper parameterMapper = new ParameterMapperImpl(resource, operationInventory, components, apiDocs, - additionalData, treatNilableAsOptional); + ParameterMapper parameterMapper = new ParameterMapperImpl(resourceFunction, operationInventory, components, + apiDocs, additionalData, treatNilableAsOptional); parameterMapper.setParameters(); List diagnostics = additionalData.diagnostics(); if (diagnostics.size() > 1 || (diagnostics.size() == 1 && !diagnostics.get(0).getCode().equals( @@ -207,7 +200,7 @@ private Optional convertResourceToOperation(FunctionDefiniti } } - ResponseMapper responseMapper = new ResponseMapperImpl(resource, operationInventory, components, + ResponseMapper responseMapper = new ResponseMapperImpl(resourceFunction, operationInventory, components, additionalData); responseMapper.setApiResponses(); return Optional.of(operationInventory); diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ServiceToOpenAPIMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ServiceToOpenAPIMapper.java index c24cc2435..4ebb98c5c 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ServiceToOpenAPIMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ServiceToOpenAPIMapper.java @@ -35,6 +35,8 @@ import io.ballerina.openapi.service.mapper.diagnostic.DiagnosticMessages; import io.ballerina.openapi.service.mapper.diagnostic.ExceptionDiagnostic; import io.ballerina.openapi.service.mapper.diagnostic.OpenAPIMapperDiagnostic; +import io.ballerina.openapi.service.mapper.hateoas.HateoasMapper; +import io.ballerina.openapi.service.mapper.hateoas.HateoasMapperImpl; import io.ballerina.openapi.service.mapper.model.AdditionalData; import io.ballerina.openapi.service.mapper.model.ModuleMemberVisitor; import io.ballerina.openapi.service.mapper.model.OASGenerationMetaInfo; @@ -209,6 +211,8 @@ public static OASResult generateOAS(OASGenerationMetaInfo oasGenerationMetaInfo) ConstraintMapper constraintMapper = new ConstraintMapperImpl(openapi, moduleMemberVisitor, diagnostics); constraintMapper.setConstraints(); + HateoasMapper hateoasMapper = new HateoasMapperImpl(); + hateoasMapper.setOpenApiLinks(serviceDefinition, openapi); return new OASResult(openapi, diagnostics); } else { return new OASResult(openapi, oasResult.getDiagnostics()); diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/constraint/Constants.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/constraint/Constants.java index ceff94309..317ba06f2 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/constraint/Constants.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/constraint/Constants.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * 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.constraint; /** diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Constants.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Constants.java new file mode 100644 index 000000000..08a4768aa --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Constants.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * 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.hateoas; + +public class Constants { + + public static final String OPENAPI_LINK_DEFAULT_REL = "_self"; + public static final String BALLERINA_LINKEDTO_KEYWORD = "linkedTo"; +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasLink.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasLink.java new file mode 100644 index 000000000..46e04e16f --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasLink.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * 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.hateoas; + +public class HateoasLink { + private String resourceName; + private String relation; + private String resourceMethod; + + public String getResourceName() { + return resourceName; + } + + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + public String getRel() { + return relation; + } + + public void setRel(String relation) { + this.relation = relation; + } + + public String getResourceMethod() { + return resourceMethod; + } + + public void setResourceMethod(String resourceMethod) { + this.resourceMethod = resourceMethod; + } +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasMapper.java new file mode 100644 index 000000000..c31243f55 --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * 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.hateoas; + +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.swagger.v3.oas.models.OpenAPI; + +/** + * This {@link HateoasMapper} uses to set HATEOAS links into the OpenAPI context. + * + * @since 1.9.0 + */ +public interface HateoasMapper { + + /** + * Sets HATEOAS links into the OpenAPI context. + * + * @param serviceNode Specific service declaration node + * @param openAPI Current OpenAPI context + */ + void setOpenApiLinks(ServiceDeclarationNode serviceNode, OpenAPI openAPI); +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasMapperImpl.java new file mode 100644 index 000000000..883ca02f4 --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/HateoasMapperImpl.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * 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.hateoas; + +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.openapi.service.mapper.Constants; +import io.ballerina.openapi.service.mapper.utils.MapperCommonUtils; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.links.Link; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static io.ballerina.openapi.service.mapper.hateoas.Constants.BALLERINA_LINKEDTO_KEYWORD; +import static io.ballerina.openapi.service.mapper.hateoas.Constants.OPENAPI_LINK_DEFAULT_REL; +import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.generateRelativePath; +import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getResourceConfigAnnotation; +import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getValueForAnnotationFields; + +/** + * This {@link HateoasMapperImpl} class represents the implementation of the {@link HateoasMapper}. + * + * @since 1.9.0 + */ +public class HateoasMapperImpl implements HateoasMapper { + + @Override + public void setOpenApiLinks(ServiceDeclarationNode serviceNode, OpenAPI openAPI) { + Paths paths = openAPI.getPaths(); + Service hateoasService = extractHateoasMetaInfo(serviceNode); + for (Node node: serviceNode.members()) { + if (!node.kind().equals(SyntaxKind.RESOURCE_ACCESSOR_DEFINITION)) { + continue; + } + FunctionDefinitionNode resource = (FunctionDefinitionNode) node; + Optional responses = getApiResponsesForResource(resource, paths); + if (responses.isEmpty()) { + continue; + } + setOpenApiLinksInApiResponse(hateoasService, resource, responses.get()); + } + } + + private Service extractHateoasMetaInfo(ServiceDeclarationNode serviceNode) { + Service service = new Service(); + for (Node child : serviceNode.children()) { + if (SyntaxKind.RESOURCE_ACCESSOR_DEFINITION.equals(child.kind())) { + FunctionDefinitionNode resourceFunction = (FunctionDefinitionNode) child; + String resourceMethod = resourceFunction.functionName().text(); + String operationId = MapperCommonUtils.getOperationId(resourceFunction); + Optional resourceName = getResourceConfigAnnotation(resourceFunction) + .flatMap(resourceConfig -> getValueForAnnotationFields(resourceConfig, "name")); + if (resourceName.isEmpty()) { + continue; + } + String cleanedResourceName = resourceName.get().replaceAll("\"", ""); + Resource hateoasResource = new Resource(resourceMethod, operationId); + service.addResource(cleanedResourceName, hateoasResource); + } + } + return service; + } + + private Optional getApiResponsesForResource(FunctionDefinitionNode resource, Paths paths) { + String resourcePath = MapperCommonUtils.unescapeIdentifier(generateRelativePath(resource)); + if (!paths.containsKey(resourcePath)) { + return Optional.empty(); + } + PathItem openApiResource = paths.get(resourcePath); + String httpMethod = resource.functionName().toString().trim(); + ApiResponses responses = null; + switch (httpMethod.trim().toUpperCase(Locale.ENGLISH)) { + case Constants.GET -> { + responses = openApiResource.getGet().getResponses(); + } + case Constants.PUT -> { + responses = openApiResource.getPut().getResponses(); + } + case Constants.POST -> { + responses = openApiResource.getPost().getResponses(); + } + case Constants.DELETE -> { + responses = openApiResource.getDelete().getResponses(); + } + case Constants.OPTIONS -> { + responses = openApiResource.getOptions().getResponses(); + } + case Constants.PATCH -> { + responses = openApiResource.getPatch().getResponses(); + } + case Constants.HEAD -> { + responses = openApiResource.getHead().getResponses(); + } + default -> { } + } + return Optional.ofNullable(responses); + } + + private void setOpenApiLinksInApiResponse(Service hateoasService, FunctionDefinitionNode resource, + ApiResponses apiResponses) { + Map swaggerLinks = mapHateoasLinksToOpenApiLinks(hateoasService, resource); + if (swaggerLinks.isEmpty()) { + return; + } + for (Map.Entry entry : apiResponses.entrySet()) { + int statusCode = Integer.parseInt(entry.getKey()); + if (statusCode >= 200 && statusCode < 300) { + entry.getValue().setLinks(swaggerLinks); + } + } + } + + private List getLinks(String linkedTo) { + List links = new ArrayList<>(); + String[] linkArray = linkedTo.replaceAll("[\\[\\]]", "").split("\\},\\s*"); + for (String linkString : linkArray) { + HateoasLink link = parseHateoasLink(linkString); + links.add(link); + } + return links; + } + + private HateoasLink parseHateoasLink(String input) { + HateoasLink hateoasLink = new HateoasLink(); + HashMap keyValueMap = new HashMap<>(); + String[] keyValuePairs = input.replaceAll("[{}]", "").split(",\\s*"); + for (String pair : keyValuePairs) { + String[] parts = pair.split(":\\s*"); + if (parts.length != 2) { + continue; + } + String key = parts[0].trim(); + String value = parts[1].replaceAll("\"", "").trim(); + keyValueMap.put(key, value); + } + hateoasLink.setResourceName(keyValueMap.get("name")); + hateoasLink.setRel(keyValueMap.getOrDefault("relation", OPENAPI_LINK_DEFAULT_REL)); + hateoasLink.setResourceMethod(keyValueMap.get("method")); + return hateoasLink; + } + + private Map mapHateoasLinksToOpenApiLinks(Service hateoasService, + FunctionDefinitionNode resourceFunction) { + Optional linkedTo = getResourceConfigAnnotation(resourceFunction) + .flatMap(resourceConfig -> getValueForAnnotationFields(resourceConfig, BALLERINA_LINKEDTO_KEYWORD)); + if (linkedTo.isEmpty()) { + return Collections.emptyMap(); + } + List links = getLinks(linkedTo.get()); + Map hateoasLinks = new HashMap<>(); + for (HateoasLink link : links) { + Optional resource = hateoasService.getHateoasResourceMapping().entrySet().stream() + .filter(resources -> link.getResourceName().equals(resources.getKey())) + .findFirst() + .flatMap(hateoasResourceMapping -> hateoasResourceMapping.getValue().stream() + .filter(hateoasRes -> link.getResourceMethod().equals(hateoasRes.resourceMethod())) + .findFirst()); + if (resource.isEmpty()) { + continue; + } + Link openapiLink = new Link(); + String operationId = resource.get().operationId(); + openapiLink.setOperationId(operationId); + hateoasLinks.put(link.getRel(), openapiLink); + } + return hateoasLinks; + } +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Resource.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Resource.java new file mode 100644 index 000000000..0eb33d695 --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Resource.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * 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.hateoas; + +/** + * A context-holder to save and retrieve HATEOAS meta-data for a given resources. + * @param resourceMethod http resource for the method + * @param operationId generated operationId + * @since 1.9.0 + */ +public record Resource(String resourceMethod, String operationId) { +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Service.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Service.java new file mode 100644 index 000000000..11f8ac1b6 --- /dev/null +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/hateoas/Service.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * 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.hateoas; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A data class to hold service level HATEOAS meta-data. + * + * @since 1.6.0 + */ +public class Service { + private final Map> hateoasResourceMapping = new HashMap<>(); + + public void addResource(String resourceName, Resource resource) { + if (hateoasResourceMapping.containsKey(resourceName)) { + hateoasResourceMapping.get(resourceName).add(resource); + return; + } + hateoasResourceMapping.put(resourceName, Arrays.asList(resource)); + } + + public Map> getHateoasResourceMapping() { + return hateoasResourceMapping; + } +} diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapper.java index 1673466a9..941e3c315 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/response/ResponseMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/utils/MapperCommonUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/utils/MapperCommonUtils.java index 2e8732579..65e23bb78 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/utils/MapperCommonUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/utils/MapperCommonUtils.java @@ -36,12 +36,15 @@ import io.ballerina.compiler.syntax.tree.AnnotationNode; import io.ballerina.compiler.syntax.tree.BasicLiteralNode; import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; import io.ballerina.compiler.syntax.tree.ListConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; +import io.ballerina.compiler.syntax.tree.MetadataNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; +import io.ballerina.compiler.syntax.tree.ResourcePathParameterNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; import io.ballerina.compiler.syntax.tree.SpecificFieldNode; @@ -97,19 +100,46 @@ public class MapperCommonUtils { private static final SyntaxKind[] validExpressionKind = {STRING_LITERAL, NUMERIC_LITERAL, BOOLEAN_LITERAL, LIST_CONSTRUCTOR, MAPPING_CONSTRUCTOR}; + public static String generateRelativePath(FunctionDefinitionNode resourceFunction) { + StringBuilder relativePath = new StringBuilder(); + relativePath.append("/"); + if (!resourceFunction.relativeResourcePath().isEmpty()) { + for (Node node: resourceFunction.relativeResourcePath()) { + if (node instanceof ResourcePathParameterNode pathNode) { + relativePath.append("{"); + relativePath.append(pathNode.paramName().get()); + relativePath.append("}"); + } else if ((resourceFunction.relativeResourcePath().size() == 1) + && (node.toString().trim().equals("."))) { + return relativePath.toString(); + } else { + relativePath.append(node.toString().trim()); + } + } + } + return relativePath.toString(); + } + /** * Generate operationId by removing special characters. * - * @param operationID input function name, record name or operation Id - * @return string with new generated name + * @param resourceFunction resource function definition + * @return string with a unique operationId */ - public static String getOperationId(String operationID) { + public static String getOperationId(FunctionDefinitionNode resourceFunction) { //For the flatten enable we need to remove first Part of valid name check // this - > !operationID.matches("\\b[a-zA-Z][a-zA-Z0-9]*\\b") && - if (operationID.matches("\\b[0-9]*\\b")) { - return operationID; + String relativePath = MapperCommonUtils.generateRelativePath(resourceFunction); + String cleanResourcePath = MapperCommonUtils.unescapeIdentifier(relativePath); + String resName = (resourceFunction.functionName().text() + "_" + + cleanResourcePath).replaceAll("\\{///\\}", "_"); + if (cleanResourcePath.equals("/")) { + resName = resourceFunction.functionName().text(); } - String[] split = operationID.split(Constants.SPECIAL_CHAR_REGEX); + if (resName.matches("\\b[0-9]*\\b")) { + return resName; + } + String[] split = resName.split(Constants.SPECIAL_CHAR_REGEX); StringBuilder validName = new StringBuilder(); for (String part : split) { if (!part.isBlank()) { @@ -120,8 +150,8 @@ public static String getOperationId(String operationID) { validName.append(part); } } - operationID = validName.toString(); - return operationID.substring(0, 1).toLowerCase(Locale.ENGLISH) + operationID.substring(1); + resName = validName.toString(); + return resName.substring(0, 1).toLowerCase(Locale.ENGLISH) + resName.substring(1); } /** @@ -416,4 +446,31 @@ public static boolean isNotSimpleValueLiteralKind(SyntaxKind valueExpressionKind return Arrays.stream(validExpressionKind).noneMatch(syntaxKind -> syntaxKind == valueExpressionKind); } + + public static Optional getResourceConfigAnnotation(FunctionDefinitionNode resourceFunction) { + Optional metadata = resourceFunction.metadata(); + if (metadata.isEmpty()) { + return Optional.empty(); + } + MetadataNode metaData = metadata.get(); + NodeList annotations = metaData.annotations(); + return annotations.stream() + .filter(ann -> "http:ResourceConfig".equals(ann.annotReference().toString().trim())) + .findFirst(); + } + + public static Optional getValueForAnnotationFields(AnnotationNode resourceConfigAnnotation, + String fieldName) { + return resourceConfigAnnotation + .annotValue() + .map(MappingConstructorExpressionNode::fields) + .flatMap(fields -> + fields.stream() + .filter(fld -> fld instanceof SpecificFieldNode) + .map(fld -> (SpecificFieldNode) fld) + .filter(fld -> fieldName.equals(fld.fieldName().toString().trim())) + .findFirst() + ).flatMap(SpecificFieldNode::valueExpr) + .map(en -> en.toString().trim()); + } } diff --git a/docs/ballerina-to-oas/proposals/hateoas_support.md b/docs/ballerina-to-oas/proposals/hateoas_support.md new file mode 100644 index 000000000..25ac94a0e --- /dev/null +++ b/docs/ballerina-to-oas/proposals/hateoas_support.md @@ -0,0 +1,164 @@ +# Proposal: Map Ballerina HATEOAS Links to OpenAPI Specification + +_Owners_: @SachinAkash01 +_Reviewers_: @lnash94 @TharmiganK +_Created_: 2023/10/25 +_Updated_: 2023/10/31 +_Issue_: [#4788](https://github.com/ballerina-platform/ballerina-library/issues/4788) + +## Summary +This proposal is to introduce the capabilities to support HATEOAS (Hypermedia As The Engine Of Application State) links +in the generated OpenAPI specification of the Ballerina OpenAPI tool. Note that in the rest of the proposal HATEOAS is +referred to as Hypermedia constraint. + +## Goals +- Enhance the Ballerina OpenAPI tool by adding support for Hypermedia constraints in the generated OpenAPI specification. + +## Motivation +OpenAPI is a widely used standard for documenting APIs. It allows developers to specify the structure and data types +expected in API requests and responses. + +Hypermedia constraints are one of the key principles in REST though it is often ignored. This principle emphasizes the +interconnection of resources, offering API users an experience similar to navigating the web. The existing tool for +generating OpenAPI specification from Ballerina code lacks support for including these Hypermedia constraints in the +OpenAPI specification. This means that when developers use Hypermedia constraints in their Ballerina code, this +information is not properly reflected in the OpenAPI specification. + +Overall, improving the OpenAPI tool to support Hypermedia constraints of the Ballerina programming language in the +generated OpenAPI specification is a worthwhile investment that will improve the overall developer experience. + +## Description +There are two ways of generating links between the resources in Ballerina, manually defining links between resources and +automatically generating links between resources. The feature implementation focuses on integrating Ballerina +Hypermedia constraints into generated OpenAPI specifications to accurately document the API. In this proposal we will +be addressing the automatic linking between the resources using `ResourceConfig` annotation in Ballerina. + +Currently in manual linking approach, we are generating the response component schema by including `*http:Links`. +There will be no changes to this behaviour of generating the OpenAPI specification. + +**Sample Ballerina Code (Manually defining links between resources):** +```ballerina +import ballerina/http; + +public type Link record { + string rel?; + string href; + string types?; + string methods?; +}; + +public type Location record {| + *http:Links; + string name; + string id; + string address; +|}; + +service /snowpeak on new http:Listener(9090) { + resource function get locations() returns Location { + return { + name: "Alps", + id: "l100", + address: "Switzerland", + _links: { + room: { + href: "/snowpeak/locations/{id}/rooms", + methods: ["GET"] + } + } + }; + } +} +``` + +**OpenAPI Specification (response component schema including the `*http:Links`):** +```yaml +Location: + - required: + - address + - id + - name + type: object + properties: + name: + type: string + description: Name of the location + id: + type: string + description: Unique identification + address: + type: string + description: Address of the location + links: + type: array + items: + type: object + properties: + rel: + type: string + default: "room" + href: + type: string + default: "/snowpeak/locations/{id}/rooms" + method: + type: string + default: "GET" +``` + +Following is the proposed way of reflecting Ballerina Hypermedia constraints using `ResourceConfig` annotation into +OpenAPI specification. + +**Sample Ballerina Code:** +```ballerina +service /snowpeak on new http:Listener(port) { + + @http:ResourceConfig { + name: "Locations", + linkedTo: [ {name: "Rooms", relation: "room", method: "get"} ] + } + resource function get locations() returns @http:Cache rep:Locations|rep:SnowpeakInternalError { + // some logic + } + + @http:ResourceConfig { + name: "Rooms" + } + resource function get locations/[string id]/rooms(string startDate, string endDate) + returns rep:Rooms|rep:SnowpeakInternalError { + // some logic + } +} +``` + +**Generated OpenAPI Specification:** +```yaml +paths: + /locations: + get: + operationId: getLocations + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + links: + room: + operationId: getLocationsIdRooms + /locations/{id}/rooms: + get: + operationId: getLocationsIdRooms + parameters: +``` + +> [!NOTE] +> Since OpenAPI doesn't support HATEOAS directly, there is no proper and direct way to reflect resource name +`@http:ResourceConfig { name: "Locations"}` in OpenAPI specification. This might be a problem when we generate +Ballerina service stub from OAS. + +## Testing +- Unit Testing: Evaluate the individual components and core logic of the Ballerina OpenAPI tool, focusing on functions, +methods, and modules to ensure correctness and Hypermedia constraint handling. +- Integration Testing: Assess the interaction and collaborations between various modules and components of the tool, +verifying the seamless integration of Hypermedia constraints into the OpenAPI specification. diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/HateoasTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/HateoasTests.java new file mode 100644 index 000000000..985cfea8f --- /dev/null +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/HateoasTests.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * 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.generators.openapi; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class HateoasTests { + private static final Path RES_DIR = Paths.get("src/test/resources/ballerina-to-openapi").toAbsolutePath(); + + @Test(description = "Automatic linking between resource functions") + public void testHateoasAutomaticLinking() throws IOException { + Path ballerinafilePath = RES_DIR.resolve("hateoas/hateoas_automatic_linking.bal"); + TestUtils.compareWithGeneratedFile(ballerinafilePath, "hateoas/hateoas_automatic_linking.yaml"); + } + + @Test(description = "Multiple reference links to a resource") + public void testHateoasMultipleLinks() throws IOException { + Path ballerinafilePath = RES_DIR.resolve("hateoas/hateoas_multiple_links.bal"); + TestUtils.compareWithGeneratedFile(ballerinafilePath, "hateoas/hateoas_multiple_links.yaml"); + } + + @Test(description = "Self relation to a resource (default relation value)") + public void testHateoasSelfRelation() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("hateoas/hateoas_self_rel.bal"); + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "hateoas/hateoas_self_rel.yaml"); + } + + @Test(description = "Hateoas with snowpeak example") + public void testHateoasSnowpeakExample() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("hateoas/snowpeak_hateoas.bal"); + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "hateoas/snowpeak_hateoas.yaml"); + } +} diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_automatic_linking.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_automatic_linking.yaml new file mode 100644 index 000000000..eadac54dd --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_automatic_linking.yaml @@ -0,0 +1,87 @@ +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: + /locations: + get: + operationId: getLocations + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + links: + room: + operationId: getLocationsIdRooms + /locations/{id}/rooms: + get: + operationId: getLocationsIdRooms + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "202": + description: Accepted + "500": + description: InternalServerError + content: + application/json: + schema: + $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 + Location: + required: + - address + - id + - name + type: object + properties: + name: + type: string + id: + type: string + address: + type: string diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_multiple_links.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_multiple_links.yaml new file mode 100644 index 000000000..ae68b01bc --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_multiple_links.yaml @@ -0,0 +1,113 @@ +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: + /locations: + get: + operationId: getLocations + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + links: + payment: + operationId: getLocationsIdPayments + room: + operationId: getLocationsIdRooms + /locations/{id}/rooms: + get: + operationId: getLocationsIdRooms + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "202": + description: Accepted + "500": + description: InternalServerError + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + "400": + description: BadRequest + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + /locations/{id}/payments: + get: + operationId: getLocationsIdPayments + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "202": + description: Accepted + "500": + description: InternalServerError + content: + application/json: + schema: + $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 + Location: + required: + - address + - id + - name + type: object + properties: + name: + type: string + id: + type: string + address: + type: string diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_self_rel.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_self_rel.yaml new file mode 100644 index 000000000..04736cbfd --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/hateoas_self_rel.yaml @@ -0,0 +1,87 @@ +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: + /locations: + get: + operationId: getLocations + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + links: + _self: + operationId: getLocationsIdRooms + /locations/{id}/rooms: + get: + operationId: getLocationsIdRooms + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "202": + description: Accepted + "500": + description: InternalServerError + content: + application/json: + schema: + $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 + Location: + required: + - address + - id + - name + type: object + properties: + name: + type: string + id: + type: string + address: + type: string diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/snowpeak_hateoas.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/snowpeak_hateoas.yaml new file mode 100644 index 000000000..463fdadb1 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/hateoas/snowpeak_hateoas.yaml @@ -0,0 +1,210 @@ +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: + /locations: + get: + operationId: getLocations + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Location' + links: + room: + operationId: getLocationsIdRooms + /locations/{id}/rooms: + get: + operationId: getLocationsIdRooms + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: startDate + in: query + required: true + schema: + type: string + - name: endDate + in: query + required: true + schema: + type: string + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Room' + links: + reservation: + operationId: postReservations + "400": + description: BadRequest + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + /reservations: + post: + operationId: postReservations + responses: + "201": + description: Created + content: + text/plain: + schema: + type: string + links: + cancel: + operationId: deleteReservationsId + edit: + operationId: putReservationsId + payment: + operationId: postReservationsId + /reservations/{id}: + put: + operationId: putReservationsId + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Ok + content: + text/plain: + schema: + type: string + "400": + description: BadRequest + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + post: + operationId: postReservationsId + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "201": + description: Created + content: + text/plain: + schema: + type: string + "400": + description: BadRequest + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorPayload' + delete: + operationId: deleteReservationsId + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Ok + content: + text/plain: + schema: + type: string + "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 + Location: + required: + - address + - id + - name + type: object + properties: + name: + type: string + id: + type: string + address: + type: string + Room: + required: + - category + - count + - currency + - id + - price + - status + - wifi + type: object + properties: + id: + type: string + category: + type: string + wifi: + type: boolean + status: + type: string + currency: + type: string + price: + type: integer + format: int64 + count: + type: integer + format: int64 diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_automatic_linking.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_automatic_linking.bal new file mode 100644 index 000000000..1609648bd --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_automatic_linking.bal @@ -0,0 +1,40 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). +// +// 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. + +import ballerina/http; + +public type Location record { + string name; + string id; + string address; +}; + +service /payloadV on new http:Listener(9090) { + @http:ResourceConfig { + name: "Locations", + linkedTo: [ {name: "Rooms", relation: "room", method: "get"} ] + } + resource function get locations() returns Location { + return {name: "Cinnamon Lodge", id: "1001", address: "Habarana"}; + } + + @http:ResourceConfig { + name: "Rooms" + } + resource function get locations/[string id]/rooms() returns error? { + return; + } +} diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_multiple_links.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_multiple_links.bal new file mode 100644 index 000000000..42c65ae2b --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_multiple_links.bal @@ -0,0 +1,48 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). +// +// 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. + +import ballerina/http; + +public type Location record { + string name; + string id; + string address; +}; + +service /payloadV on new http:Listener(9090) { + @http:ResourceConfig { + name: "Locations", + linkedTo: [ {name: "Rooms", relation: "room", method: "get"}, + {name: "Payments", relation: "payment", method: "get"}] + } + resource function get locations() returns Location { + return {name: "Cinnamon Lodge", id: "1001", address: "Habarana"}; + } + + @http:ResourceConfig { + name: "Rooms" + } + resource function get locations/[string id]/rooms() returns error? { + return; + } + + @http:ResourceConfig { + name: "Payments" + } + resource function get locations/[string id]/payments() returns error? { + return; + } +} diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_self_rel.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_self_rel.bal new file mode 100644 index 000000000..60dc804fd --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/hateoas_self_rel.bal @@ -0,0 +1,40 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). +// +// 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. + +import ballerina/http; + +public type Location record { + string name; + string id; + string address; +}; + +service /payloadV on new http:Listener(9090) { + @http:ResourceConfig { + name: "Locations", + linkedTo: [ {name: "Rooms", method: "get"} ] + } + resource function get locations() returns Location { + return {name: "Cinnamon Lodge", id: "1001", address: "Habarana"}; + } + + @http:ResourceConfig { + name: "Rooms" + } + resource function get locations/[string id]/rooms() returns error? { + return; + } +} diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/snowpeak_hateoas.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/snowpeak_hateoas.bal new file mode 100644 index 000000000..1ad07caaf --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/hateoas/snowpeak_hateoas.bal @@ -0,0 +1,92 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). +// +// 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. + +import ballerina/http; + +public type Location record { + string name; + string id; + string address; +}; + +public type Room record { + string id; + string category; + boolean wifi; + string status; + string currency; + int price; + int count; +}; + +service /payloadV on new http:Listener(9090) { + @http:ResourceConfig { + name: "Locations", + linkedTo: [ {name: "Rooms", relation: "room", method: "get"}] + } + resource function get locations() returns Location[] { + return [{name: "Alps", id: "l1000", address: "NC 29384, some place, switzerland"}, + {name: "Pilatus", id: "l2000", address: "NC 29444, some place, switzerland"}]; + } + + @http:ResourceConfig { + name: "Rooms", + linkedTo: [{name: "Reservations", relation: "reservation", method: "post"}] + } + resource function get locations/[string id]/rooms(string startDate, string endDate) returns Room[] { + return [{ + "id": "r1000", + "category": "DELUXE", + "capacity": 5, + "wifi": true, + "status": "AVAILABLE", + "currency": "USD", + "price": 200, + "count": 3 + }]; + } + + @http:ResourceConfig { + name: "Reservations", + linkedTo: [ {name: "DeleteReservation", relation: "cancel", method: "delete"}, + {name: "UpdateReservation", relation: "edit", method: "put"}, + {name: "PayReservation", relation: "payment", method: "post"}] + } + resource function post reservations() returns string { + return "Room Reserved!"; + } + + @http:ResourceConfig { + name: "DeleteReservation" + } + resource function delete reservations/[string id]() returns string { + return "Reservation Deleted!"; + } + + @http:ResourceConfig { + name: "UpdateReservation" + } + resource function put reservations/[string id]() returns string { + return "Reservation Updated!"; + } + + @http:ResourceConfig { + name: "PayReservation" + } + resource function post reservations/[string id]() returns string { + return "Payment Successful!"; + } +} diff --git a/openapi-cli/src/test/resources/testng.xml b/openapi-cli/src/test/resources/testng.xml index b70c7e5b3..5273f05fb 100644 --- a/openapi-cli/src/test/resources/testng.xml +++ b/openapi-cli/src/test/resources/testng.xml @@ -47,6 +47,7 @@ under the License. +