diff --git a/build.gradle b/build.gradle index c61f28fc0..4db1d7a5b 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,7 @@ ext.netLingalaZip4jVersion = project.netLingalaZip4jVersion ext.slf4jVersion = project.slf4jVersion ext.commonsLang3Version = project.commonsLang3Version ext.commonsIoVersion = project.commonsIoVersion +ext.commonsCodecVersion = project.commonsCodecVersion ext.netLingalaZip4jVersion = project.netLingalaZip4jVersion ext.stdlibIoVersion = project.stdlibIoVersion ext.stdlibLogVersion = project.stdlibLogVersion diff --git a/gradle.properties b/gradle.properties index ccf738a12..fe5bc97f1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ group=io.ballerina version=1.9.0-SNAPSHOT #dependency -ballerinaLangVersion=2201.9.0-20240208-103300-0823dc95 +ballerinaLangVersion=2201.9.0-20240229-103900-a949e6d4 testngVersion=7.6.1 slf4jVersion=1.7.30 org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 @@ -13,6 +13,7 @@ netLingalaZip4jVersion=2.8.0 jacocoVersion=0.8.10 swaggerParserVersion=2.1.16 puppycrawlCheckstyleVersion = 10.12.1 +commonsCodecVersion=1.16.0 # Stdlib Level 01 stdlibIoVersion=1.6.0 diff --git a/gradle/javaProject.gradle b/gradle/javaProject.gradle index 7f99ed579..0ab2bdc36 100644 --- a/gradle/javaProject.gradle +++ b/gradle/javaProject.gradle @@ -47,7 +47,7 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:2.15.3" implementation "com.github.jknack:handlebars:4.2.0" implementation "com.google.code.findbugs:jsr305:3.0.2" - implementation "info.picocli:picocli:4.0.1" + implementation "info.picocli:picocli:4.7.5" implementation "io.swagger.core.v3:swagger-core:2.2.9" implementation "io.swagger.core.v3:swagger-models:2.2.9" implementation "io.swagger.parser.v3:swagger-parser:${swaggerParserVersion}" diff --git a/module-ballerina-openapi/build.gradle b/module-ballerina-openapi/build.gradle index 1625cdd87..1a96753b9 100644 --- a/module-ballerina-openapi/build.gradle +++ b/module-ballerina-openapi/build.gradle @@ -29,7 +29,7 @@ dependencies { implementation project(':openapi-validator') implementation project(':openapi-core') implementation project(':openapi-cli') -// implementation project(':openapi-client-idl-plugin') + implementation project(':openapi-bal-task-plugin') implementation project(':openapi-ls-extension') implementation project(':openapi-extension') } @@ -61,8 +61,7 @@ def targetNativeJar = file("""$project.rootDir/${packageName}-validator/build/li def targetOpenApiCommonJar = file("$project.rootDir/ballerina-to-openapi/build/libs/ballerina-to-openapi-${project.version}.jar") def targetOpenApiCoreJar = file("$project.rootDir/openapi-core/build/libs/openapi-core-${project.version}.jar") def targetOpenApiCliJar = file("$project.rootDir/openapi-cli/build/libs/openapi-cli-${project.version}.jar") -//def targetOpenApiClientIDLJar = file("$project.rootDir/openapi-client-idl-plugin" + -// "/build/libs/openapi-client-idl-plugin-${project.version}.jar") +def targetOpenApiBalTaskJar = file("$project.rootDir/openapi-bal-task-plugin/build/libs/openapi-bal-task-plugin-${project.version}.jar") def targetOpenApiBuiltInExtensionJar = file("$project.rootDir/openapi-extension/build/libs/openapi-extension-${project.version}.jar") def targetOpenApiBuildExtensionJar = file("$project.rootDir/openapi-build-extension/build/libs/openapi-build-extension-${project.version}.jar") def targetOpenApiLSJar = file("$project.rootDir/openapi-ls-extension/build/libs/openapi-ls-extension-${project.version}.jar") @@ -239,10 +238,10 @@ task ballerinaBuild { into file("$artifactLibParent/libs") } -// copy { -// from targetOpenApiClientIDLJar -// into file("$artifactLibParent/libs") -// } + copy { + from targetOpenApiBalTaskJar + into file("$artifactLibParent/libs") + } copy { from targetOpenApiLSJar @@ -339,6 +338,7 @@ ballerinaBuild.dependsOn ":${packageName}-ls-extension:build" ballerinaBuild.dependsOn ":${packageName}-validator:build" ballerinaBuild.dependsOn ":${packageName}-extension:build" ballerinaBuild.dependsOn ":${packageName}-build-extension:build" +ballerinaBuild.dependsOn ":${packageName}-bal-task-plugin:build" ballerinaBuild.dependsOn updateCompilerTomlFile ballerinaBuild.dependsOn initializeVariables ballerinaBuild.dependsOn generatePomFileForMavenJavaPublication diff --git a/openapi-bal-task-plugin/build.gradle b/openapi-bal-task-plugin/build.gradle new file mode 100644 index 000000000..63bf0382f --- /dev/null +++ b/openapi-bal-task-plugin/build.gradle @@ -0,0 +1,74 @@ +/* + * 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. + */ + + +apply from: "$rootDir/gradle/javaProject.gradle" +apply plugin: "com.github.johnrengelman.shadow" +apply plugin: "java-library" + +description = "OpenAPI Tooling - OpenAPI to Ballerina" + +configurations.all { + resolutionStrategy.preferProjectModules() +} + +dependencies { + implementation project(':openapi-core') + 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 "org.ballerinalang:ballerina-lang" + implementation "org.ballerinalang:ballerina-parser" + implementation "org.ballerinalang:formatter-core" + implementation "org.ballerinalang:ballerina-cli" + implementation "org.ballerinalang:ballerina-tools-api" + implementation "org.ballerinalang:toml-parser:${ballerinaLangVersion}" + implementation "com.google.code.findbugs:jsr305" + implementation "commons-codec:commons-codec:${commonsCodecVersion}" + testImplementation "org.testng:testng" +} + +shadowJar { + configurations = [project.configurations.runtimeClasspath] + dependencies { + include(dependency("commons-codec:commons-codec:${commonsCodecVersion}")) + include(dependency('io.swagger.parser.v3:swagger-parser')) + exclude('META-INF/*.SF') + exclude('META-INF/*.DSA') + exclude('META-INF/*.RSA') + } + // Customize the JAR file name without the '-all' suffix + archiveFileName = "openapi-bal-task-plugin-${project.version}.jar" +} + +compileJava { + doFirst { + options.compilerArgs = [ + '--module-path', classpath.asPath, + ] + classpath = files() + } +} +build.dependsOn shadowJar + +// Disable the default 'jar' task +tasks.named('jar').configure { + enabled = false +} diff --git a/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/Constants.java b/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/Constants.java new file mode 100644 index 000000000..8f7fa4f6e --- /dev/null +++ b/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/Constants.java @@ -0,0 +1,83 @@ +/* + * 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.bal.tool; + +import io.ballerina.tools.diagnostics.DiagnosticSeverity; + +/** + * This class includes constants for ballerina package build generator. + * + * @since 1.9.0 + */ +public class Constants { + public static final String TAGS = "tags"; + public static final String OPERATIONS = "operations"; + public static final String NULLABLE = "nullable"; + public static final String CLIENT_METHODS = "clientMethods"; + public static final String LICENSE = "license"; + public static final String TRUE = "true"; + public static final String MODE = "mode"; + public static final String CLIENT = "client"; + public static final String CACHE_FILE = "openapi-cache.txt"; + + /** + * Enum class for containing diagnostic messages. + */ + public enum DiagnosticMessages { + LICENSE_PATH_BLANK("OAS_CLIENT_01", "given license file path is an empty string.", + DiagnosticSeverity.WARNING), + ERROR_WHILE_READING_LICENSE_FILE("OAS_CLIENT_02", "unexpected error occurred while reading the license", + DiagnosticSeverity.ERROR), + ERROR_WHILE_GENERATING_CLIENT("OAS_CLIENT_03", "unexpected error occurred while generating the client", + DiagnosticSeverity.ERROR), + PARSER_ERROR("OAS_CLIENT_04", "", DiagnosticSeverity.ERROR), + UNEXPECTED_EXCEPTIONS("OAS_CLIENT_05", "unexpected error occurred while reading the contract", + DiagnosticSeverity.ERROR), + EMPTY_CONTRACT_PATH("OAS_CLIENT_06", "given openapi contract file path is an empty string.", + DiagnosticSeverity.ERROR), + WARNING_FOR_OTHER_GENERATION("OAS_CLIENT_07", "`%s` mode does not support for bal build code generation.", + DiagnosticSeverity.ERROR), + WARNING_FOR_UNSUPPORTED_CONTRACT("OAS_CLIENT_08", "unsupported contract type. please use .yml, " + + ".yaml, or .json files for code generation.", + DiagnosticSeverity.ERROR), + INVALID_CONTRACT_PATH("OAS_CLIENT_09", "invalid openapi contract file path.", + DiagnosticSeverity.ERROR); + + private final String code; + private final String description; + private final DiagnosticSeverity severity; + + DiagnosticMessages(String code, String description, DiagnosticSeverity severity) { + this.code = code; + this.description = description; + this.severity = severity; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + public DiagnosticSeverity getSeverity() { + return severity; + } + } +} diff --git a/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/OpenAPICodeGeneratorTool.java b/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/OpenAPICodeGeneratorTool.java new file mode 100644 index 000000000..cd9b8b79b --- /dev/null +++ b/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/OpenAPICodeGeneratorTool.java @@ -0,0 +1,465 @@ +/* + * 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.bal.tool; + +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; +import io.ballerina.openapi.core.GeneratorUtils; +import io.ballerina.openapi.core.exception.BallerinaOpenApiException; +import io.ballerina.openapi.core.generators.client.BallerinaClientGenerator; +import io.ballerina.openapi.core.generators.client.model.OASClientConfig; +import io.ballerina.openapi.core.generators.schema.BallerinaTypesGenerator; +import io.ballerina.openapi.core.generators.service.model.OASServiceMetadata; +import io.ballerina.openapi.core.model.Filter; +import io.ballerina.openapi.core.model.GenSrcFile; +import io.ballerina.projects.Package; +import io.ballerina.projects.buildtools.CodeGeneratorTool; +import io.ballerina.projects.buildtools.ToolContext; +import io.ballerina.toml.semantic.diagnostics.TomlNodeLocation; +import io.ballerina.tools.diagnostics.DiagnosticFactory; +import io.ballerina.tools.diagnostics.DiagnosticInfo; +import io.ballerina.tools.diagnostics.Location; +import io.swagger.v3.oas.models.OpenAPI; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.ballerinalang.formatter.core.Formatter; +import org.ballerinalang.formatter.core.FormatterException; + +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.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static io.ballerina.openapi.bal.tool.Constants.CACHE_FILE; +import static io.ballerina.openapi.bal.tool.Constants.CLIENT; +import static io.ballerina.openapi.bal.tool.Constants.CLIENT_METHODS; +import static io.ballerina.openapi.bal.tool.Constants.LICENSE; +import static io.ballerina.openapi.bal.tool.Constants.MODE; +import static io.ballerina.openapi.bal.tool.Constants.NULLABLE; +import static io.ballerina.openapi.bal.tool.Constants.OPERATIONS; +import static io.ballerina.openapi.bal.tool.Constants.TAGS; +import static io.ballerina.openapi.bal.tool.Constants.TRUE; +import static io.ballerina.openapi.core.GeneratorConstants.CLIENT_FILE_NAME; +import static io.ballerina.openapi.core.GeneratorConstants.DO_NOT_MODIFY_FILE_HEADER; +import static io.ballerina.openapi.core.GeneratorConstants.JSON_EXTENSION; +import static io.ballerina.openapi.core.GeneratorConstants.RESOURCE; +import static io.ballerina.openapi.core.GeneratorConstants.TYPE_FILE_NAME; +import static io.ballerina.openapi.core.GeneratorConstants.UTIL_FILE_NAME; +import static io.ballerina.openapi.core.GeneratorConstants.YAML_EXTENSION; +import static io.ballerina.openapi.core.GeneratorConstants.YML_EXTENSION; +import static io.ballerina.openapi.core.GeneratorUtils.normalizeOpenAPI; + +/** + * This class includes the implementation of the {@code BuildToolRunner} for the OpenAPI tool. + * + * @since 1.9.0 + */ +public class OpenAPICodeGeneratorTool implements CodeGeneratorTool { + static String hashOpenAPI; + + @Override + public String toolName() { + return "openapi"; + } + + @Override + public void execute(ToolContext toolContext) { + ImmutablePair codeGeneratorConfig; + TomlNodeLocation location = toolContext.currentPackage().ballerinaToml().get().tomlAstNode().location(); + try { + // Validate the OAS file whether we can handle within OpenAPI tool + if (!canHandle(toolContext)) { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.WARNING_FOR_UNSUPPORTED_CONTRACT; + createDiagnostics(toolContext, error, location); + return; + } + // Handle the code generation + String oasFilePath = toolContext.filePath(); + Path packagePath = toolContext.currentPackage().project().sourceRoot(); + Optional openAPI = getOpenAPIContract(packagePath, Path.of(oasFilePath), location, toolContext); + if (openAPI.isEmpty()) { + return; + } + //Extract the details using the `tool config options` table in the `Ballerina.toml` file. + //If the `tool options` table is not specified in the TOML file, the client will be generated by default. + Map options = toolContext.options(); + if (options == null) { + // Default generate client + Filter filter = new Filter(); + OASClientConfig clientConfig = new OASClientConfig.Builder() + .withFilters(filter).withOpenAPI(openAPI.get()).build(); + OASServiceMetadata serviceMetaData = new OASServiceMetadata.Builder() + .withFilters(filter).withOpenAPI(openAPI.get()).build(); + codeGeneratorConfig = new ImmutablePair<>(clientConfig, serviceMetaData); + if (validateCache(toolContext, clientConfig)) { + return; + } + generateClient(toolContext, codeGeneratorConfig); + } else { + codeGeneratorConfig = extractOptionDetails(toolContext, openAPI.get()); + if (validateCache(toolContext, codeGeneratorConfig.getLeft())) { + return; + } + if (options.containsKey(MODE)) { + String value = (String) options.get(MODE).value().toString().trim(); + handleCodeGenerationMode(toolContext, codeGeneratorConfig, location, value); + } else { + // Create client for the given OAS + generateClient(toolContext, codeGeneratorConfig); + } + } + } catch (BallerinaOpenApiException e) { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.PARSER_ERROR; + createDiagnostics(toolContext, error, location); + } catch (IOException | FormatterException e) { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.ERROR_WHILE_GENERATING_CLIENT; + createDiagnostics(toolContext, error, location); + } + } + + /** + * This method uses to validate the cache. + */ + private static boolean validateCache(ToolContext toolContext, OASClientConfig clientConfig) throws IOException { + Path cachePath = toolContext.cachePath(); + hashOpenAPI = getHashValue(clientConfig, toolContext.targetModule()); + if (!Files.isDirectory(cachePath)) { + return false; + } + // read the cache file + Path cacheFilePath = Paths.get(cachePath.toString(), CACHE_FILE); + String cacheContent = Files.readString(Paths.get(cacheFilePath.toString())); + return cacheContent.equals(hashOpenAPI); + } + + /** + * This method uses to handle the code generation mode. ex: client, service + */ + private void handleCodeGenerationMode(ToolContext toolContext, + ImmutablePair codeGeneratorConfig, + TomlNodeLocation location, String mode) + throws BallerinaOpenApiException, IOException, FormatterException { + if (mode.equals(CLIENT)) { + // Create client for the given OAS + generateClient(toolContext, codeGeneratorConfig); + } else { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.WARNING_FOR_OTHER_GENERATION; + createDiagnostics(toolContext, error, location, mode); + } + } + + /** + * This method uses to check whether given specification can be handled via the openapi client generation tool. + * This includes basic requirements like file extension check. + */ + private static boolean canHandle(ToolContext toolContext) { + String oasPath = toolContext.filePath(); + if (oasPath.isBlank()) { + TomlNodeLocation location = toolContext.currentPackage().ballerinaToml().get().tomlAstNode().location(); + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.EMPTY_CONTRACT_PATH; + createDiagnostics(toolContext, error, location); + return false; + } + return (oasPath.endsWith(YAML_EXTENSION) || oasPath.endsWith(JSON_EXTENSION) || + oasPath.endsWith(YML_EXTENSION)); + } + + /** + * This method uses to read the openapi contract and return the {@code OpenAPI} object. + */ + private Optional getOpenAPIContract(Path ballerinaFilePath, Path openAPIPath, Location location, + ToolContext toolContext) { + Path relativePath; + try { + Path inputPath = Paths.get(openAPIPath.toString()); + if (inputPath.isAbsolute()) { + relativePath = inputPath; + } else { + File file = new File(ballerinaFilePath.toString()); + File openapiContract = new File(file, openAPIPath.toString()); + relativePath = Paths.get(openapiContract.getCanonicalPath()); + } + if (Files.exists(relativePath)) { + return Optional.of(normalizeOpenAPI(relativePath, true)); + } else { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.INVALID_CONTRACT_PATH; + createDiagnostics(toolContext, error, location); + } + + } catch (IOException | BallerinaOpenApiException e) { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.UNEXPECTED_EXCEPTIONS; + createDiagnostics(toolContext, error, location); + } + return Optional.empty(); + } + + /** + * This method uses to extract the options given by the user. + */ + public static ImmutablePair extractOptionDetails(ToolContext toolContext, + OpenAPI openAPI) throws + IOException, BallerinaOpenApiException { + + Filter filter = new Filter(); + OASClientConfig.Builder clientMetaDataBuilder = new OASClientConfig.Builder(); + OASServiceMetadata.Builder serviceMetaDataBuilder = new OASServiceMetadata.Builder(); + clientMetaDataBuilder.withOpenAPI(openAPI); + serviceMetaDataBuilder.withOpenAPI(openAPI); + Map options = toolContext.options(); + for (Map.Entry field : options.entrySet()) { + String value = field.getValue().value().toString().trim(); + String fieldName = field.getKey(); + switch (fieldName) { + case TAGS: + filter.setTags(getArrayItems(field.getValue())); + break; + case OPERATIONS: + filter.setOperations(getArrayItems(field.getValue())); + break; + case NULLABLE: + serviceMetaDataBuilder.withNullable(value.contains(TRUE)); + clientMetaDataBuilder.withNullable(value.contains(TRUE)); + break; + case CLIENT_METHODS: + clientMetaDataBuilder.withResourceMode(value.contains(RESOURCE)); + break; + case LICENSE: + Optional licenseContent; + if (value.isBlank()) { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.LICENSE_PATH_BLANK; + createDiagnostics(toolContext, error, field.getValue().location()); + licenseContent = Optional.of(DO_NOT_MODIFY_FILE_HEADER); + } else { + licenseContent = getLicenseContent(toolContext, Paths.get(value)); + } + clientMetaDataBuilder.withLicense(licenseContent.orElse(DO_NOT_MODIFY_FILE_HEADER)); + break; + default: + break; + } + } + clientMetaDataBuilder.withFilters(filter); + serviceMetaDataBuilder.withFilters(filter); + return new ImmutablePair<>(clientMetaDataBuilder.build(), serviceMetaDataBuilder.build()); + } + + private static List getArrayItems(Object valueNode) { + List arrayItems = new ArrayList<>(); + if (valueNode instanceof ArrayList) { + return (List) valueNode; + } + return arrayItems; + } + + /** + * This method uses to generate the client module for the given openapi contract. + */ + private void generateClient(ToolContext toolContext, ImmutablePair codeGeneratorConfig) throws BallerinaOpenApiException, IOException, FormatterException { + OASClientConfig clientConfig = codeGeneratorConfig.getLeft(); + List sources = generateClientFiles(clientConfig); + Path outputPath = toolContext.outputPath(); + writeGeneratedSources(sources, outputPath); + // Update the cache file + Path cachePath = toolContext.cachePath(); + List sourcesForCache = new ArrayList<>(); + GenSrcFile genSrcFile = new GenSrcFile(GenSrcFile.GenFileType.CACHE_SRC, null, + CACHE_FILE, hashOpenAPI); + sourcesForCache.add(genSrcFile); + writeGeneratedSources(sourcesForCache, cachePath); + } + + /** + * This method uses to generate hash value for the given code generation details. + * //TODO: This will be extended to support service generation. + */ + private static String getHashValue(OASClientConfig clientConfig, String targetPath) { + String openAPIDefinitions = clientConfig.getOpenAPI().toString().trim().replaceAll("\\s+", ""); + StringBuilder summaryOfCodegen = new StringBuilder(); + summaryOfCodegen.append(openAPIDefinitions) + .append(targetPath) + .append(clientConfig.isResourceMode()) + .append(clientConfig.getLicense()) + .append(clientConfig.isNullable()); + List tags = clientConfig.getFilters().getTags(); + tags.sort(String.CASE_INSENSITIVE_ORDER); + for (String str : tags) { + summaryOfCodegen.append(str); + } + List operations = clientConfig.getFilters().getOperations(); + operations.sort(String.CASE_INSENSITIVE_ORDER); + for (String str : operations) { + summaryOfCodegen.append(str); + } + return DigestUtils.sha256Hex(summaryOfCodegen.toString()).toUpperCase(Locale.ENGLISH); + } + + /** + * This method uses to generate ballerina files for openapi client stub. + * This will return list of (client.bal, util.bal, types.bal) {@code GenSrcFile}. + */ + private static List generateClientFiles(OASClientConfig oasClientConfig) throws + BallerinaOpenApiException, IOException, FormatterException { + + List sourceFiles = new ArrayList<>(); + + // Generate ballerina client files. + String licenseContent = oasClientConfig.getLicense(); + BallerinaClientGenerator ballerinaClientGenerator = new BallerinaClientGenerator(oasClientConfig); + String mainContent = Formatter.format(ballerinaClientGenerator.generateSyntaxTree()).toString(); + sourceFiles.add(new GenSrcFile(GenSrcFile.GenFileType.GEN_SRC, null, CLIENT_FILE_NAME, + licenseContent == null || licenseContent.isBlank() ? mainContent : + licenseContent + System.lineSeparator() + mainContent)); + String utilContent = Formatter.format( + ballerinaClientGenerator.getBallerinaUtilGenerator().generateUtilSyntaxTree()).toString(); + + if (!utilContent.isBlank()) { + sourceFiles.add(new GenSrcFile(GenSrcFile.GenFileType.UTIL_SRC, null, UTIL_FILE_NAME, + licenseContent == null || licenseContent.isBlank() ? utilContent : + licenseContent + System.lineSeparator() + utilContent)); + } + + // Generate ballerina records to represent schemas. + List typeDefinitionNodeList = new LinkedList<>(); + typeDefinitionNodeList.addAll(ballerinaClientGenerator.getTypeDefinitionNodeList()); + typeDefinitionNodeList.addAll(ballerinaClientGenerator + .getBallerinaAuthConfigGenerator().getAuthRelatedTypeDefinitionNodes()); + BallerinaTypesGenerator ballerinaSchemaGenerator = new BallerinaTypesGenerator(oasClientConfig.getOpenAPI(), + oasClientConfig.isNullable(), typeDefinitionNodeList); + SyntaxTree schemaSyntaxTree = ballerinaSchemaGenerator.generateSyntaxTree(); + String schemaContent = Formatter.format(schemaSyntaxTree).toString(); + + if (oasClientConfig.getFilters().getTags().isEmpty()) { + // Remove unused records and enums when generating the client by the tags given. + schemaContent = GeneratorUtils.removeUnusedEntities(schemaSyntaxTree, mainContent, schemaContent, + null); + } + if (!schemaContent.isBlank()) { + sourceFiles.add(new GenSrcFile(GenSrcFile.GenFileType.MODEL_SRC, null, TYPE_FILE_NAME, + licenseContent == null || licenseContent.isBlank() ? schemaContent : + licenseContent + System.lineSeparator() + schemaContent)); + } + + return sourceFiles; + } + + /** + * Util to read license content. + */ + private static Optional getLicenseContent(ToolContext context, Path licensePath) { + Package packageInstance = context.currentPackage(); + Location location = context.options().get("license").location(); + Path ballerinaFilePath = packageInstance.project().sourceRoot(); + try { + Path relativePath = getLicensePath(licensePath, ballerinaFilePath); + return (relativePath != null) ? createLicenseContent(relativePath, + location, context) : Optional.empty(); + } catch (IOException e) { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages.ERROR_WHILE_READING_LICENSE_FILE; + createDiagnostics(context, error, location); + return Optional.empty(); + } + } + + /** + * Util to get license path. + */ + private static Path getLicensePath(Path licensePath, Path ballerinaFilePath) throws IOException { + Path relativePath = null; + if (!licensePath.toString().isBlank()) { + Path finalLicensePath = Paths.get(licensePath.toString()); + if (finalLicensePath.isAbsolute()) { + relativePath = finalLicensePath; + } else { + File openapiContract = new File(ballerinaFilePath.toString(), licensePath.toString()); + relativePath = Paths.get(openapiContract.getCanonicalPath()); + } + } + return relativePath; + } + + /** + * Util to create license content. + */ + private static Optional createLicenseContent(Path relativePath, Location location, + ToolContext toolContext) { + String licenseHeader; + try { + String newLine = System.lineSeparator(); + Path filePath = Paths.get((new File(relativePath.toString()).getCanonicalPath())); + licenseHeader = Files.readString(Paths.get(filePath.toString())); + if (!licenseHeader.endsWith(newLine)) { + licenseHeader = licenseHeader + newLine + newLine; + } else if (!licenseHeader.endsWith(newLine + newLine)) { + licenseHeader = licenseHeader + newLine; + } + } catch (IOException e) { + Constants.DiagnosticMessages error = Constants.DiagnosticMessages + .ERROR_WHILE_READING_LICENSE_FILE; + createDiagnostics(toolContext, error, location); + return Optional.empty(); + } + return Optional.of(licenseHeader); + } + + /** + * This method uses to report the diagnostics. + */ + private static void createDiagnostics(ToolContext toolContext, Constants.DiagnosticMessages error, + Location location, String... args) { + String message = String.format(error.getDescription(), (Object[]) args); + DiagnosticInfo diagnosticInfo = new DiagnosticInfo(error.getCode(), message, + error.getSeverity()); + toolContext.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, location)); + } + + /** + * This method uses to write the generated sources into the given output path. + */ + private void writeGeneratedSources(List sources, Path outputPath) throws IOException { + for (GenSrcFile file : sources) { + Path filePath = Paths.get(outputPath.resolve(file.getFileName()).toFile().getCanonicalPath()); + String fileContent = file.getContent(); + writeFile(filePath, fileContent); + } + } + + /** + * This method uses to write the content into the given file path. + */ + public static void writeFile(Path filePath, String content) throws IOException { + File file = new File(filePath.toString()); + // Ensure the directory structure exists + File parentDirectory = file.getParentFile(); + if (!parentDirectory.exists() && !parentDirectory.mkdirs()) { + return; // Directory creation failed + } + try (FileWriter writer = new FileWriter(filePath.toString(), StandardCharsets.UTF_8)) { + writer.write(content); + } + } +} diff --git a/openapi-bal-task-plugin/src/main/java/module-info.java b/openapi-bal-task-plugin/src/main/java/module-info.java new file mode 100644 index 000000000..870ef753f --- /dev/null +++ b/openapi-bal-task-plugin/src/main/java/module-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 Inc. 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. + */ + +module io.ballerina.openapi.bal.tool { + requires io.ballerina.lang; + requires io.ballerina.parser; + requires io.ballerina.tools.api; + requires io.ballerina.formatter.core; + requires io.ballerina.cli; + requires io.swagger.v3.core; + requires io.swagger.v3.oas.models; + requires jsr305; + requires org.apache.commons.io; + requires org.slf4j; + requires org.apache.commons.lang3; + requires io.ballerina.openapi.core; + requires io.ballerina.toml; + requires org.apache.commons.codec; +} diff --git a/openapi-bal-task-plugin/src/main/resources/META-INF/services/io.ballerina.projects.buildtools.CodeGeneratorTool b/openapi-bal-task-plugin/src/main/resources/META-INF/services/io.ballerina.projects.buildtools.CodeGeneratorTool new file mode 100644 index 000000000..e2567689f --- /dev/null +++ b/openapi-bal-task-plugin/src/main/resources/META-INF/services/io.ballerina.projects.buildtools.CodeGeneratorTool @@ -0,0 +1 @@ +io.ballerina.openapi.bal.tool.OpenAPICodeGeneratorTool diff --git a/openapi-bal-task-plugin/src/main/resources/openapi-options-schema.json b/openapi-bal-task-plugin/src/main/resources/openapi-options-schema.json new file mode 100644 index 000000000..f938aefe4 --- /dev/null +++ b/openapi-bal-task-plugin/src/main/resources/openapi-options-schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "clientMethods": { + "type": "string" + }, + "operations": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "nullable": { + "type": "boolean" + }, + "mode": { + "type":"string" + }, + "license": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/openapi-bal-task-plugin/src/test/resources/testng.xml b/openapi-bal-task-plugin/src/test/resources/testng.xml new file mode 100644 index 000000000..d8b718bfe --- /dev/null +++ b/openapi-bal-task-plugin/src/test/resources/testng.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/openapi-cli/build.gradle b/openapi-cli/build.gradle index 345d44e3b..073dc5edb 100644 --- a/openapi-cli/build.gradle +++ b/openapi-cli/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation "org.ballerinalang:formatter-core" implementation "org.ballerinalang:ballerina-cli" implementation "org.ballerinalang:ballerina-tools-api" + implementation group: 'org.ballerinalang', name: 'toml-parser', version: "${ballerinaLangVersion}" implementation "io.ballerina.stdlib:http-native" implementation "com.google.code.findbugs:jsr305" testImplementation "org.testng:testng" @@ -113,7 +114,6 @@ test { shadowJar { configurations = [project.configurations.runtimeClasspath] dependencies { - include(dependency('com.github.jknack:handlebars')) include(dependency('org.antlr:antlr4:4.5')) include(dependency('io.swagger.parser.v3:swagger-parser')) include(dependency('com.atlassian.commonmark:commonmark')) diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Add.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Add.java new file mode 100644 index 000000000..98f243a0d --- /dev/null +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Add.java @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 Inc. 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.cmd; + +import io.ballerina.cli.BLauncherCmd; +import io.ballerina.openapi.core.exception.BallerinaOpenApiException; +import io.ballerina.toml.syntax.tree.AbstractNodeFactory; +import io.ballerina.toml.syntax.tree.ArrayNode; +import io.ballerina.toml.syntax.tree.DocumentMemberDeclarationNode; +import io.ballerina.toml.syntax.tree.DocumentNode; +import io.ballerina.toml.syntax.tree.IdentifierToken; +import io.ballerina.toml.syntax.tree.KeyNode; +import io.ballerina.toml.syntax.tree.KeyValueNode; +import io.ballerina.toml.syntax.tree.Minutiae; +import io.ballerina.toml.syntax.tree.MinutiaeList; +import io.ballerina.toml.syntax.tree.Node; +import io.ballerina.toml.syntax.tree.NodeFactory; +import io.ballerina.toml.syntax.tree.NodeList; +import io.ballerina.toml.syntax.tree.SeparatedNodeList; +import io.ballerina.toml.syntax.tree.SyntaxKind; +import io.ballerina.toml.syntax.tree.SyntaxTree; +import io.ballerina.toml.syntax.tree.TableNode; +import io.ballerina.toml.syntax.tree.Token; +import io.ballerina.toml.syntax.tree.ValueNode; +import io.ballerina.toml.validator.SampleNodeGenerator; +import io.ballerina.tools.text.TextDocument; +import io.ballerina.tools.text.TextDocuments; +import picocli.CommandLine; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintStream; +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.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import static io.ballerina.cli.cmd.CommandUtil.exitError; +import static io.ballerina.openapi.cmd.CmdConstants.BALLERINA_TOML; +import static io.ballerina.openapi.cmd.CmdConstants.DEFAULT_CLIENT_ID; +import static io.ballerina.openapi.cmd.CmdConstants.OPENAPI_ADD_CMD; +import static io.ballerina.toml.syntax.tree.AbstractNodeFactory.createIdentifierToken; +import static io.ballerina.toml.syntax.tree.AbstractNodeFactory.createSeparatedNodeList; +import static io.ballerina.toml.syntax.tree.AbstractNodeFactory.createToken; + +/** + * Main class to implement "add" subcommand for openapi to ballerina code generation in build command. + * + * @since 1.9.0 + */ +@CommandLine.Command( + name = "add", + description = "Update Ballerina.toml with the OpenAPI CLI code generation implementation" +) +public class Add implements BLauncherCmd { + private static final String COMMAND_IDENTIFIER = "openapi-add"; + private final PrintStream outStream; + private final boolean exitWhenFinish; + + @CommandLine.Mixin + private final BaseCmd baseCmd = new BaseCmd(); + @CommandLine.ParentCommand + private OpenApiCmd openApiMainCmd; + + @CommandLine.Option(names = {"--module"}, description = "Location of the generated Ballerina client") + private String outputModule; + + @CommandLine.Option(names = {"--id"}, description = "ID for the generated Ballerina client") + private String id; + + @CommandLine.Option(names = {"-p", "--package"}, description = "Location for the Ballerina package") + private String packagePath; + + public Add() { + this.outStream = System.err; + this.exitWhenFinish = true; + } + + public Add(PrintStream outStream, boolean exitWhenFinish) { + this.outStream = outStream; + this.exitWhenFinish = exitWhenFinish; + } + + @Override + public void execute() { + Path projectPath = packagePath != null ? Path.of(packagePath) : + Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + + try { + if (baseCmd.helpFlag) { + String commandUsageInfo = BLauncherCmd.getCommandUsageInfo(COMMAND_IDENTIFIER); + outStream.println(commandUsageInfo); + return; + } + //check the given path is the Ballerina package + Optional ballerinaTomlPath = validateBallerinaProject(projectPath); + if (ballerinaTomlPath.isEmpty()) { + outStream.printf(ErrorMessages.INVALID_BALLERINA_PACKAGE, projectPath.toAbsolutePath()); + exitError(this.exitWhenFinish); + } else { + validateInputPath(); + Path tomlPath = projectPath.resolve(ballerinaTomlPath.get()); + TextDocument configDocument = TextDocuments.from(Files.readString(tomlPath)); + SyntaxTree syntaxTree = SyntaxTree.from(configDocument); + DocumentNode rootNode = syntaxTree.rootNode(); + NodeList nodeList = rootNode.members(); + NodeList moduleMembers = AbstractNodeFactory.createEmptyNodeList(); + for (DocumentMemberDeclarationNode node: nodeList) { + moduleMembers = moduleMembers.add(node); + } + //set default client id + createDefaultClientID(); + CmdOptions options = collectCLIOptions(); + moduleMembers = addNewLine(moduleMembers, 1); + moduleMembers = populateOpenAPITomlConfig(options, moduleMembers); + Token eofToken = AbstractNodeFactory.createIdentifierToken(""); + DocumentNode documentNode = NodeFactory.createDocumentNode(moduleMembers, eofToken); + TextDocument textDocument = TextDocuments.from(documentNode.toSourceCode()); + String content = SyntaxTree.from(textDocument).toSourceCode(); + //write toml file + try (FileWriter writer = new FileWriter(tomlPath.toString(), StandardCharsets.UTF_8)) { + writer.write(content); + outStream.print(ErrorMessages.TOML_UPDATED_MSG); + } + } + } catch (BallerinaOpenApiException | IOException e) { + outStream.println(e.getMessage()); + exitError(exitWhenFinish); + } + if (this.exitWhenFinish) { + Runtime.getRuntime().exit(0); + } + } + + private void createDefaultClientID() { + if (id == null || id.isBlank()) { + String file = baseCmd.inputPath.substring(baseCmd.inputPath.lastIndexOf('/') + 1, + baseCmd.inputPath.lastIndexOf('.')); + String mode = baseCmd.mode; + id = String.format(DEFAULT_CLIENT_ID, mode == null || mode.isBlank() ? "client" : mode, file); + } + } + + private void validateInputPath() { + if (baseCmd.inputPath == null || baseCmd.inputPath.isBlank()) { + outStream.printf(ErrorMessages.INVALID_INPUT_PATH); + exitError(this.exitWhenFinish); + } + } + + private CmdOptions collectCLIOptions() { + return new CmdOptions.CmdOptionsBuilder() + .withInput(baseCmd.inputPath) + .withId(id) + .withOutputModule(outputModule) + .withPackagePath(packagePath) + .withClientMethod(baseCmd.generateClientMethods) + .withMode(baseCmd.mode) + .withNullable(baseCmd.nullable) + .withOperations(getOperations()) + .withTags(getTags()) + .withLicensePath(baseCmd.licenseFilePath).build(); + } + + /** + * This util is to create the openapi tool config in toml. + *
+     *     [[tool.openapi]]
+     *     id = "openapi_client"
+     *     filePath = "input.yaml"
+     *     targetModule = "delivery"
+     *     options.mode = "client"
+     *     option.operations = ["op1","op2","op3"]
+     *     options.nullable = true
+     *     options.clientMethod = "remote"
+     * 
+ */ + private NodeList populateOpenAPITomlConfig(CmdOptions optionsBuilder, + NodeList + moduleMembers) { + TableNode openapiToolConfig = SampleNodeGenerator.createTable("[tool.openapi]", null); + moduleMembers = moduleMembers.add(openapiToolConfig); + moduleMembers = moduleMembers.add(SampleNodeGenerator.createStringKV("id", + optionsBuilder.getId(), null)); + moduleMembers = moduleMembers.add(SampleNodeGenerator.createStringKV("filePath", + optionsBuilder.getInput(), null)); + if (optionsBuilder.getOutputModule() != null) { + moduleMembers = moduleMembers.add(SampleNodeGenerator.createStringKV("targetModule", + optionsBuilder.getOutputModule(), null)); + } + if (optionsBuilder.getMode() != null) { + moduleMembers = moduleMembers.add(SampleNodeGenerator.createStringKV("options.mode", + optionsBuilder.getMode(), null)); + } + if (optionsBuilder.getTags() != null && !optionsBuilder.getTags().isEmpty()) { + Node[] arrayItems = getArrayItemNodes(getTags()); + SeparatedNodeList value = createSeparatedNodeList(arrayItems); + ArrayNode arrayNode = NodeFactory.createArrayNode(createToken(SyntaxKind.OPEN_BRACKET_TOKEN), value, + createToken(SyntaxKind.CLOSE_BRACKET_TOKEN)); + KeyValueNode tagNodes = NodeFactory.createKeyValueNode(getKeyNode("options.tags"), getAssignToken(), + arrayNode); + moduleMembers = moduleMembers.add(tagNodes); + } + if (optionsBuilder.getOperations() != null && !optionsBuilder.getOperations().isEmpty()) { + if (optionsBuilder.getTags() != null && !optionsBuilder.getTags().isEmpty()) { + moduleMembers = addNewLine(moduleMembers, 1); + } + Node[] arrayItems = getArrayItemNodes(getOperations()); + SeparatedNodeList value = createSeparatedNodeList(arrayItems); + ArrayNode arrayNode = NodeFactory.createArrayNode(createToken(SyntaxKind.OPEN_BRACKET_TOKEN), value, + createToken(SyntaxKind.CLOSE_BRACKET_TOKEN)); + KeyValueNode tagNodes = NodeFactory.createKeyValueNode(getKeyNode("options.operations"), + getAssignToken(), arrayNode); + moduleMembers = moduleMembers.add(tagNodes); + } + if (optionsBuilder.isNullable()) { + moduleMembers = moduleMembers.add(SampleNodeGenerator.createBooleanKV("options.nullable", + optionsBuilder.isNullable(), null)); + } + if (optionsBuilder.getClientMethod() != null) { + moduleMembers = moduleMembers.add(SampleNodeGenerator.createStringKV("options.clientMethods", + optionsBuilder.getClientMethod(), null)); + } + if (optionsBuilder.getLicensePath() != null && !optionsBuilder.getLicensePath().isBlank()) { + moduleMembers = moduleMembers.add(SampleNodeGenerator.createStringKV("options.licensePath", + optionsBuilder.getLicensePath(), null)); + } + moduleMembers = addNewLine(moduleMembers, 2); + return moduleMembers; + } + + private Node[] getArrayItemNodes(List items) { + List itemList = new ArrayList<>(); + IdentifierToken doubleQuotes = createIdentifierToken("\""); + for (String item : items) { + itemList.add(NodeFactory.createStringLiteralNode(doubleQuotes, createIdentifierToken(item), doubleQuotes)); + itemList.add(createToken(SyntaxKind.COMMA_TOKEN)); + } + // Remove the trailing comma if present + if (!itemList.isEmpty()) { + itemList.remove(itemList.size() - 1); + } + + return itemList.toArray(new Node[0]); + } + + @Override + public String getName() { + return OPENAPI_ADD_CMD; + } + + @Override + public void printLongDesc(StringBuilder stringBuilder) { + //This is the long description of the command and all handle within help command + } + + @Override + public void printUsage(StringBuilder stringBuilder) { + //This is the usage description of the command and all handle within help command + } + + @Override + public void setParentCmdParser(CommandLine commandLine) { + //This is not using in this command + } + + private Optional validateBallerinaProject(Path projectPath) throws BallerinaOpenApiException { + Optional ballerinaToml; + try (Stream stream = Files.list(projectPath)) { + ballerinaToml = stream.filter(file -> !Files.isDirectory(file)) + .map(Path::getFileName) + .filter(Objects::nonNull) + .filter(file -> BALLERINA_TOML.equals(file.toString())) + .findFirst(); + } catch (IOException e) { + outStream.printf(ErrorMessages.INVALID_BALLERINA_PACKAGE, projectPath.toAbsolutePath(), e.getMessage()); + exitError(this.exitWhenFinish); + return Optional.empty(); + } + return ballerinaToml; + } + + public List getTags() { + return baseCmd.tags == null ? new ArrayList<>() : Arrays.asList(baseCmd.tags.split(",")); + } + + public List getOperations() { + return baseCmd.operations == null ? new ArrayList<>() : Arrays.asList(baseCmd.operations.split(",")); + } + + private static KeyNode getKeyNode(String key) { + return NodeFactory.createKeyNode(NodeFactory.createSeparatedNodeList( + new Node[]{NodeFactory.createIdentifierToken(key)})); + } + + private static Token getAssignToken() { + MinutiaeList whitespaceMinList = NodeFactory.createMinutiaeList( + new Minutiae[]{NodeFactory.createWhitespaceMinutiae(" ")}); + return NodeFactory.createToken(SyntaxKind.EQUAL_TOKEN, whitespaceMinList, whitespaceMinList); + } + + public static NodeList addNewLine(NodeList moduleMembers, int n) { + for (int i = 0; i < n; i++) { + moduleMembers = moduleMembers.add(AbstractNodeFactory.createIdentifierToken(System.lineSeparator())); + } + return moduleMembers; + } +} diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/BaseCmd.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/BaseCmd.java new file mode 100644 index 000000000..4c7242e41 --- /dev/null +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/BaseCmd.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 Inc. 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.cmd; + +import picocli.CommandLine; + +/** + * This class is to store the cli command options that are commonly used int parent and subcommands. + * + * @since 1.9.0 + */ +public class BaseCmd { + @CommandLine.Option(names = {"-h", "--help"}, hidden = true) + public boolean helpFlag; + @CommandLine.Option(names = {"-i", "--input"}, description = "Generating the client and service both files") + public String inputPath; + + @CommandLine.Option(names = {"--license"}, description = "Location of the file which contains the license header") + public String licenseFilePath; + + @CommandLine.Option(names = {"--mode"}, description = "Generate only service file or client file according to the" + + " given mode type") + public String mode; + + @CommandLine.Option(names = {"-n", "--nullable"}, description = "Generate the code by setting nullable true") + public boolean nullable; + + @CommandLine.Option(names = {"--tags"}, description = "Tag that need to write service") + public String tags; + + @CommandLine.Option(names = {"--operations"}, description = "Operations that need to write service") + public String operations; + + @CommandLine.Option(names = {"--client-methods"}, hidden = true, description = "Generate the client methods" + + " with provided type . Only \"resource\"(default) and \"remote\" options are supported.") + public String generateClientMethods; + +} diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java index 04cc65396..1dc601749 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java @@ -204,4 +204,6 @@ public String getValue() { public static final String DOUBLE_LINE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR; public static final List SUPPORTED_OPENAPI_VERSIONS = List.of("2.0", "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0"); + public static final String DEFAULT_CLIENT_ID = "oas_%s_%s"; + public static final String OPENAPI_ADD_CMD = "add"; } diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdOptions.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdOptions.java new file mode 100644 index 000000000..85a8bf10b --- /dev/null +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdOptions.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 Inc. 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.cmd; + +import java.util.List; + +/** + * This builder class for contains the all the option for use CLI. + */ +public class CmdOptions { + private final String input; + private final String outputModule; + private final String id; + private final String packagePath; + private final List tags; + private final List operations; + private final String mode; + private final String clientMethod; + private final boolean nullable; + private final String licensePath; + + private CmdOptions(CmdOptionsBuilder builder) { + this.input = builder.input; + this.outputModule = builder.outputModule; + this.id = builder.id; + this.packagePath = builder.packagePath; + this.operations = builder.operations; + this.tags = builder.tags; + this.mode = builder.mode; + this.clientMethod = builder.clientMethod; + this.nullable = builder.nullable; + this.licensePath = builder.licensePath; + } + + public String getInput() { + return input; + } + + public String getOutputModule() { + return outputModule; + } + + public String getId() { + return id; + } + + public String getPackagePath() { + return packagePath; + } + + public List getTags() { + return tags; + } + + public List getOperations() { + return operations; + } + + public boolean isNullable() { + return nullable; + } + + public String getMode() { + return mode; + } + + public String getClientMethod() { + return clientMethod; + } + + + public String getLicensePath() { + return licensePath; + } + /** + * CMD options builder class. + */ + public static class CmdOptionsBuilder { + private String outputModule; + private String id; + private String packagePath; + private List tags; + private List operations; + private String mode; + private String clientMethod; + private boolean nullable; + private String input; + private String licensePath; + + public CmdOptionsBuilder withOutputModule(String outputModule) { + this.outputModule = outputModule; + return this; + } + + public CmdOptionsBuilder withId(String id) { + this.id = id; + return this; + } + + public CmdOptionsBuilder withPackagePath(String packagePath) { + this.packagePath = packagePath; + return this; + } + + public CmdOptionsBuilder withTags(List tags) { + this.tags = tags; + return this; + } + + public CmdOptionsBuilder withOperations(List operations) { + this.operations = operations; + return this; + } + + public CmdOptionsBuilder withMode(String mode) { + this.mode = mode; + return this; + } + + public CmdOptionsBuilder withClientMethod(String clientMethod) { + this.clientMethod = clientMethod; + return this; + } + + public CmdOptionsBuilder withNullable(boolean nullable) { + this.nullable = nullable; + return this; + } + + public CmdOptionsBuilder withInput(String input) { + this.input = input; + return this; + } + + public CmdOptionsBuilder withLicensePath(String licensePath) { + this.licensePath = licensePath; + return this; + } + public CmdOptions build() { + return new CmdOptions(this); + } + } +} diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/ErrorMessages.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/ErrorMessages.java index c039e49d4..6ef882d75 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/ErrorMessages.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/ErrorMessages.java @@ -29,6 +29,12 @@ public class ErrorMessages { public static final String MISSING_CONTRACT_PATH = "An OpenAPI definition path is required to generate the " + "service. \ne.g: bal openapi --input or "; + public static final String INVALID_BALLERINA_PACKAGE = "ERROR: invalid Ballerina package directory: %s, " + + "cannot find 'Ballerina.toml' file.%n%n"; + public static final String INVALID_INPUT_PATH = "ERROR: provide input oas contract path.%n%n"; + + public static final String TOML_UPDATED_MSG = "The `Ballerina.toml` file is updated successfully with" + + " the OpenAPI tool configuration."; private ErrorMessages() { throw new AssertionError(); diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java index 27405d923..3cbceb26c 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java @@ -58,7 +58,8 @@ */ @CommandLine.Command( name = "openapi", - description = "Generate the Ballerina sources for a given OpenAPI definition and vice versa." + description = "Generate the Ballerina sources for a given OpenAPI definition and vice versa.", + subcommands = {Add.class} ) public class OpenApiCmd implements BLauncherCmd { private static final String CMD_NAME = "openapi"; @@ -68,36 +69,17 @@ public class OpenApiCmd implements BLauncherCmd { private boolean exitWhenFinish; private boolean clientResourceMode; - @CommandLine.Option(names = {"-h", "--help"}, hidden = true) - private boolean helpFlag; - - @CommandLine.Option(names = {"-i", "--input"}, description = "Generating the client and service both files") - private boolean inputPath; - - @CommandLine.Option(names = {"--license"}, description = "Location of the file which contains the license header") - private String licenseFilePath; + @CommandLine.Mixin + private BaseCmd baseCmd = new BaseCmd(); @CommandLine.Option(names = {"-o", "--output"}, description = "Location of the generated Ballerina service, " + "client and model files.") private String outputPath; - @CommandLine.Option(names = {"--mode"}, description = "Generate only service file or client file according to the" + - " given mode type") - private String mode; - - @CommandLine.Option(names = {"-n", "--nullable"}, description = "Generate the code by setting nullable true") - private boolean nullable; - @CommandLine.Option(names = {"-s", "--service"}, description = "Service name that need to documented as openapi " + "contract") private String service; - @CommandLine.Option(names = {"--tags"}, description = "Tag that need to write service") - private String tags; - - @CommandLine.Option(names = {"--operations"}, description = "Operations that need to write service") - private String operations; - @CommandLine.Option(names = {"--service-name"}, description = "Service name for generated files") private String generatedServiceName; @@ -107,10 +89,6 @@ public class OpenApiCmd implements BLauncherCmd { @CommandLine.Option(names = {"--with-tests"}, hidden = true, description = "Generate test files") private boolean includeTestFiles; - @CommandLine.Option(names = {"--client-methods"}, hidden = true, description = "Generate the client methods" + - " with provided type . Only \"resource\"(default) and \"remote\" options are supported.") - private String generateClientMethods; - @CommandLine.Option(names = {"--with-service-type"}, hidden = true, description = "Generate service type") private boolean generateServiceType; @@ -138,84 +116,77 @@ public OpenApiCmd(PrintStream outStream, Path executionDir, boolean exitWhenFini } @Override public void execute() { - - if (helpFlag) { + if (isHelp()) { String commandUsageInfo = BLauncherCmd.getCommandUsageInfo(getName()); outStream.println(commandUsageInfo); return; } //Check if cli input argument is present - if (inputPath) { - //Check if an OpenApi definition is provided - if (argList == null) { - outStream.println(ErrorMessages.MISSING_CONTRACT_PATH); + //Check if an OpenApi definition is provided + if (baseCmd.inputPath == null || baseCmd.inputPath.isBlank()) { + outStream.println(ErrorMessages.MISSING_CONTRACT_PATH); + exitError(this.exitWhenFinish); + return; + } + // If given input is yaml contract, it generates service file and client stub + // else if given ballerina service file it generates openapi contract file + // else it generates error message to enter correct input file + String fileName = baseCmd.inputPath; + if (fileName.endsWith(YAML_EXTENSION) || fileName.endsWith(JSON_EXTENSION) || + fileName.endsWith(YML_EXTENSION)) { + List tag = new ArrayList<>(); + List operation = new ArrayList<>(); + if (baseCmd.tags != null) { + tag.addAll(Arrays.asList(baseCmd.tags.split(","))); + } + if (baseCmd.operations != null) { + String[] ids = baseCmd.operations.split(","); + List normalizedOperationIds = + Arrays.stream(ids).map(operationId -> getValidName(operationId, false)) + .collect(Collectors.toList()); + operation.addAll(normalizedOperationIds); + } + Filter filter = new Filter(tag, operation); + + if (baseCmd.generateClientMethods != null && !baseCmd.generateClientMethods.isBlank() && + (!baseCmd.generateClientMethods.equals(RESOURCE) && + !baseCmd.generateClientMethods.equals(REMOTE))) { + // Exit the code generation process + outStream.println("'--client-methods' only supports `remote` or `resource` options."); exitError(this.exitWhenFinish); - return; } - // If given input is yaml contract, it generates service file and client stub - // else if given ballerina service file it generates openapi contract file - // else it generates error message to enter correct input file - String fileName = argList.get(0); - if (fileName.endsWith(YAML_EXTENSION) || fileName.endsWith(JSON_EXTENSION) || - fileName.endsWith(YML_EXTENSION)) { - List tag = new ArrayList<>(); - List operation = new ArrayList<>(); - if (tags != null) { - tag.addAll(Arrays.asList(tags.split(","))); - } - if (operations != null) { - String[] ids = operations.split(","); - List normalizedOperationIds = - Arrays.stream(ids).map(operationId -> getValidName(operationId, false)) - .collect(Collectors.toList()); - operation.addAll(normalizedOperationIds); - } - Filter filter = new Filter(tag, operation); + // Add the resource flag enable + clientResourceMode = baseCmd.generateClientMethods == null || baseCmd.generateClientMethods.isBlank() || + (!baseCmd.generateClientMethods.equals(REMOTE)); - if (generateClientMethods != null && !generateClientMethods.isBlank() && - (!generateClientMethods.equals(RESOURCE) && !generateClientMethods.equals(REMOTE))) { - // Exit the code generation process - outStream.println("'--client-methods' only supports `remote` or `resource` options."); - exitError(this.exitWhenFinish); - } - // Add the resource flag enable - clientResourceMode = generateClientMethods == null || generateClientMethods.isBlank() || - (!generateClientMethods.equals(REMOTE)); - - if (!clientResourceMode && mode != null && mode.equals(SERVICE)) { - // Exit the code generation process - outStream.println("'--client-methods' option is only available in client generation mode."); - exitError(this.exitWhenFinish); - } + if (!clientResourceMode && baseCmd.mode != null && baseCmd.mode.equals(SERVICE)) { + // Exit the code generation process + outStream.println("'--client-methods' option is only available in client generation mode."); + exitError(this.exitWhenFinish); + } - if (generateWithoutDataBinding && mode != null && mode.equals(CLIENT)) { - // Exit the code generation process - outStream.println("'--without-data-binding' option is only available in service generation mode."); - exitError(this.exitWhenFinish); - } - try { - openApiToBallerina(fileName, filter); - } catch (IOException e) { - outStream.println(e.getLocalizedMessage()); - exitError(this.exitWhenFinish); - } - } else if (fileName.endsWith(BAL_EXTENSION)) { - // Add the resource flag enable - if (generateClientMethods != null && !generateClientMethods.isBlank()) { - // Exit the code generation process - outStream.println("'--client-methods' option is only available in client generation mode."); - exitError(this.exitWhenFinish); - } - ballerinaToOpenApi(fileName); - } else { - outStream.println(ErrorMessages.MISSING_CONTRACT_PATH); + if (generateWithoutDataBinding && baseCmd.mode != null && baseCmd.mode.equals(CLIENT)) { + // Exit the code generation process + outStream.println("'--without-data-binding' option is only available in service generation mode."); exitError(this.exitWhenFinish); } + try { + openApiToBallerina(fileName, filter); + } catch (IOException e) { + outStream.println(e.getLocalizedMessage()); + exitError(this.exitWhenFinish); + } + } else if (fileName.endsWith(BAL_EXTENSION)) { + // Add the resource flag enable + if (baseCmd.generateClientMethods != null && !baseCmd.generateClientMethods.isBlank()) { + // Exit the code generation process + outStream.println("'--client-methods' option is only available in client generation mode."); + exitError(this.exitWhenFinish); + } + ballerinaToOpenApi(fileName); } else { - String commandUsageInfo = BLauncherCmd.getCommandUsageInfo(getName()); - outStream.println(commandUsageInfo); + outStream.println(ErrorMessages.MISSING_CONTRACT_PATH); exitError(this.exitWhenFinish); - return; } if (this.exitWhenFinish) { @@ -223,6 +194,11 @@ public void execute() { } } + private boolean isHelp() { + return baseCmd.helpFlag || (argList != null && argList.get(0).equals("help")) + || (argList == null && baseCmd.inputPath == null); + } + /** * This util method to generate openApi contract based on the given service ballerina file. * @param fileName input resource file @@ -286,12 +262,12 @@ private void openApiToBallerina(String fileName, Filter filter) throws IOExcepti } getTargetOutputPath(); Path resourcePath = Paths.get(openApiFile.getCanonicalPath()); - if (nullable) { + if (baseCmd.nullable) { outStream.println("WARNING: All the constraints in the OpenAPI contract will be ignored when generating" + " the Ballerina client/service with the `--nullable` option"); } - if (mode != null) { - switch (mode) { + if (baseCmd.mode != null) { + switch (baseCmd.mode) { case "service": generateServiceFile(generator, serviceName, resourcePath, filter); break; @@ -342,8 +318,8 @@ private void getTargetOutputPath() { private String setLicenseHeader() { String licenseHeader = ""; try { - if (this.licenseFilePath != null && !this.licenseFilePath.isBlank()) { - Path filePath = Paths.get((new File(this.licenseFilePath).getCanonicalPath())); + if (this.baseCmd.licenseFilePath != null && !this.baseCmd.licenseFilePath.isBlank()) { + Path filePath = Paths.get((new File(this.baseCmd.licenseFilePath).getCanonicalPath())); licenseHeader = Files.readString(Paths.get(filePath.toString())); if (!licenseHeader.endsWith("\n")) { licenseHeader = licenseHeader + "\n\n"; @@ -352,7 +328,7 @@ private String setLicenseHeader() { } } } catch (IOException e) { - outStream.println("Invalid license file path : " + this.licenseFilePath + + outStream.println("Invalid license file path : " + this.baseCmd.licenseFilePath + ". " + e.getMessage() + "."); exitError(this.exitWhenFinish); } @@ -368,7 +344,7 @@ private String setLicenseHeader() { private void generatesClientFile(BallerinaCodeGenerator generator, Path resourcePath, Filter filter, boolean resourceMode) { try { - generator.generateClient(resourcePath.toString(), targetOutputPath.toString(), filter, nullable, + generator.generateClient(resourcePath.toString(), targetOutputPath.toString(), filter, baseCmd.nullable, resourceMode); } catch (IOException | FormatterException | BallerinaOpenApiException e) { if (e.getLocalizedMessage() != null) { @@ -393,7 +369,7 @@ private void generateServiceFile(BallerinaCodeGenerator generator, String servic try { assert resourcePath != null; generator.generateService(resourcePath.toString(), serviceName, targetOutputPath.toString(), filter, - nullable, generateServiceType, generateWithoutDataBinding); + baseCmd.nullable, generateServiceType, generateWithoutDataBinding); } catch (IOException | FormatterException | BallerinaOpenApiException e) { outStream.println("Error occurred when generating service for OpenAPI contract at " + argList.get(0) + ". " + e.getMessage() + "."); @@ -412,7 +388,7 @@ private void generateBothFiles(BallerinaCodeGenerator generator, String fileName try { assert resourcePath != null; generator.generateClientAndService(resourcePath.toString(), fileName, targetOutputPath.toString(), filter, - nullable, generateClientResourceFunctions, generateServiceType, generateWithoutDataBinding); + baseCmd.nullable, generateClientResourceFunctions, generateServiceType, generateWithoutDataBinding); } catch (IOException | BallerinaOpenApiException | FormatterException e) { outStream.println("Error occurred when generating service for openAPI contract at " + argList.get(0) + "." + " " + e.getMessage() + "."); diff --git a/openapi-cli/src/main/java/module-info.java b/openapi-cli/src/main/java/module-info.java index fb892fe7e..2ca2a94f4 100644 --- a/openapi-cli/src/main/java/module-info.java +++ b/openapi-cli/src/main/java/module-info.java @@ -21,6 +21,7 @@ requires io.ballerina.parser; requires io.ballerina.stdlib.http; requires io.ballerina.cli; + requires io.ballerina.toml; requires io.ballerina.tools.api; requires io.ballerina.formatter.core; requires io.ballerina.openapi.service; diff --git a/openapi-cli/src/main/resources/cli-help/ballerina-openapi-add.help b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-add.help new file mode 100644 index 000000000..74aa806cd --- /dev/null +++ b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-add.help @@ -0,0 +1,71 @@ +NAME + bal openapi add - Update the `Ballerina.toml` file with the OpenAPI tool configuration details for generating a Ballerina service or client. + +SYNOPSIS + bal openapi add [-i | --input] + [-p | --package] + [--id] + [--module] + [--mode] + [--tags] + [--operations] + [-n | --nullable] + [--license] + [--client-methods] + +DESCRIPTION + Update the `Ballerina.toml` file with the OpenAPI tool configuration details for generating a Ballerina source + (either a mock service or a client stub) from the given OpenAPI definition file and other command options. + +OPTIONS + -i, --input + This is a mandatory input. The given OpenAPI contract will + generate a Ballerina service and the client stub files for the given + OpenAPI contract. The OpenAPI contract can be either a YAML or a JSON. + + --id + This is a mandatory input. The given service/client ID will be used + to identify the generated service/client in the `Ballerina.toml` file. + This ID will be used to generate the Ballerina service/client + sources cache in the target. + + --module + This is a mandatory input. The given module name will be used to + generate the Ballerina service/client sources in the given module + directory. + + -p, --package + This is an optional input. The given package location will be used + to update the `Ballerina.toml` file in the given + package directory. + + --mode + Mode type can be 'service' or 'client'. The Ballerina service and + client will be generated according to the specified mode. The default option is `client`. + + -n, --nullable + Optional. The JSON schema properties that are not marked as `nullable:true` may + return as null in some responses. It will result in a JSON schema to + Ballerina record data binding error. This is a safe option to + generate all data types in the record with Ballerina nil support. + + --license + Optional. The `.bal` files will generate with the given copyright or + license header. + + --tags + Optional. These tags are used to filter the operations tags that are needed to + generate services. + + --operations + Optional. List of operations to generate the Ballerina service or client. + + --client-methods + Optional. This option can be used in client generation to select the client method type, which can be `resource` or `remote`. The default option is `resource`. + +EXAMPLES + Update the `Ballerina.toml` file for generating a Ballerina client using a `hello.yaml` OpenAPI contract to generate a directory named `hello`. + $ bal openapi add -i hello.yaml --id hello_client --mode client --module hello + + Update the `Ballerina.toml` file for generating a Ballerina client using a `hello.yaml` OpenAPI contract with the tags filter. + $ bal openapi add -i hello.yaml --id hello_client --mode client --module hello --tags tag_ID diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/NegativeCmdTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/NegativeCmdTests.java new file mode 100644 index 000000000..65f108e16 --- /dev/null +++ b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/NegativeCmdTests.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 Inc. 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.cmd; + +import org.testng.Assert; +import org.testng.annotations.Test; +import picocli.CommandLine; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * This class is to store negative tests related to openAPI CLI. + */ +public class NegativeCmdTests extends OpenAPICommandTest { + @Test(description = "Test for invalid ballerina package in `add` sub command") + public void testInvalidBallerinaPackage() throws IOException { + Path resourceDir = Paths.get(System.getProperty("user.dir")).resolve("build/resources/test"); + Path packagePath = resourceDir.resolve(Paths.get("cmd")); + String[] addArgs = {"--input", "petstore.yaml", "-p", packagePath.toString(), + "--module", "delivery", "--nullable", "--license", "license.txt", "--mode", "client", + "--client-methods", "resource"}; + Add add = new Add(printStream, false); + new CommandLine(add).parseArgs(addArgs); + add.execute(); + String output = readOutput(true); + Assert.assertTrue(output.contains("ERROR: invalid Ballerina package directory:")); + } +} diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java index 98a5ed278..e8e780078 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java @@ -87,14 +87,15 @@ public void testOpenAPICmdHelpWithoutFlag() throws IOException { " ballerina-openapi - Generate a Ballerina service")); } - @Test(description = "Test openapi gen-service without openapi contract file") + @Test(description = "Test openapi gen-service without openapi contract file", + expectedExceptions = {CommandLine.MissingParameterException.class}) public void testWithoutOpenApiContract() throws IOException { String[] args = {"--input"}; OpenApiCmd cmd = new OpenApiCmd(printStream, tmpDir, false); new CommandLine(cmd).parseArgs(args); cmd.execute(); String output = readOutput(true); - Assert.assertTrue(output.contains("An OpenAPI definition path is required to generate the service.")); + Assert.assertTrue(output.contains("Missing required parameter for option '--input' ()")); } @Test(description = "Test openapi gen-service for successful service generation") @@ -738,6 +739,29 @@ public void testForComplexPathInBothClientAndService() { "Following files were created.")); } + @Test(description = "Test openapi add sub command") + public void testAddCmd() throws IOException { + Path resourceDir = Paths.get(System.getProperty("user.dir")).resolve("build/resources/test"); + Path packagePath = resourceDir.resolve(Paths.get("cmd/bal-task-client")); + String[] addArgs = {"--input", "petstore.yaml", "-p", packagePath.toString(), + "--module", "delivery", "--nullable", "--license", "license.txt", "--mode", "client", + "--client-methods", "resource"}; + Add add = new Add(printStream, false); + new CommandLine(add).parseArgs(addArgs); + add.execute(); + String newLine = System.lineSeparator(); + String tomlContent = Files.readString(packagePath.resolve("Ballerina.toml")); + String generatedTool = "[[tool.openapi]]" + newLine + + "id = \"oas_client_petstore\"" + newLine + + "filePath = \"petstore.yaml\"" + newLine + + "targetModule = \"delivery\"" + newLine + + "options.mode = \"client\"" + newLine + + "options.nullable = true" + newLine + + "options.clientMethods = \"resource\"" + newLine + + "options.licensePath = \"license.txt\"" + newLine; + Assert.assertTrue(tomlContent.contains(generatedTool)); + } + @AfterTest public void clean() { System.setErr(null); diff --git a/openapi-cli/src/test/resources/cmd/bal-task-client/Ballerina.toml b/openapi-cli/src/test/resources/cmd/bal-task-client/Ballerina.toml new file mode 100644 index 000000000..098917d06 --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/bal-task-client/Ballerina.toml @@ -0,0 +1,11 @@ +[project] +org-name = "test" +version = "0.1.0" + +[[tool.openapi]] +id = "client" +filePath = "sample.yaml" +targetModule = "delivery" +options.mode = "client" +options.tags = ["1","2","3"] +options.nullable = true diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_02/Ballerina.toml b/openapi-cli/src/test/resources/cmd/bal-task-client/openapi.yaml similarity index 100% rename from openapi-integration-tests/src/test/resources/client-idl-projects/project_02/Ballerina.toml rename to openapi-cli/src/test/resources/cmd/bal-task-client/openapi.yaml diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_03/Ballerina.toml b/openapi-cli/src/test/resources/cmd/bal-task-client/sample.yaml similarity index 100% rename from openapi-integration-tests/src/test/resources/client-idl-projects/project_03/Ballerina.toml rename to openapi-cli/src/test/resources/cmd/bal-task-client/sample.yaml diff --git a/openapi-cli/src/test/resources/testng.xml b/openapi-cli/src/test/resources/testng.xml index 5273f05fb..c2931b64f 100644 --- a/openapi-cli/src/test/resources/testng.xml +++ b/openapi-cli/src/test/resources/testng.xml @@ -50,6 +50,7 @@ under the License. + diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/BallerinaClientGenerator.java b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/BallerinaClientGenerator.java index 47aa8b535..adfcb1fe3 100644 --- a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/BallerinaClientGenerator.java +++ b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/BallerinaClientGenerator.java @@ -487,8 +487,7 @@ private List createRemoteFunctions(Paths paths, Filter f if (!filterTags.isEmpty() || !filterOperations.isEmpty()) { // Generate remote function only if it is available in tag filter or operation filter or both if (operationTags != null || ((!filterOperations.isEmpty()) && (operationId != null))) { - if (GeneratorUtils.hasTags(operationTags, filterTags) || - ((operationId != null) && filterOperations.contains(operationId.trim()))) { + if (isaFilteredOperation(filterTags, filterOperations, operationTags, operationId)) { // Generate remote function FunctionDefinitionNode functionDefinitionNode = getClientMethodFunctionDefinitionNode( @@ -508,6 +507,12 @@ private List createRemoteFunctions(Paths paths, Filter f return functionDefinitionNodeList; } + private static boolean isaFilteredOperation(List filterTags, List filterOperations, + List operationTags, String operationId) { + return (operationTags != null && GeneratorUtils.hasTags(operationTags, filterTags)) || + ((operationId != null) && filterOperations.contains(operationId.trim())); + } + /** * Generate function definition node. *
diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/model/OASClientConfig.java b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/model/OASClientConfig.java
index 5875cc2bb..edc4f9a0f 100644
--- a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/model/OASClientConfig.java
+++ b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/client/model/OASClientConfig.java
@@ -20,6 +20,8 @@
 import io.ballerina.openapi.core.model.Filter;
 import io.swagger.v3.oas.models.OpenAPI;
 
+import static io.ballerina.openapi.core.GeneratorConstants.DO_NOT_MODIFY_FILE_HEADER;
+
 /**
  * This class stores metadata that related to client code generations.
  *
@@ -31,6 +33,7 @@ public class OASClientConfig {
     private final boolean nullable;
     private final boolean resourceMode;
     private final boolean isPlugin;
+    private final String license;
 
 
     private OASClientConfig(Builder clientConfigBuilder) {
@@ -39,6 +42,7 @@ private OASClientConfig(Builder clientConfigBuilder) {
         this.nullable = clientConfigBuilder.nullable;
         this.isPlugin = clientConfigBuilder.isPlugin;
         this.resourceMode = clientConfigBuilder.resourceMode;
+        this.license = clientConfigBuilder.license;
     }
 
     public OpenAPI getOpenAPI() {
@@ -60,6 +64,9 @@ public boolean isResourceMode() {
     public boolean isPlugin() {
         return isPlugin;
     }
+    public String getLicense() {
+        return license;
+    }
 
     /**
      * Client IDL plugin meta data builder class.
@@ -70,6 +77,7 @@ public static class Builder {
         private boolean nullable = false;
         private boolean resourceMode = true;
         private boolean isPlugin = false;
+        private String license = DO_NOT_MODIFY_FILE_HEADER;
 
         public Builder withOpenAPI(OpenAPI openAPI) {
             this.openAPI = openAPI;
@@ -96,6 +104,11 @@ public Builder withPlugin(boolean isPlugin) {
             return this;
         }
 
+        public Builder withLicense(String license) {
+            this.license = license;
+            return this;
+        }
+
         public OASClientConfig build() {
             return new OASClientConfig(this);
         }
diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/model/GenSrcFile.java b/openapi-core/src/main/java/io/ballerina/openapi/core/model/GenSrcFile.java
index 55d4463e2..2e1adde2e 100644
--- a/openapi-core/src/main/java/io/ballerina/openapi/core/model/GenSrcFile.java
+++ b/openapi-core/src/main/java/io/ballerina/openapi/core/model/GenSrcFile.java
@@ -39,7 +39,8 @@ public enum GenFileType {
         TEST_SRC,
         CONFIG_SRC,
         RES,
-        UTIL_SRC;
+        UTIL_SRC,
+        CACHE_SRC;
 
         public boolean isOverwritable() {
             if (this == GEN_SRC || this == RES || this == MODEL_SRC || this == UTIL_SRC) {
diff --git a/openapi-integration-tests/src/test/java/io/ballerina/openapi/TestUtil.java b/openapi-integration-tests/src/test/java/io/ballerina/openapi/TestUtil.java
index 200c69b9a..8ac620351 100644
--- a/openapi-integration-tests/src/test/java/io/ballerina/openapi/TestUtil.java
+++ b/openapi-integration-tests/src/test/java/io/ballerina/openapi/TestUtil.java
@@ -40,8 +40,8 @@
 import java.util.stream.Stream;
 
 import static io.ballerina.openapi.extension.build.ValidatorTests.WHITESPACE_PATTERN;
-import static io.ballerina.openapi.idl.client.IDLClientGenPluginTests.DISTRIBUTION_FILE_NAME;
-import static io.ballerina.openapi.idl.client.IDLClientGenPluginTests.TEST_RESOURCE;
+import static io.ballerina.openapi.bal.task.client.ClientGenPluginTests.DISTRIBUTION_FILE_NAME;
+import static io.ballerina.openapi.bal.task.client.ClientGenPluginTests.TEST_RESOURCE;
 
 /**
  * This class for storing the test utils for integration tests.
@@ -192,9 +192,9 @@ public static void deleteGeneratedFiles(String generatedFileName) throws IOExcep
     public static File[] getMatchingFiles(String project, List ids) throws IOException, InterruptedException {
         List buildArgs = new LinkedList<>();
         //TODO: Change this function after fixing module name with client declaration alias.
-        Process process = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve(project), buildArgs);
-        Assert.assertEquals(process.waitFor(), 0);
-        File dir = new File(RESOURCE.resolve("client-idl-projects/" + project + "/generated/").toString());
+        boolean isExit = executeBuild(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve(project), buildArgs);
+        Assert.assertTrue(isExit);
+        File dir = new File(RESOURCE.resolve("client-bal-task-projects/" + project + "/generated/").toString());
         File[] matchingFiles = dir.listFiles(new FileFilter() {
             public boolean accept(File pathname) {
                 for (String id : ids) {
diff --git a/openapi-integration-tests/src/test/java/io/ballerina/openapi/idl/client/IDLClientGenPluginNegativeTests.java b/openapi-integration-tests/src/test/java/io/ballerina/openapi/bal/task/client/ClientGenPluginNegativeTests.java
similarity index 86%
rename from openapi-integration-tests/src/test/java/io/ballerina/openapi/idl/client/IDLClientGenPluginNegativeTests.java
rename to openapi-integration-tests/src/test/java/io/ballerina/openapi/bal/task/client/ClientGenPluginNegativeTests.java
index f9a1c043a..7c48e7c47 100644
--- a/openapi-integration-tests/src/test/java/io/ballerina/openapi/idl/client/IDLClientGenPluginNegativeTests.java
+++ b/openapi-integration-tests/src/test/java/io/ballerina/openapi/bal/task/client/ClientGenPluginNegativeTests.java
@@ -16,7 +16,7 @@
  * under the License.
  */
 
-package io.ballerina.openapi.idl.client;
+package io.ballerina.openapi.bal.task.client;
 
 import io.ballerina.openapi.OpenAPITest;
 import io.ballerina.openapi.TestUtil;
@@ -43,10 +43,10 @@
 /**
  * Negative tests for OpenAPI client IDL import.
  */
-public class IDLClientGenPluginNegativeTests extends OpenAPITest {
+public class ClientGenPluginNegativeTests extends OpenAPITest {
 
     public static final String DISTRIBUTION_FILE_NAME = DISTRIBUTIONS_DIR.toString();
-    public static final Path TEST_RESOURCE = Paths.get(RESOURCES_PATH.toString() + "/client-idl-projects");
+    public static final Path TEST_RESOURCE = Paths.get(RESOURCES_PATH.toString() + "/client-bal-task-projects");
     @BeforeClass
     public void setupDistributions() throws IOException {
         TestUtil.cleanDistribution();
@@ -56,7 +56,7 @@ public void setupDistributions() throws IOException {
     public void testClientDeclarationInsideFunction() throws IOException, InterruptedException {
         Process process = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve("project_04"),
                 new ArrayList<>());
-        File dir = new File(RESOURCE.resolve("client-idl-projects/project_04/generated/").toString());
+        File dir = new File(RESOURCE.resolve("client-bal-task-projects/project_04/generated/").toString());
         Assert.assertFalse(dir.exists());
         String msg = "ERROR [main.bal:(2:5,2:11)] 'client' qualifier not allowed";
         assertOnErrorStream(process, msg);
@@ -66,7 +66,7 @@ public void testClientDeclarationInsideFunction() throws IOException, Interrupte
     public void testNonOpenAPIClientDeclaration() throws IOException, InterruptedException {
         Process process = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve("project_07"),
                 new ArrayList<>());
-        File dir = new File(RESOURCE.resolve("client-idl-projects/project_07/generated/").toString());
+        File dir = new File(RESOURCE.resolve("client-bal-task-projects/project_07/generated/").toString());
         Assert.assertFalse(dir.exists());
         String msg = "ERROR [main.bal:(1:1,1:176)] no matching plugin found for client declaration";
         assertOnErrorStream(process, msg);
@@ -76,7 +76,7 @@ public void testNonOpenAPIClientDeclaration() throws IOException, InterruptedExc
     public void testInvalidSwaggerRemotePath() throws IOException, InterruptedException {
         Process process = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve("project_10"),
                 new ArrayList<>());
-        File dir = new File(RESOURCE.resolve("client-idl-projects/project_10/generated/").toString());
+        File dir = new File(RESOURCE.resolve("client-bal-task-projects/project_10/generated/").toString());
         Assert.assertFalse(dir.exists());
         String msg = "ERROR [main.bal:(1:1,1:132)] unable to get resource from uri, reason: ";
         assertOnErrorStream(process, msg);
@@ -86,7 +86,7 @@ public void testInvalidSwaggerRemotePath() throws IOException, InterruptedExcept
     public void testInvalidSwaggerLocalPath() throws IOException, InterruptedException {
         Process process = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve("project_11"),
                 new ArrayList<>());
-        File dir = new File(RESOURCE.resolve("client-idl-projects/project_11/generated/").toString());
+        File dir = new File(RESOURCE.resolve("client-bal-task-projects/project_11/generated/").toString());
         Assert.assertFalse(dir.exists());
         String msg = "ERROR [main.bal:(1:1,1:31)] could not locate the file:";
         assertOnErrorStream(process, msg);
@@ -96,7 +96,7 @@ public void testInvalidSwaggerLocalPath() throws IOException, InterruptedExcepti
     public void testInvalidSwaggerContract() throws IOException, InterruptedException {
         Process process = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve("project_12"),
                 new ArrayList<>());
-        File dir = new File(RESOURCE.resolve("client-idl-projects/project_12/generated/").toString());
+        File dir = new File(RESOURCE.resolve("client-bal-task-projects/project_12/generated/").toString());
         Assert.assertFalse(dir.exists());
         String msg = "ERROR [main.bal:(1:1,1:30)] OpenAPI definition has errors:";
         assertOnErrorStream(process, msg);
@@ -108,7 +108,7 @@ public void testInvalidSwaggerContract() throws IOException, InterruptedExceptio
     public void testUnsupportedSwaggerVersion() throws IOException, InterruptedException {
         Process process = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve("project_13"),
                 new ArrayList<>());
-        File dir = new File(RESOURCE.resolve("client-idl-projects/project_13/generated/").toString());
+        File dir = new File(RESOURCE.resolve("client-bal-task-projects/project_13/generated/").toString());
         Assert.assertFalse(dir.exists());
         String msg = "ERROR [main.bal:(2:5,2:119)] provided openAPI contract version is not supported";
         assertOnErrorStream(process, msg);
diff --git a/openapi-integration-tests/src/test/java/io/ballerina/openapi/bal/task/client/ClientGenPluginTests.java b/openapi-integration-tests/src/test/java/io/ballerina/openapi/bal/task/client/ClientGenPluginTests.java
new file mode 100644
index 000000000..e31bd82a2
--- /dev/null
+++ b/openapi-integration-tests/src/test/java/io/ballerina/openapi/bal/task/client/ClientGenPluginTests.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com).
+ *
+ * WSO2 Inc. 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.bal.task.client;
+
+import io.ballerina.openapi.OpenAPITest;
+import io.ballerina.openapi.TestUtil;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedList;
+import java.util.List;
+
+import static io.ballerina.openapi.TestUtil.DISTRIBUTIONS_DIR;
+import static io.ballerina.openapi.TestUtil.RESOURCES_PATH;
+import static io.ballerina.openapi.TestUtil.getMatchingFiles;
+
+/**
+ * This class is for client ballerina package integration tests.
+ */
+public class ClientGenPluginTests extends OpenAPITest {
+
+    public static final String DISTRIBUTION_FILE_NAME = DISTRIBUTIONS_DIR.toString();
+    public static final Path TEST_RESOURCE = Paths.get(RESOURCES_PATH.toString() + "/client-bal-task-projects");
+
+    @BeforeClass
+    public void setupDistributions() throws IOException {
+        TestUtil.cleanDistribution();
+    }
+
+    @Test
+    public void testValidSwaggerContract() throws IOException, InterruptedException {
+        List ids = new LinkedList<>();
+        ids.add("delivery01");
+        ids.add("delivery02");
+        ids.add("delivery03");
+        ids.add("delivery04");
+        ids.add("delivery05");
+        File[] matchingFiles = getMatchingFiles("project_01", ids);
+        Assert.assertNotNull(matchingFiles);
+        Assert.assertEquals(matchingFiles.length, 5);
+    }
+}
diff --git a/openapi-integration-tests/src/test/java/io/ballerina/openapi/idl/client/IDLClientGenPluginTests.java b/openapi-integration-tests/src/test/java/io/ballerina/openapi/idl/client/IDLClientGenPluginTests.java
deleted file mode 100644
index aacff40f4..000000000
--- a/openapi-integration-tests/src/test/java/io/ballerina/openapi/idl/client/IDLClientGenPluginTests.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com). All Rights Reserved.
- *
- * WSO2 Inc. 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.idl.client;
-
-import io.ballerina.openapi.OpenAPITest;
-import io.ballerina.openapi.TestUtil;
-import org.testng.Assert;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static io.ballerina.openapi.TestUtil.DISTRIBUTIONS_DIR;
-import static io.ballerina.openapi.TestUtil.RESOURCE;
-import static io.ballerina.openapi.TestUtil.RESOURCES_PATH;
-import static io.ballerina.openapi.TestUtil.executeRun;
-import static io.ballerina.openapi.TestUtil.getMatchingFiles;
-
-/**
- * This class is for client IDL integration tests.
- */
-public class IDLClientGenPluginTests extends OpenAPITest {
-
-    public static final String DISTRIBUTION_FILE_NAME = DISTRIBUTIONS_DIR.toString();
-    public static final Path TEST_RESOURCE = Paths.get(RESOURCES_PATH.toString() + "/client-idl-projects");
-
-    @BeforeClass
-    public void setupDistributions() throws IOException {
-        TestUtil.cleanDistribution();
-    }
-
-    @Test
-    public void testValidSwaggerContract() throws IOException, InterruptedException {
-        List ids = new LinkedList<>();
-        ids.add("bar");
-        File[] matchingFiles = getMatchingFiles("project_01", ids);
-        Assert.assertNotNull(matchingFiles);
-        Assert.assertEquals(matchingFiles.length, 1);
-    }
-
-    @Test
-    public void testClientDeclarationWithOutAnnotation() throws IOException, InterruptedException {
-        List ids = new LinkedList<>();
-        ids.add("foo");
-        File[] matchingFiles = getMatchingFiles("project_02", ids);
-        Assert.assertNotNull(matchingFiles);
-        Assert.assertEquals(matchingFiles.length, 1);
-    }
-
-    @Test
-    public void testClientDeclarationWithAnnotation() throws IOException, InterruptedException {
-        List ids = new LinkedList<>();
-        ids.add("bar");
-        File[] matchingFiles = getMatchingFiles("project_03", ids);
-        Assert.assertNotNull(matchingFiles);
-        Assert.assertEquals(matchingFiles.length, 1);
-    }
-
-    @Test
-    public void testModuleLevelClientDeclarationNode() throws IOException, InterruptedException {
-        List ids = new LinkedList<>();
-        ids.add("bar");
-        File[] matchingFiles = getMatchingFiles("project_05", ids);
-        Assert.assertNotNull(matchingFiles);
-        Assert.assertEquals(matchingFiles.length, 1);
-    }
-
-    @Test(description = "When multiple client declarations have same annotation")
-    public void testMultipleClientsWithSameAnnotation() throws IOException, InterruptedException {
-        List ids = new LinkedList<>();
-        ids.add("foo");
-        File[] matchingFiles = getMatchingFiles("project_06", ids);
-        Assert.assertNotNull(matchingFiles);
-        Assert.assertEquals(matchingFiles.length, 1);
-    }
-
-    @Test
-    public void testInvokeAPIFromGeneratedClient() throws IOException, InterruptedException {
-        Process successful = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve("project_08"),
-                new ArrayList<>());
-        Assert.assertEquals(successful.waitFor(), 0);
-    }
-
-    @Test(description = "test client declarations with local YAML contract paths")
-    public void testWithLocalPathYAML() throws IOException, InterruptedException {
-        List ids = new LinkedList<>();
-        ids.add("bar");
-        File[] matchingFiles = getMatchingFiles("project_09", ids);
-        Assert.assertNotNull(matchingFiles);
-        Assert.assertEquals(matchingFiles.length, 1);
-    }
-
-    @Test(description = "test client declarations with local JSON contract paths")
-    public void testWithLocalPathJSON() throws IOException, InterruptedException {
-        List ids = new LinkedList<>();
-        ids.add("bar");
-        File[] matchingFiles = getMatchingFiles("project_14", ids);
-        Assert.assertNotNull(matchingFiles);
-        Assert.assertEquals(matchingFiles.length, 1);
-    }
-
-    @Test
-    public void testWithClientClassName() throws IOException, InterruptedException {
-        String classContent = "public isolated client class Client {";
-        Process process = executeRun(DISTRIBUTION_FILE_NAME, TEST_RESOURCE.resolve("project_15"), new LinkedList<>());
-        process.waitFor();
-        Path clientFile =
-                Path.of(RESOURCE.resolve("client-idl-projects/project_15/generated/bar/client.bal").toString());
-        Stream expectedServiceLines = Files.lines(clientFile);
-        String generatedContent = expectedServiceLines.collect(Collectors.joining(System.lineSeparator()));
-        expectedServiceLines.close();
-        Assert.assertTrue((generatedContent.trim()).replaceAll("\\s+", "")
-                .contains(classContent.trim().replaceAll("\\s+", "")));
-    }
-}
diff --git a/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/Ballerina.toml
new file mode 100644
index 000000000..2dcba2601
--- /dev/null
+++ b/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/Ballerina.toml
@@ -0,0 +1,45 @@
+[package]
+org = "hansaninissanka"
+name = "client_bal_task"
+version = "0.1.0"
+distribution = "2201.9.0"
+
+[build-options]
+observabilityIncluded = true
+
+# with input valid yaml
+[[tool.openapi]]
+id = "client"
+filePath = "./openapi.yaml"
+targetModule = "delivery01"
+options.mode = "client"
+
+
+# with input as url
+[[tool.openapi]]
+id = "client02"
+filePath = "openapi.yaml"
+targetModule = "delivery02"
+options.mode = "client"
+
+# without target
+[[tool.openapi]]
+id = "client03"
+filePath = "openapi.yaml"
+targetModule = "delivery03"
+options.mode = "client"
+options.license = "license.txt"
+
+# with license file
+[[tool.openapi]]
+id = "client04"
+filePath = "openapi.yaml"
+targetModule = "delivery04"
+options.mode = "client"
+options.license = "license.txt"
+
+# defualt behaviour
+[[tool.openapi]]
+id = "client05"
+filePath = "openapi.yaml"
+targetModule = "delivery05"
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_04/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/license.txt
similarity index 100%
rename from openapi-integration-tests/src/test/resources/client-idl-projects/project_04/Ballerina.toml
rename to openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/license.txt
diff --git a/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/main.bal b/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/main.bal
new file mode 100644
index 000000000..6b71ef415
--- /dev/null
+++ b/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/main.bal
@@ -0,0 +1,2 @@
+public function main() {
+}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_01/openapi.yaml b/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/openapi.yaml
similarity index 88%
rename from openapi-integration-tests/src/test/resources/client-idl-projects/project_01/openapi.yaml
rename to openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/openapi.yaml
index f5d6142f8..e67d24d70 100644
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_01/openapi.yaml
+++ b/openapi-integration-tests/src/test/resources/client-bal-task-projects/project_01/openapi.yaml
@@ -8,8 +8,9 @@ servers:
 paths:
   /users:
     get:
+      operationId: listUsers
       summary: Returns a list of users.
       description: Optional extended description in Markdown.
       responses:
-        '200':
+        200:
           description: OK
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_01/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_01/Ballerina.toml
deleted file mode 100644
index 005e00d52..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_01/Ballerina.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-org = "openapi_client_idl_test"
-name = "project_1"
-version = "0.1.0"
-
-[build-options]
-observabilityIncluded = true
-
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_01/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_01/main.bal
deleted file mode 100644
index d6bb4a209..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_01/main.bal
+++ /dev/null
@@ -1,4 +0,0 @@
-client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/openweathermap/openapi.yaml" as bar;
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_02/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_02/main.bal
deleted file mode 100644
index ffc7af250..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_02/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/openweathermap/openapi.yaml" as foo;
-
-public function main() {
-    foo:client x;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_03/license.txt b/openapi-integration-tests/src/test/resources/client-idl-projects/project_03/license.txt
deleted file mode 100644
index 44b7f2743..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_03/license.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-//abc abc
-//abc abc
\ No newline at end of file
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_03/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_03/main.bal
deleted file mode 100644
index 5324da7d7..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_03/main.bal
+++ /dev/null
@@ -1,14 +0,0 @@
-import ballerina/openapi;
-
-@openapi:ClientConfig {
-    tags:["abc", "ads"],
-    operations: ["o1"],
-    nullable: false,
-    isResource: false,
-    license: "license.txt"
-}
-client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/openweathermap/openapi.yaml" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_04/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_04/main.bal
deleted file mode 100644
index 152c8c34f..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_04/main.bal
+++ /dev/null
@@ -1,4 +0,0 @@
-public function main() {
-    client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/openweathermap/openapi.yaml" as foo;
-    foo:client x;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_05/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_05/Ballerina.toml
deleted file mode 100644
index e69de29bb..000000000
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_05/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_05/main.bal
deleted file mode 100644
index c8670b223..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_05/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/openweathermap/openapi.yaml" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_06/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_06/Ballerina.toml
deleted file mode 100644
index e69de29bb..000000000
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_06/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_06/main.bal
deleted file mode 100644
index 5554e6408..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_06/main.bal
+++ /dev/null
@@ -1,20 +0,0 @@
-import ballerina/openapi;
-
-@openapi:ClientConfig {
-    tags:["abc", "ads"],
-    operations: ["o1"],
-    nullable: false
-}
-client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/openweathermap/openapi.yaml" as foo;
-
-@openapi:ClientConfig {
-    tags:["abc", "ads"],
-    operations: ["o1"],
-    nullable: false
-}
-client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/openweathermap/openapi.yaml" as bar;
-
-public function main() {
-    bar:client y;
-    foo:client x;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_07/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_07/Ballerina.toml
deleted file mode 100644
index e69de29bb..000000000
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_07/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_07/main.bal
deleted file mode 100644
index 268e7070e..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_07/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "https://raw.githubusercontent.com/ballerina-platform/graphql-tools/main/graphql-cli/src/test/resources/specs/graphql-config-with-basic-auth-client-config.yaml" as foo;
-
-public function main() {
-    foo:client x;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_08/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_08/Ballerina.toml
deleted file mode 100644
index e69de29bb..000000000
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_08/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_08/main.bal
deleted file mode 100644
index 8e88847dc..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_08/main.bal
+++ /dev/null
@@ -1,16 +0,0 @@
-import ballerina/io;
-import ballerina/openapi;
-
-@openapi:ClientConfig {
-    operations: ["getUSCountiesStatus"],
-    nullable: true
-}
-client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/covid19/openapi.yaml" as bar;
-
-public function main() returns error? {
-    bar:client countryClient = check new ();
-    bar:CovidJHUCounties country = check countryClient->/v3/covid\-19/jhucsse/counties.get;
-    if country is bar:CovidJHUCounty[] {
-        io:println(country[0]);
-    }
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/Ballerina.toml
deleted file mode 100644
index 005e00d52..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/Ballerina.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-org = "openapi_client_idl_test"
-name = "project_1"
-version = "0.1.0"
-
-[build-options]
-observabilityIncluded = true
-
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/main.bal
deleted file mode 100644
index 5a58da41f..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "openapi.yaml" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/openapi.yaml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/openapi.yaml
deleted file mode 100644
index f5d6142f8..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_09/openapi.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-openapi: 3.0.1
-info:
-  title: Sample API
-  description: API description in Markdown.
-  version: 1.0.0
-servers:
-  - url: 'https://api.example.com'
-paths:
-  /users:
-    get:
-      summary: Returns a list of users.
-      description: Optional extended description in Markdown.
-      responses:
-        '200':
-          description: OK
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/Ballerina.toml
deleted file mode 100644
index 005e00d52..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/Ballerina.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-org = "openapi_client_idl_test"
-name = "project_1"
-version = "0.1.0"
-
-[build-options]
-observabilityIncluded = true
-
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/main.bal
deleted file mode 100644
index 4d3747421..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "https://raw.githubusercontent.com/ballerina-platform/openapi-connectors/main/openapi/openweathermap01/openapi.yaml" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/openapi.yaml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/openapi.yaml
deleted file mode 100644
index f5d6142f8..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_10/openapi.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-openapi: 3.0.1
-info:
-  title: Sample API
-  description: API description in Markdown.
-  version: 1.0.0
-servers:
-  - url: 'https://api.example.com'
-paths:
-  /users:
-    get:
-      summary: Returns a list of users.
-      description: Optional extended description in Markdown.
-      responses:
-        '200':
-          description: OK
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/Ballerina.toml
deleted file mode 100644
index 005e00d52..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/Ballerina.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-org = "openapi_client_idl_test"
-name = "project_1"
-version = "0.1.0"
-
-[build-options]
-observabilityIncluded = true
-
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/main.bal
deleted file mode 100644
index e1d9c107e..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "openapi2.yaml" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/openapi.yaml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/openapi.yaml
deleted file mode 100644
index f5d6142f8..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_11/openapi.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-openapi: 3.0.1
-info:
-  title: Sample API
-  description: API description in Markdown.
-  version: 1.0.0
-servers:
-  - url: 'https://api.example.com'
-paths:
-  /users:
-    get:
-      summary: Returns a list of users.
-      description: Optional extended description in Markdown.
-      responses:
-        '200':
-          description: OK
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/Ballerina.toml
deleted file mode 100644
index 005e00d52..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/Ballerina.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-org = "openapi_client_idl_test"
-name = "project_1"
-version = "0.1.0"
-
-[build-options]
-observabilityIncluded = true
-
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/main.bal
deleted file mode 100644
index 5a58da41f..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "openapi.yaml" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/openapi.yaml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/openapi.yaml
deleted file mode 100644
index f57c88172..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_12/openapi.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-openapi: 3.0.1
-info:
-  title: Sample API
-  description: API description in Markdown.
-  version: 1.0.0
-servers:
-  - url: 'https://api.example.com'
-paths:
-  /users/{id}:
-    get:
-      summary: Returns a list of users.
-      description: Optional extended description in Markdown.
-      responses:
-        '200':
-          description: OK
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/Ballerina.toml
deleted file mode 100644
index ea7803f1a..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/Ballerina.toml
+++ /dev/null
@@ -1,7 +0,0 @@
-[package]
-org = "openapi_client_idl_test"
-name = "project_1"
-version = "0.1.0"
-
-[build-options]
-observabilityIncluded = true
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/main.bal
deleted file mode 100644
index a5751fe65..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v2.0/yaml/petstore.yaml" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/openapi.yaml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/openapi.yaml
deleted file mode 100644
index 84839c338..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_13/openapi.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-swagger: 2.0.0
-info:
-  title: Sample API
-  description: API description in Markdown.
-  version: 1.0.0
-host: petstore.swagger.io
-basePath: /v1
-paths:
-  /users/{id}:
-    get:
-      summary: Returns a list of users.
-      description: Optional extended description in Markdown.
-      responses:
-        '200':
-          description: OK
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/Ballerina.toml
deleted file mode 100644
index 005e00d52..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/Ballerina.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-org = "openapi_client_idl_test"
-name = "project_1"
-version = "0.1.0"
-
-[build-options]
-observabilityIncluded = true
-
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/main.bal
deleted file mode 100644
index e3a7412a7..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "openapi.json" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/openapi.json b/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/openapi.json
deleted file mode 100644
index 490c582fb..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_14/openapi.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
-  "openapi": "3.0.1",
-  "info": {
-    "title": " ",
-    "version": "1.0.0"
-  },
-  "servers": [
-    {
-      "url": "localhost:9090/"
-    }
-  ],
-  "paths": {
-    "/greeting": {
-      "get": {
-        "operationId": "operation1_get_/greeting",
-        "responses": {
-          "200": {
-            "description": "Ok",
-            "content": {
-              "text/plain": {
-                "schema": {
-                  "type": "string"
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-  },
-  "components": {}
-}
\ No newline at end of file
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/Ballerina.toml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/Ballerina.toml
deleted file mode 100644
index 005e00d52..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/Ballerina.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-org = "openapi_client_idl_test"
-name = "project_1"
-version = "0.1.0"
-
-[build-options]
-observabilityIncluded = true
-
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/main.bal b/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/main.bal
deleted file mode 100644
index 5a58da41f..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/main.bal
+++ /dev/null
@@ -1,5 +0,0 @@
-client "openapi.yaml" as bar;
-
-public function main() {
-    bar:client y;
-}
diff --git a/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/openapi.yaml b/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/openapi.yaml
deleted file mode 100644
index f5d6142f8..000000000
--- a/openapi-integration-tests/src/test/resources/client-idl-projects/project_15/openapi.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-openapi: 3.0.1
-info:
-  title: Sample API
-  description: API description in Markdown.
-  version: 1.0.0
-servers:
-  - url: 'https://api.example.com'
-paths:
-  /users:
-    get:
-      summary: Returns a list of users.
-      description: Optional extended description in Markdown.
-      responses:
-        '200':
-          description: OK
diff --git a/openapi-integration-tests/src/test/resources/testng.xml b/openapi-integration-tests/src/test/resources/testng.xml
index a04f3eb05..3dd6517ac 100644
--- a/openapi-integration-tests/src/test/resources/testng.xml
+++ b/openapi-integration-tests/src/test/resources/testng.xml
@@ -28,8 +28,8 @@
             
             
             
-
-
+            
+
         
     
 
diff --git a/settings.gradle b/settings.gradle
index 4f1f94d95..a8abf9ae6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -26,6 +26,7 @@ include(':openapi-cli')
 include(':openapi-ls-extension')
 include(':openapi-validator')
 include(':openapi-extension')
+include(':openapi-bal-task-plugin')
 include(':openapi-extension-tests')
 include(':openapi-integration-tests')
 //include(':openapi-tests')