diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ServiceToOpenAPIMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ServiceToOpenAPIMapper.java index 0c7d7c1da..4b0e7aaa7 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ServiceToOpenAPIMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/ServiceToOpenAPIMapper.java @@ -17,7 +17,9 @@ */ package io.ballerina.openapi.service.mapper; +import io.ballerina.compiler.api.ModuleID; import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.ModuleSymbol; import io.ballerina.compiler.api.symbols.ServiceDeclarationSymbol; import io.ballerina.compiler.api.symbols.Symbol; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; @@ -95,6 +97,24 @@ public static List generateOAS3Definition(Project project, SyntaxTree SemanticModel semanticModel, String serviceName, Boolean needJson, Path inputPath) { + return generateOAS3Definition(project, syntaxTree, semanticModel, serviceName, needJson, inputPath, false); + } + + /** + * This method will generate openapi definition Map lists with ballerina code. + * + * @param syntaxTree - Syntax tree the related to ballerina service + * @param semanticModel - Semantic model related to ballerina module + * @param serviceName - Service name that need to generate the openAPI specification + * @param needJson - Flag for enabling the generated file format with json or YAML + * @param inputPath - Input file path for resolve the annotation details + * @param ballerinaExtension - Flag to enable ballerina type extension + * @return - {@link java.util.Map} with openAPI definitions for service nodes + */ + public static List generateOAS3Definition(Project project, SyntaxTree syntaxTree, + SemanticModel semanticModel, + String serviceName, Boolean needJson, + Path inputPath, Boolean ballerinaExtension) { Map servicesToGenerate = new HashMap<>(); List availableService = new ArrayList<>(); List diagnostics = new ArrayList<>(); @@ -115,7 +135,7 @@ public static List generateOAS3Definition(Project project, SyntaxTree for (Map.Entry serviceNode : servicesToGenerate.entrySet()) { String openApiName = getOpenApiFileName(syntaxTree.filePath(), serviceNode.getKey(), needJson); OASResult oasDefinition = generateOasFroServiceNode(project, openApiName, - semanticModel, inputPath, serviceNode.getValue()); + semanticModel, inputPath, serviceNode.getValue(), ballerinaExtension); outputs.add(oasDefinition); } } @@ -127,13 +147,15 @@ public static List generateOAS3Definition(Project project, SyntaxTree } public static OASResult generateOasFroServiceNode(Project project, String openApiName, SemanticModel semanticModel, - Path inputPath, ServiceNode serviceNode) { + Path inputPath, ServiceNode serviceNode, + Boolean ballerinaExtension) { OASGenerationMetaInfo.OASGenerationMetaInfoBuilder builder = new OASGenerationMetaInfo.OASGenerationMetaInfoBuilder(); builder.setServiceNode(serviceNode) .setSemanticModel(semanticModel) .setOpenApiFileName(openApiName) .setBallerinaFilePath(inputPath) + .setBallerinaExtension(ballerinaExtension) .setProject(project); OASGenerationMetaInfo oasGenerationMetaInfo = builder.build(); OASResult oasDefinition = generateOAS(oasGenerationMetaInfo); @@ -275,7 +297,12 @@ public static OASResult generateOAS(OASGenerationMetaInfo oasGenerationMetaInfo) openapi.setComponents(null); } // Remove ballerina extensions - BallerinaTypeExtensioner.removeExtensions(openapi); + Optional serviceModuleId = getServiceModuleId(serviceDefinition, semanticModel); + if (oasGenerationMetaInfo.getBallerinaExtension() && serviceModuleId.isPresent()) { + BallerinaTypeExtensioner.removeCurrentModuleTypeExtensions(openapi, serviceModuleId.get()); + } else { + BallerinaTypeExtensioner.removeExtensions(openapi); + } return new OASResult(openapi, diagnostics); } else { return new OASResult(openapi, oasResult.getDiagnostics()); @@ -331,6 +358,11 @@ private static String extractBasePath(OpenAPI openApiFromServiceContract) { return servers.get(0).getUrl().split("\\{server}:\\{port}").length == 2 ? parts[1] : ""; } + private static Optional getServiceModuleId(ServiceNode serviceNode, SemanticModel semanticModel) { + return semanticModel.symbol(serviceNode.getInternalNode()) + .flatMap(symbol -> symbol.getModule().map(ModuleSymbol::id)); + } + /** * Travers every syntax tree and collect all the listener nodes. * diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/example/OpenAPIExampleMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/example/OpenAPIExampleMapperImpl.java index 1174c4be2..df23fc98c 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/example/OpenAPIExampleMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/example/OpenAPIExampleMapperImpl.java @@ -149,7 +149,7 @@ private Optional getTypeDefinitionNode(String name, Schema Optional ballerinaType; if (ballerinaExt.isPresent()) { BallerinaPackage ballerinaPkg = ballerinaExt.get(); - String typeName = ballerinaPkg.name().orElse(name); + String typeName = Objects.isNull(ballerinaPkg.name()) ? name : ballerinaPkg.name(); ballerinaType = semanticModel.types().getTypeByName(ballerinaPkg.orgName(), ballerinaPkg.moduleName(), ballerinaPkg.version(), typeName); } else { diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/OASGenerationMetaInfo.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/OASGenerationMetaInfo.java index b1e679d28..d6705d471 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/OASGenerationMetaInfo.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/OASGenerationMetaInfo.java @@ -36,6 +36,7 @@ public class OASGenerationMetaInfo { private final SemanticModel semanticModel; private final ServiceNode serviceNode; private final Project project; + private final Boolean ballerinaExtensionLevel; public OASGenerationMetaInfo(OASGenerationMetaInfoBuilder builder) { this.openApiFileName = builder.openApiFileName; @@ -47,6 +48,7 @@ public OASGenerationMetaInfo(OASGenerationMetaInfoBuilder builder) { } this.serviceNode = serviceNodeFromBuilder; this.project = builder.project; + this.ballerinaExtensionLevel = builder.ballerinaExtension; } public String getOpenApiFileName() { @@ -69,6 +71,10 @@ public Project getProject() { return project; } + public Boolean getBallerinaExtension() { + return ballerinaExtensionLevel; + } + /** * This method is used to create a new {@link OASGenerationMetaInfoBuilder} instance. */ @@ -80,6 +86,7 @@ public static class OASGenerationMetaInfoBuilder { private ServiceDeclarationNode serviceDeclarationNode; private ServiceNode serviceNode; private Project project; + private Boolean ballerinaExtension = false; public OASGenerationMetaInfoBuilder setBallerinaFilePath(Path ballerinaFilePath) { this.ballerinaFilePath = ballerinaFilePath; @@ -111,6 +118,11 @@ public OASGenerationMetaInfoBuilder setOpenApiFileName(String openApiFileName) { return this; } + public OASGenerationMetaInfoBuilder setBallerinaExtension(Boolean balExt) { + this.ballerinaExtension = balExt; + return this; + } + public void setProject(Project project) { this.project = project; } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/ServiceDeclaration.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/ServiceDeclaration.java index 9d6606f9c..632f01167 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/ServiceDeclaration.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/ServiceDeclaration.java @@ -236,7 +236,7 @@ private Optional getOpenAPISpecFromResources(Package pkg, SemanticModel return Optional.empty(); } OASResult oasResult = generateOasFroServiceNode(pkg.project(), serviceName.get(), semanticModel, null, - serviceContract.get()); + serviceContract.get(), false); if (oasResult.getDiagnostics().stream() .anyMatch(diagnostic -> diagnostic.getDiagnosticSeverity().equals(DiagnosticSeverity.ERROR))) { diagnostics.add(new ExceptionDiagnostic(DiagnosticMessages.OAS_CONVERTOR_136, serviceName.get())); diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/extension/BallerinaPackage.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/extension/BallerinaPackage.java index 6edbfffce..484bcb9e3 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/extension/BallerinaPackage.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/extension/BallerinaPackage.java @@ -17,8 +17,6 @@ */ package io.ballerina.openapi.service.mapper.type.extension; -import java.util.Optional; - /** * This {@link BallerinaPackage} record represents the Ballerina package details. * @param orgName organization name @@ -30,5 +28,5 @@ * @since 2.1.0 */ public record BallerinaPackage(String orgName, String pkgName, String moduleName, String version, - String modulePrefix, Optional name) { + String modulePrefix, String name) { } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/extension/BallerinaTypeExtensioner.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/extension/BallerinaTypeExtensioner.java index 07ad658d5..a4f94a2cf 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/extension/BallerinaTypeExtensioner.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/extension/BallerinaTypeExtensioner.java @@ -46,6 +46,18 @@ public static void addExtension(Schema schema, TypeSymbol typeSymbol) { } public static void removeExtensions(OpenAPI openAPI) { + removeExtensionsFromSchemas(openAPI, (extensions, orgName, moduleName) -> true); + } + + public static void removeCurrentModuleTypeExtensions(OpenAPI openAPI, ModuleID moduleID) { + String orgName = moduleID.orgName(); + String moduleName = moduleID.moduleName(); + + removeExtensionsFromSchemas(openAPI, (extensions, org, mod) -> fromSameModule(extensions, orgName, + moduleName)); + } + + private static void removeExtensionsFromSchemas(OpenAPI openAPI, ExtensionRemovalCondition condition) { Components components = openAPI.getComponents(); if (Objects.isNull(components)) { return; @@ -58,12 +70,22 @@ public static void removeExtensions(OpenAPI openAPI) { schemas.forEach((key, schema) -> { Map extensions = schema.getExtensions(); - if (Objects.nonNull(extensions)) { + if (Objects.nonNull(extensions) && condition.shouldRemove(extensions, null, null)) { extensions.remove(X_BALLERINA_TYPE); } }); } + @FunctionalInterface + private interface ExtensionRemovalCondition { + boolean shouldRemove(Map extensions, String orgName, String moduleName); + } + + private static boolean fromSameModule(Map extensions, String orgName, String moduleName) { + return extensions.get(X_BALLERINA_TYPE) instanceof BallerinaPackage ballerinaPkg && + orgName.equals(ballerinaPkg.orgName()) && moduleName.equals(ballerinaPkg.moduleName()); + } + public static Optional getExtension(Schema schema) { Map extensions = schema.getExtensions(); if (Objects.isNull(extensions)) { @@ -84,6 +106,6 @@ static Optional getBallerinaPackage(TypeSymbol typeSymbol) { } ModuleID moduleID = module.get().id(); return Optional.of(new BallerinaPackage(moduleID.orgName(), moduleID.packageName(), moduleID.moduleName(), - moduleID.version(), moduleID.modulePrefix(), typeSymbol.getName())); + moduleID.version(), moduleID.modulePrefix(), typeSymbol.getName().orElse(null))); } } 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 index 83038b8a0..48aeb4807 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/BaseCmd.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/BaseCmd.java @@ -54,8 +54,7 @@ public class BaseCmd { @CommandLine.Option(names = {"--status-code-binding"}, description = "Generate the client methods with " + "status code response binding") public boolean statusCodeBinding; - - + @CommandLine.Option(names = {"--mock"}, hidden = true, description = "Generate mock client with given response example") public boolean mock; diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OASContractGenerator.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OASContractGenerator.java index ffd43ef52..8381ac80a 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OASContractGenerator.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OASContractGenerator.java @@ -43,6 +43,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Optional; import static io.ballerina.openapi.service.mapper.utils.CodegenUtils.resolveContractFileName; @@ -59,6 +60,7 @@ public class OASContractGenerator { private Project project; private List diagnostics = new ArrayList<>(); private PrintStream outStream = System.out; + private Boolean ballerinaExtension = false; /** * Initialize constructor. @@ -67,6 +69,12 @@ public OASContractGenerator() { } + public void setBallerinaExtension(Boolean ballerinaExtension) { + if (Objects.nonNull(ballerinaExtension)) { + this.ballerinaExtension = ballerinaExtension; + } + } + public List getDiagnostics() { return diagnostics; } @@ -118,7 +126,7 @@ public void generateOAS3DefinitionsAllService(Path servicePath, Path outPath, St } semanticModel = compilation.getSemanticModel(docId.moduleId()); List openAPIDefinitions = ServiceToOpenAPIMapper.generateOAS3Definition(project, syntaxTree, - semanticModel, serviceName, needJson, inputPath); + semanticModel, serviceName, needJson, inputPath, ballerinaExtension); if (!openAPIDefinitions.isEmpty()) { List fileNames = new ArrayList<>(); 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 2403383b2..07af694c5 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 @@ -126,6 +126,9 @@ public class OpenApiCmd implements BLauncherCmd { description = "Generate service without data binding") private boolean generateWithoutDataBinding; + @CommandLine.Option(names = {"--with-bal-ext"}, hidden = true, description = "Generate ballerina type extensions") + private boolean addBallerinaExtension; + @CommandLine.Parameters private List argList; @@ -223,7 +226,7 @@ public void execute() { if (baseCmd.statusCodeBinding) { if (baseCmd.mode != null && baseCmd.mode.equals(SERVICE)) { - outStream.println("ERROR: the '--status-code-binding' option is only available in client " + + outStream.println("the '--status-code-binding' option is only available in client " + "generation mode."); exitError(this.exitWhenFinish); } @@ -238,6 +241,12 @@ public void execute() { } } + if (addBallerinaExtension) { + outStream.println("'--with-bal-ext' option is only available in OpenAPI specification " + + "generation mode."); + exitError(this.exitWhenFinish); + } + try { openApiToBallerina(fileName, filter); } catch (IOException e) { @@ -289,6 +298,7 @@ private void ballerinaToOpenApi(String fileName) { getTargetOutputPath(); // Check service name it is mandatory OASContractGenerator openApiConverter = new OASContractGenerator(); + openApiConverter.setBallerinaExtension(addBallerinaExtension); openApiConverter.generateOAS3DefinitionsAllService(balFilePath, targetOutputPath, service, generatedFileType); mapperDiagnostics.addAll(openApiConverter.getDiagnostics()); diff --git a/openapi-client-native/ballerina-tests/Dependencies.toml b/openapi-client-native/ballerina-tests/Dependencies.toml index 8a0495924..6579eb387 100644 --- a/openapi-client-native/ballerina-tests/Dependencies.toml +++ b/openapi-client-native/ballerina-tests/Dependencies.toml @@ -75,7 +75,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.11.2" +version = "2.11.1" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, diff --git a/openapi-integration-tests/src/test/java/io/ballerina/openapi/cmd/BallerinaToOpenAPITests.java b/openapi-integration-tests/src/test/java/io/ballerina/openapi/cmd/BallerinaToOpenAPITests.java index 4c0cde27a..83082d548 100644 --- a/openapi-integration-tests/src/test/java/io/ballerina/openapi/cmd/BallerinaToOpenAPITests.java +++ b/openapi-integration-tests/src/test/java/io/ballerina/openapi/cmd/BallerinaToOpenAPITests.java @@ -44,7 +44,7 @@ */ public class BallerinaToOpenAPITests extends OpenAPITest { public static final String DISTRIBUTION_FILE_NAME = DISTRIBUTIONS_DIR.toString(); - public static final Path TEST_RESOURCE = Paths.get(RESOURCES_PATH.toString() + "/ballerina_sources"); + public static final Path TEST_RESOURCE = Paths.get(RESOURCES_PATH + "/ballerina_sources"); @BeforeClass public void setupDistributions() throws IOException { @@ -92,7 +92,7 @@ public void multipleService() throws IOException, InterruptedException { List buildArgs = new LinkedList<>(); buildArgs.add("-i"); buildArgs.add("project_7/service.bal"); - boolean successful = TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs); + Assert.assertTrue(TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs)); Assert.assertTrue(Files.exists(TEST_RESOURCE.resolve("mTitle_openapi.yaml"))); } @@ -101,7 +101,7 @@ public void multipleServiceWithAnnotation() throws IOException, InterruptedExcep List buildArgs = new LinkedList<>(); buildArgs.add("-i"); buildArgs.add("project_8/service.bal"); - boolean successful = TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs); + Assert.assertTrue(TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs)); Assert.assertTrue(Files.exists(TEST_RESOURCE.resolve("mTitle01_openapi.yaml"))); Assert.assertTrue(Files.exists(TEST_RESOURCE.resolve("mVersion_openapi.yaml"))); } @@ -123,7 +123,7 @@ public void nonHttpService() throws IOException, InterruptedException { List buildArgs = new LinkedList<>(); buildArgs.add("-i"); buildArgs.add("project_11/service.bal"); - boolean successful = TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs); + TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs); Assert.assertFalse(Files.exists(TEST_RESOURCE.resolve("query_openapi.yaml"))); } @@ -154,7 +154,7 @@ public void exampleMapping() throws IOException, InterruptedException { } //TODO enable after resolving dependency issue - @Test(description = "Service is with openapi annotation include all oas infor section details", enabled = false) + @Test(description = "Service is with openapi annotation include all oas info section details", enabled = false) public void openAPInForSectionTest() throws IOException, InterruptedException { executeCommand("project_openapi_info/service_file.bal", "info_openapi.yaml", "project_openapi_info/result.yaml"); @@ -166,6 +166,18 @@ public void openAPIExampleAnnotations() throws IOException, InterruptedException "project_openapi_examples/result.yaml"); } + @Test(description = "Generate without ballerina extension option") + public void openAPIGenWithoutBalExt() throws IOException, InterruptedException { + executeCommand("project_openapi_bal_ext/main.bal", "api_v1_openapi.yaml", + "project_openapi_bal_ext/result_0.yaml"); + } + + @Test(description = "Generate with ballerina extension option") + public void openAPIGenWithBalExt() throws IOException, InterruptedException { + executeCommand("project_openapi_bal_ext/main.bal", "api_v1_openapi.yaml", + "project_openapi_bal_ext/result_1.yaml", true); + } + @AfterClass public void cleanUp() throws IOException { TestUtil.cleanDistribution(); @@ -183,7 +195,21 @@ private void executeCommand(String resourcePath, String generatedFile, String ex List buildArgs = new LinkedList<>(); buildArgs.add("-i"); buildArgs.add(resourcePath); - boolean successful = TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs); + Assert.assertTrue(TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs)); + Assert.assertTrue(Files.exists(TEST_RESOURCE.resolve(generatedFile))); + String generatedOpenAPI = getStringFromGivenBalFile(TEST_RESOURCE.resolve(generatedFile)); + String expectedYaml = getStringFromGivenBalFile(TEST_RESOURCE.resolve(expectedPath)); + Assert.assertEquals(expectedYaml, generatedOpenAPI); + } + + private void executeCommand(String resourcePath, String generatedFile, String expectedPath, boolean balExt) + throws IOException, InterruptedException { + List buildArgs = new LinkedList<>(); + buildArgs.add("-i"); + buildArgs.add(resourcePath); + buildArgs.add("--with-bal-ext"); + buildArgs.add(String.valueOf(balExt)); + Assert.assertTrue(TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs)); Assert.assertTrue(Files.exists(TEST_RESOURCE.resolve(generatedFile))); String generatedOpenAPI = getStringFromGivenBalFile(TEST_RESOURCE.resolve(generatedFile)); String expectedYaml = getStringFromGivenBalFile(TEST_RESOURCE.resolve(expectedPath)); diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/Ballerina.toml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/Ballerina.toml new file mode 100644 index 000000000..8d198de49 --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "ballerina" +name = "openapi_bal_ext" +version = "0.1.0" diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/main.bal b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/main.bal new file mode 100644 index 000000000..d8e584c4f --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/main.bal @@ -0,0 +1,32 @@ +import openapi_bal_ext.module as mod; + +import ballerina/http; +import ballerina/time; + +type Subject record {| + string name; + int credits; + time:Date examDate; +|}; + +public type Student record {| + *mod:BasicStudent; + Subject[] subjects; +|}; + +service /api/v1 on new http:Listener(9090) { + + resource function get students() returns Student[] { + return [ + { + name: "John", + age: 25, + subjects: [ + {name: "Math", credits: 4, examDate: {day: 10, month: 10, year: 2023}}, + {name: "Science", credits: 3, examDate: {day: 15, month: 10, year: 2023}} + ] + } + ]; + } +} + diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/modules/module/module.bal b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/modules/module/module.bal new file mode 100644 index 000000000..a3aa66ead --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/modules/module/module.bal @@ -0,0 +1,4 @@ +public type BasicStudent record {| + string name; + int age; +|}; diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_0.yaml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_0.yaml new file mode 100644 index 000000000..888e567c3 --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_0.yaml @@ -0,0 +1,128 @@ +openapi: 3.0.1 +info: + title: Api V1 + version: 0.1.0 +servers: + - url: "{server}:{port}/api/v1" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /students: + get: + operationId: getStudents + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Student' +components: + schemas: + BasicStudent: + required: + - age + - name + type: object + properties: + name: + type: string + age: + type: integer + format: int64 + additionalProperties: false + Date: + type: object + description: Date in proleptic Gregorian calendar. + allOf: + - $ref: '#/components/schemas/DateFields' + - $ref: '#/components/schemas/OptionalTimeOfDayFields' + - type: object + properties: + utcOffset: + $ref: '#/components/schemas/ZoneOffset' + DateFields: + required: + - day + - month + - year + type: object + properties: + year: + type: integer + format: int64 + month: + type: integer + format: int64 + day: + type: integer + format: int64 + description: Fields of the Date record. + OptionalTimeOfDayFields: + type: object + properties: + hour: + type: integer + format: int64 + minute: + type: integer + format: int64 + second: + $ref: '#/components/schemas/Seconds' + description: TimeOfDay with all the fields beign optional. + Seconds: + type: number + description: Holds the seconds as a decimal value. + format: double + Student: + type: object + allOf: + - $ref: '#/components/schemas/BasicStudent' + - required: + - subjects + type: object + properties: + subjects: + type: array + items: + $ref: '#/components/schemas/Subject' + additionalProperties: false + Subject: + required: + - credits + - examDate + - name + type: object + properties: + name: + type: string + credits: + type: integer + format: int64 + examDate: + $ref: '#/components/schemas/Date' + additionalProperties: false + ZoneOffset: + required: + - hours + type: object + properties: + hours: + type: integer + format: int64 + minutes: + type: integer + format: int64 + seconds: + type: number + description: |- + IETF zone files have historical zones that are offset by + integer seconds; we use Seconds type so that this is a subtype + of Delta + format: double + additionalProperties: false diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_1.yaml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_1.yaml new file mode 100644 index 000000000..0dafce369 --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_1.yaml @@ -0,0 +1,170 @@ +openapi: 3.0.1 +info: + title: Api V1 + version: 0.1.0 +servers: + - url: "{server}:{port}/api/v1" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /students: + get: + operationId: getStudents + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Student' +components: + schemas: + BasicStudent: + required: + - age + - name + type: object + properties: + name: + type: string + age: + type: integer + format: int64 + additionalProperties: false + x-ballerina-type: + orgName: ballerina + pkgName: openapi_bal_ext + moduleName: openapi_bal_ext.module + version: 0.1.0 + modulePrefix: module + name: BasicStudent + Date: + type: object + description: Date in proleptic Gregorian calendar. + allOf: + - $ref: '#/components/schemas/DateFields' + - $ref: '#/components/schemas/OptionalTimeOfDayFields' + - type: object + properties: + utcOffset: + $ref: '#/components/schemas/ZoneOffset' + x-ballerina-type: + orgName: ballerina + pkgName: time + moduleName: time + version: 2.4.0 + modulePrefix: time + name: Date + DateFields: + required: + - day + - month + - year + type: object + properties: + year: + type: integer + format: int64 + month: + type: integer + format: int64 + day: + type: integer + format: int64 + description: Fields of the Date record. + x-ballerina-type: + orgName: ballerina + pkgName: time + moduleName: time + version: 2.4.0 + modulePrefix: time + name: DateFields + OptionalTimeOfDayFields: + type: object + properties: + hour: + type: integer + format: int64 + minute: + type: integer + format: int64 + second: + $ref: '#/components/schemas/Seconds' + description: TimeOfDay with all the fields beign optional. + x-ballerina-type: + orgName: ballerina + pkgName: time + moduleName: time + version: 2.4.0 + modulePrefix: time + name: OptionalTimeOfDayFields + Seconds: + type: number + description: Holds the seconds as a decimal value. + format: double + x-ballerina-type: + orgName: ballerina + pkgName: time + moduleName: time + version: 2.4.0 + modulePrefix: time + name: Seconds + Student: + type: object + allOf: + - $ref: '#/components/schemas/BasicStudent' + - required: + - subjects + type: object + properties: + subjects: + type: array + items: + $ref: '#/components/schemas/Subject' + additionalProperties: false + Subject: + required: + - credits + - examDate + - name + type: object + properties: + name: + type: string + credits: + type: integer + format: int64 + examDate: + $ref: '#/components/schemas/Date' + additionalProperties: false + ZoneOffset: + required: + - hours + type: object + properties: + hours: + type: integer + format: int64 + minutes: + type: integer + format: int64 + seconds: + type: number + description: |- + IETF zone files have historical zones that are offset by + integer seconds; we use Seconds type so that this is a subtype + of Delta + format: double + additionalProperties: false + x-ballerina-type: + orgName: ballerina + pkgName: time + moduleName: time + version: 2.4.0 + modulePrefix: time + name: ZoneOffset