diff --git a/flow-model-generator/modules/flow-model-generator-core/build.gradle b/flow-model-generator/modules/flow-model-generator-core/build.gradle index f8101b754..f17c7357f 100644 --- a/flow-model-generator/modules/flow-model-generator-core/build.gradle +++ b/flow-model-generator/modules/flow-model-generator-core/build.gradle @@ -51,6 +51,14 @@ dependencies { balTools("org.ballerinalang:jballerina-tools:${ballerinaLangVersion}") { transitive = false } + + implementation "io.ballerina.openapi:core:2.1.1-20240926-171100-1f88ade" + implementation ("io.swagger.parser.v3:swagger-parser:${swaggerParserVersion}") { + exclude group: "io.swagger", module: "swagger-compat-spec-parser" + exclude group: "org.slf4j", module: "slf4j-ext" + exclude group: "javax.validation", module: "validation-api" + } + implementation "io.swagger.core.v3:swagger-models" } def balDistribution = file("$project.buildDir/extracted-distribution/jballerina-tools-${ballerinaLangVersion}") diff --git a/flow-model-generator/modules/flow-model-generator-core/src/main/java/io/ballerina/flowmodelgenerator/core/OpenApiServiceGenerator.java b/flow-model-generator/modules/flow-model-generator-core/src/main/java/io/ballerina/flowmodelgenerator/core/OpenApiServiceGenerator.java new file mode 100644 index 000000000..f54803f8a --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-core/src/main/java/io/ballerina/flowmodelgenerator/core/OpenApiServiceGenerator.java @@ -0,0 +1,315 @@ +/* + * 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.flowmodelgenerator.core; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.MethodSymbol; +import io.ballerina.compiler.api.symbols.ModuleSymbol; +import io.ballerina.compiler.api.symbols.ObjectTypeSymbol; +import io.ballerina.compiler.api.symbols.ResourceMethodSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.SymbolKind; +import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; +import io.ballerina.compiler.api.symbols.TypeDescKind; +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.openapi.core.generators.common.GeneratorUtils; +import io.ballerina.openapi.core.generators.common.TypeHandler; +import io.ballerina.openapi.core.generators.common.exception.BallerinaOpenApiException; +import io.ballerina.openapi.core.generators.common.model.Filter; +import io.ballerina.openapi.core.generators.common.model.GenSrcFile; +import io.ballerina.openapi.core.generators.service.ServiceGenerationHandler; +import io.ballerina.openapi.core.generators.service.model.OASServiceMetadata; +import io.ballerina.openapi.core.generators.type.GeneratorConstants; +import io.ballerina.projects.Document; +import io.ballerina.tools.diagnostics.Diagnostic; +import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import io.ballerina.tools.text.LinePosition; +import io.ballerina.tools.text.LineRange; +import io.swagger.v3.oas.models.OpenAPI; +import org.ballerinalang.formatter.core.Formatter; +import org.ballerinalang.formatter.core.FormatterException; +import org.ballerinalang.langserver.commons.eventsync.exceptions.EventSyncException; +import org.ballerinalang.langserver.commons.workspace.WorkspaceDocumentException; +import org.ballerinalang.langserver.commons.workspace.WorkspaceManager; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.ballerina.openapi.core.generators.common.GeneratorConstants.DEFAULT_FILE_HEADER; + +/** + * Generates service from the OpenAPI contract. + * + * @since 1.4.0 + */ +public class OpenApiServiceGenerator { + + private final WorkspaceManager workspaceManager; + private final Path oAContractPath; + private final Path projectPath; + private final int port; + public static final List SUPPORTED_OPENAPI_VERSIONS = List.of("2.0", "3.0.0", "3.0.1", "3.0.2", "3.0.3", + "3.1.0"); + public static final String LS = System.lineSeparator(); + public static final String CLOSE_BRACE = "}"; + public static final String IMPORT = "import ballerina/http;"; + public static final String SERVICE_DECLARATION = "service OASServiceType on new http:Listener(%s) {"; + public static final String SERVICE_OBJ_FILE = "service_contract.bal"; + public static final String SERVICE_IMPL_FILE = "service_implementation.bal"; + + public OpenApiServiceGenerator(Path oAContractPath, Path projectPath, int port, WorkspaceManager workspaceManager) { + this.oAContractPath = oAContractPath; + this.projectPath = projectPath; + this.workspaceManager = workspaceManager; + this.port = port; + } + + public LineRange generateService() throws IOException, BallerinaOpenApiException, FormatterException, + WorkspaceDocumentException, EventSyncException { + Filter filter = new Filter(new ArrayList<>(), new ArrayList<>()); + + List diagnostics = new ArrayList<>(); + List genFiles = generateBallerinaService(oAContractPath, filter, diagnostics); + if (genFiles.isEmpty()) { + throw new BallerinaOpenApiException("Cannot generate service from the given OpenAPI contract."); + } + + List errorMessages = new ArrayList<>(); + for (Diagnostic diagnostic : diagnostics) { + DiagnosticSeverity severity = diagnostic.diagnosticInfo().severity(); + if (severity == DiagnosticSeverity.ERROR) { + errorMessages.add(diagnostic.message()); + } + } + + if (!errorMessages.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (String errorMessage : errorMessages) { + sb.append(DiagnosticSeverity.ERROR).append(": ").append(errorMessage).append(System.lineSeparator()); + } + throw new BallerinaOpenApiException(sb.toString()); + } + + writeGeneratedSources(genFiles, projectPath); + + Path serviceImplPath = projectPath.resolve(SERVICE_IMPL_FILE); + genServiceDeclaration(projectPath.resolve(SERVICE_OBJ_FILE), serviceImplPath); + + this.workspaceManager.loadProject(serviceImplPath); + Optional document = this.workspaceManager.document(serviceImplPath); + if (document.isEmpty()) { + throw new BallerinaOpenApiException("Invalid service implementation is generated."); + } + + return LineRange.from(SERVICE_IMPL_FILE, LinePosition.from(1, + 0), document.get().syntaxTree().rootNode().lineRange().endLine()); + } + + public List generateBallerinaService(Path openAPI, Filter filter, List diagnostics) + throws IOException, FormatterException, BallerinaOpenApiException { + OpenAPI openAPIDef = GeneratorUtils.normalizeOpenAPI(openAPI, false, false); + if (openAPIDef.getInfo() == null) { + throw new BallerinaOpenApiException("Info section of the definition file cannot be empty/null: " + + openAPI); + } + + checkOpenAPIVersion(openAPIDef); + + // Validate the service generation + List complexPaths = GeneratorUtils.getComplexPaths(openAPIDef); + if (!complexPaths.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("service generation can not be done as the openapi definition contain following complex " + + "path(s) :").append(System.lineSeparator()); + for (String path : complexPaths) { + sb.append(path).append(System.lineSeparator()); + } + throw new BallerinaOpenApiException(sb.toString()); + } + + OASServiceMetadata oasServiceMetadata = new OASServiceMetadata.Builder() + .withOpenAPI(openAPIDef) + .withFilters(filter) + .withNullable(true) + .withGenerateServiceType(false) + .withGenerateServiceContract(true) + .withGenerateWithoutDataBinding(false) + .build(); + TypeHandler.createInstance(openAPIDef, true); + ServiceGenerationHandler serviceGenerationHandler = new ServiceGenerationHandler(); + List sourceFiles = generateFilesForService(serviceGenerationHandler, oasServiceMetadata); + + diagnostics.addAll(serviceGenerationHandler.getDiagnostics()); + diagnostics.addAll(TypeHandler.getInstance().getDiagnostics()); + return sourceFiles; + } + + private List generateFilesForService(ServiceGenerationHandler serviceGenerationHandler, + OASServiceMetadata oasServiceMetadata) throws + FormatterException, BallerinaOpenApiException { + List sourceFiles = serviceGenerationHandler.generateServiceFiles(oasServiceMetadata); + String schemaSyntaxTree = Formatter.format(TypeHandler.getInstance() + .generateTypeSyntaxTree()).toSourceCode(); + if (!schemaSyntaxTree.isBlank()) { + sourceFiles.add(new GenSrcFile(GenSrcFile.GenFileType.MODEL_SRC, oasServiceMetadata.getSrcPackage(), + GeneratorConstants.TYPE_FILE_NAME, + (oasServiceMetadata.getLicenseHeader().isBlank() ? DEFAULT_FILE_HEADER : + oasServiceMetadata.getLicenseHeader()) + schemaSyntaxTree)); + } + return sourceFiles; + } + + private void writeGeneratedSources(List sources, Path srcPath) throws IOException { + List listFiles = new ArrayList<>(); + if (Files.exists(srcPath)) { + File[] files = new File(String.valueOf(srcPath)).listFiles(); + if (files != null) { + listFiles.addAll(Arrays.asList(files)); + } + } + + for (File file : listFiles) { + for (GenSrcFile gFile : sources) { + if (file.getName().equals(gFile.getFileName())) { + int duplicateCount = 0; + setGeneratedFileName(listFiles, gFile, duplicateCount); + } + } + } + + for (GenSrcFile file : sources) { + Path filePath; + if (file.getType().isOverwritable()) { + filePath = Paths.get(srcPath.resolve(file.getFileName()).toFile().getCanonicalPath()); + writeFile(filePath, file.getContent()); + } else { + filePath = srcPath.resolve(file.getFileName()); + if (Files.notExists(filePath)) { + String fileContent = file.getContent(); + writeFile(filePath, fileContent); + } + } + } + } + + public static void setGeneratedFileName(List listFiles, GenSrcFile gFile, int duplicateCount) { + for (File listFile : listFiles) { + String listFileName = listFile.getName(); + if (listFileName.contains(".") && ((listFileName.split("\\.")).length >= 2) && + (listFileName.split("\\.")[0].equals(gFile.getFileName().split("\\.")[0]))) { + duplicateCount = 1 + duplicateCount; + } + } + gFile.setFileName(gFile.getFileName().split("\\.")[0] + "." + (duplicateCount) + "." + + gFile.getFileName().split("\\.")[1]); + } + + private void genServiceDeclaration(Path serviceObjPath, Path serviceImplPath) throws IOException, + WorkspaceDocumentException, EventSyncException, BallerinaOpenApiException { + this.workspaceManager.loadProject(projectPath.resolve(serviceObjPath)); + Optional semanticModel = + this.workspaceManager.semanticModel(projectPath.resolve(serviceObjPath)); + Optional document = this.workspaceManager.document(projectPath.resolve(serviceObjPath)); + if (semanticModel.isEmpty() || document.isEmpty()) { + throw new BallerinaOpenApiException("Invalid service object is created"); + } + + TypeDefinitionSymbol symbol = getServiceTypeSymbol(semanticModel.get().moduleSymbols(), "OASServiceType"); + if (symbol == null) { + throw new BallerinaOpenApiException("Cannot find service type definition"); + } + + TypeSymbol typeSymbol = symbol.typeDescriptor(); + if (typeSymbol.typeKind() != TypeDescKind.OBJECT) { + throw new BallerinaOpenApiException("Cannot find service object type definition"); + } + + Map methodSymbolMap = ((ObjectTypeSymbol) typeSymbol).methods(); + StringBuilder serviceImpl = new StringBuilder(IMPORT); + serviceImpl.append(LS); + serviceImpl.append(String.format(SERVICE_DECLARATION, port)); + serviceImpl.append(LS); + for (Map.Entry entry : methodSymbolMap.entrySet()) { + MethodSymbol methodSymbol = entry.getValue(); + if (methodSymbol instanceof ResourceMethodSymbol resourceMethodSymbol) { + serviceImpl.append(getMethodSignature(resourceMethodSymbol, getParentModuleName(symbol))); + } + } + serviceImpl.append(CLOSE_BRACE).append(LS); + writeFile(serviceImplPath, serviceImpl.toString()); + } + + private TypeDefinitionSymbol getServiceTypeSymbol(List symbols, String name) { + for (Symbol symbol : symbols) { + if (symbol.kind() == SymbolKind.TYPE_DEFINITION) { + Optional typeName = symbol.getName(); + if (typeName.isPresent() && typeName.get().equals(name)) { + return (TypeDefinitionSymbol) symbol; + } + } + } + return null; + } + + private String getParentModuleName(Symbol symbol) { + Optional module = symbol.getModule(); + return module.map(moduleSymbol -> moduleSymbol.id().toString()).orElse(null); + } + + private String getMethodSignature(ResourceMethodSymbol resourceMethodSymbol, String parentModuleName) { + String resourceSignature = resourceMethodSymbol.signature(); + if (Objects.nonNull(parentModuleName)) { + resourceSignature = resourceSignature.replace(parentModuleName + ":", ""); + } + return LS + "\t" + sanitizePackageNames(resourceSignature) + " {" + LS + LS + "\t}" + LS; + } + + private String sanitizePackageNames(String input) { + Pattern pattern = Pattern.compile("(\\w+)/(\\w+:)(\\d+\\.\\d+\\.\\d+):"); + Matcher matcher = pattern.matcher(input); + return matcher.replaceAll("$2"); + } + + private static void writeFile(Path filePath, String content) throws IOException { + try (FileWriter writer = new FileWriter(filePath.toString(), StandardCharsets.UTF_8)) { + writer.write(content); + } + } + + private void checkOpenAPIVersion(OpenAPI openAPIDef) throws BallerinaOpenApiException { + if (!SUPPORTED_OPENAPI_VERSIONS.contains(openAPIDef.getOpenapi())) { + String sb = String.format("WARNING: The tool has not been tested with OpenAPI version %s. The generated " + + "code may potentially contain errors.", openAPIDef.getOpenapi()) + System.lineSeparator(); + throw new BallerinaOpenApiException(sb); + } + } +} diff --git a/flow-model-generator/modules/flow-model-generator-core/src/main/java/module-info.java b/flow-model-generator/modules/flow-model-generator-core/src/main/java/module-info.java index 9e5fae82a..1eb894a5c 100644 --- a/flow-model-generator/modules/flow-model-generator-core/src/main/java/module-info.java +++ b/flow-model-generator/modules/flow-model-generator-core/src/main/java/module-info.java @@ -28,6 +28,8 @@ requires io.ballerina.central.client; requires com.google.gson; requires com.graphqljava; + requires io.ballerina.openapi.core; + requires io.swagger.v3.oas.models; exports io.ballerina.flowmodelgenerator.core; } diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/build.gradle b/flow-model-generator/modules/flow-model-generator-ls-extension/build.gradle index 68ab7fd5b..2eed20661 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/build.gradle +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/build.gradle @@ -37,6 +37,7 @@ dependencies { implementation project(':flow-model-generator:flow-model-generator-core') implementation "org.ballerinalang:ballerina-lang:${ballerinaLangVersion}" + implementation "org.ballerinalang:formatter-core:${ballerinaLangVersion}" implementation "org.ballerinalang:ballerina-tools-api:${ballerinaLangVersion}" implementation "org.ballerinalang:language-server-commons:${ballerinaLangVersion}" implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:${eclipseLsp4jVersion}" @@ -48,6 +49,7 @@ dependencies { balTools("org.ballerinalang:jballerina-tools:${ballerinaLangVersion}") { transitive = false } + implementation "io.ballerina.openapi:core:2.1.1-20240926-171100-1f88ade" } def balDistribution = file("$project.buildDir/extracted-distribution/jballerina-tools-${ballerinaLangVersion}") diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java index 456d4cd99..ba666cd93 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java @@ -29,6 +29,7 @@ import io.ballerina.flowmodelgenerator.core.FunctionGenerator; import io.ballerina.flowmodelgenerator.core.ModelGenerator; import io.ballerina.flowmodelgenerator.core.NodeTemplateGenerator; +import io.ballerina.flowmodelgenerator.core.OpenApiServiceGenerator; import io.ballerina.flowmodelgenerator.core.SourceGenerator; import io.ballerina.flowmodelgenerator.core.SuggestedComponentService; import io.ballerina.flowmodelgenerator.core.SuggestedModelGenerator; @@ -41,6 +42,7 @@ import io.ballerina.flowmodelgenerator.extension.request.FlowModelSourceGeneratorRequest; import io.ballerina.flowmodelgenerator.extension.request.FlowModelSuggestedGenerationRequest; import io.ballerina.flowmodelgenerator.extension.request.FlowNodeDeleteRequest; +import io.ballerina.flowmodelgenerator.extension.request.OpenAPIServiceGenerationRequest; import io.ballerina.flowmodelgenerator.extension.request.SuggestedComponentRequest; import io.ballerina.flowmodelgenerator.extension.response.CopilotContextResponse; import io.ballerina.flowmodelgenerator.extension.response.FlowModelAvailableNodesResponse; @@ -49,6 +51,7 @@ import io.ballerina.flowmodelgenerator.extension.response.FlowModelNodeTemplateResponse; import io.ballerina.flowmodelgenerator.extension.response.FlowModelSourceGeneratorResponse; import io.ballerina.flowmodelgenerator.extension.response.FlowNodeDeleteResponse; +import io.ballerina.flowmodelgenerator.extension.response.OpenApiServiceGenerationResponse; import io.ballerina.projects.Document; import io.ballerina.projects.DocumentId; import io.ballerina.projects.Module; @@ -369,6 +372,26 @@ public CompletableFuture deleteFlowNode(FlowNodeDeleteRe }); } + @JsonRequest + public CompletableFuture generateServiceFromOpenApiContract( + OpenAPIServiceGenerationRequest request) { + + return CompletableFuture.supplyAsync(() -> { + OpenApiServiceGenerationResponse response = new OpenApiServiceGenerationResponse(); + try { + Path openApiContractPath = Path.of(request.openApiContractPath()); + Path projectPath = Path.of(request.projectPath()); + OpenApiServiceGenerator openApiServiceGenerator = new OpenApiServiceGenerator(openApiContractPath, + projectPath, request.port(), workspaceManager); + response.setService(openApiServiceGenerator.generateService()); + } catch (Throwable e) { + //TODO: Handle errors generated by the flow model generator service. + response.setError(e); + } + return response; + }); + } + private static String getRelativePath(Path projectPath, Path filePath) { if (projectPath == null || filePath == null) { return ""; diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/request/OpenAPIServiceGenerationRequest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/request/OpenAPIServiceGenerationRequest.java new file mode 100644 index 000000000..0c5cc735f --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/request/OpenAPIServiceGenerationRequest.java @@ -0,0 +1,32 @@ +/* + * 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.flowmodelgenerator.extension.request; + +/** + * Represents a request to generate the service from OpenAPI contract. + * + * @param openApiContractPath Location for OpenAPI contract + * @param projectPath Location for the generated services + * @param port port + * + * @since 1.4.0 + */ +public record OpenAPIServiceGenerationRequest(String openApiContractPath, String projectPath, int port) { + +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/response/OpenApiServiceGenerationResponse.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/response/OpenApiServiceGenerationResponse.java new file mode 100644 index 000000000..362062428 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/response/OpenApiServiceGenerationResponse.java @@ -0,0 +1,39 @@ +/* + * 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.flowmodelgenerator.extension.response; + +import io.ballerina.tools.text.LineRange; + +/** + * Represents the response for OpenAPI service generation API. + * + * @since 1.4.0 + */ +public class OpenApiServiceGenerationResponse extends AbstractFlowModelResponse { + + private LineRange service; + + public LineRange getService() { + return service; + } + + public void setService(LineRange service) { + this.service = service; + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/module-info.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/module-info.java index ca5159c41..7001d666e 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/module-info.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/module-info.java @@ -24,4 +24,5 @@ requires com.google.gson; requires io.ballerina.tools.api; requires io.ballerina.flow.model.generator; + requires io.ballerina.openapi.core; } diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ServiceGeneratorTest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ServiceGeneratorTest.java new file mode 100644 index 000000000..9eea3f916 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ServiceGeneratorTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023, 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.flowmodelgenerator.extension; + +import com.google.gson.JsonObject; +import io.ballerina.flowmodelgenerator.extension.request.OpenAPIServiceGenerationRequest; +import org.ballerinalang.langserver.BallerinaLanguageServer; +import org.ballerinalang.langserver.util.TestUtil; +import org.eclipse.lsp4j.jsonrpc.Endpoint; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Test cases for the OpenAPI service generation. + * + * @since 1.4.0 + */ +public class ServiceGeneratorTest extends AbstractLSTest { + + @DataProvider(name = "data-provider") + @Override + protected Object[] getConfigsList() { + return new Object[][]{ + {Path.of("config1.json")} + }; + } + + @Override + @Test(dataProvider = "data-provider") + public void test(Path config) throws IOException { + Endpoint endpoint = TestUtil.newLanguageServer().withLanguageServer(new BallerinaLanguageServer()).build(); + Path configJsonPath = configDir.resolve(config); + TestConfig testConfig = gson.fromJson(Files.newBufferedReader(configJsonPath), TestConfig.class); + Path contractPath = resDir.resolve("contracts").resolve(testConfig.contractFile()); + + Path project = resDir.resolve("project"); + Files.createDirectories(project); + String projectPath = project.toAbsolutePath().toString(); + OpenAPIServiceGenerationRequest request = + new OpenAPIServiceGenerationRequest(contractPath.toAbsolutePath().toString(), projectPath, 9090); + JsonObject resp = getResponse(endpoint, request); + deleteFolder(project.toFile()); + if (!resp.getAsJsonObject("service").equals(testConfig.lineRange())) { + TestConfig updatedConfig = new TestConfig(testConfig.contractFile(), resp.get("service").getAsJsonObject()); + updateConfig(configJsonPath, updatedConfig); + Assert.fail(String.format("Failed test: '%s'", configJsonPath)); + } + TestUtil.shutdownLanguageServer(endpoint); + } + + @Override + protected String getResourceDir() { + return "openapi_service_gen"; + } + + @Override + protected Class clazz() { + return ServiceGeneratorTest.class; + } + + @Override + protected String getApiName() { + return "generateServiceFromOpenApiContract"; + } + + private void deleteFolder(File folder) { + File[] files = folder.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) { + deleteFolder(f); + } else { + f.delete(); + } + } + } + } + + /** + * Represents the test configuration for the service generation. + * + * @param contractFile OpenAPI contract file + * @param lineRange line range of service declaration + * @since 1.4.0 + */ + private record TestConfig(String contractFile, JsonObject lineRange) { + + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/openapi_service_gen/config/config1.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/openapi_service_gen/config/config1.json new file mode 100644 index 000000000..3f0d7fdc5 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/openapi_service_gen/config/config1.json @@ -0,0 +1,14 @@ +{ + "contractFile": "petstore.yaml", + "lineRange": { + "fileName": "service_implementation.bal", + "startLine": { + "line": 1, + "offset": 0 + }, + "endLine": { + "line": 14, + "offset": 1 + } + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/openapi_service_gen/contracts/petstore.yaml b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/openapi_service_gen/contracts/petstore.yaml new file mode 100644 index 000000000..8e1dc0706 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/openapi_service_gen/contracts/petstore.yaml @@ -0,0 +1,158 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: OpenApi Petstore + license: + name: MIT +servers: + - url: http://petstore.{host}.io/v1 + description: The production API server + variables: + host: + default: openapi + description: this value is assigned by the service provider + - url: https://{subdomain}.swagger.io:{port}/{basePath} + description: The production API server + variables: + subdomain: + default: petstore + description: this value is assigned by the service provider + port: + enum: + - '8443' + - '443' + default: '443' + basePath: + default: v2 +tags: + - name: pets + description: Pets Tag + - name: list + description: List Tag + +paths: + /pets: + get: + summary: List all pets + description: Show a list of pets in the system + operationId: listPets + tags: + - pets + - list + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + responses: + '200': + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPet + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Dog" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /action: + x-MULTI: + operationId: getAction + responses: + '200': + description: Successful + examples: + application/json: Ok + x-METHODS: + - HEAD + - OPTIONS + - PATCH + - DELETE + - POST + - PUT + - GET +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + type: + type: string + Dog: + allOf: + - $ref: "#/components/schemas/Pet" + - type: object + properties: + bark: + type: boolean + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + message: + type: string diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/testng.xml b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/testng.xml index 4d831c8e3..c701f108b 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/testng.xml +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/testng.xml @@ -33,6 +33,7 @@ under the License. + diff --git a/gradle.properties b/gradle.properties index ca5e3a7c5..87f18f198 100644 --- a/gradle.properties +++ b/gradle.properties @@ -51,3 +51,5 @@ stdlibGraphqlVersion=1.12.0 # Persist Tool persistToolVersion=1.3.0 + +swaggerParserVersion=2.1.22