diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java index a9fa6ba08..d50c7b5b9 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/Constants.java @@ -326,9 +326,18 @@ public String toString() { public static final String FALSE = "false"; public static final String SLASH = "/"; public static final String HYPHEN = "-"; + + //`@openapi:ServiceInfo` annotation constants public static final String CONTRACT = "contract"; public static final String VERSION = "version"; public static final String TITLE = "title"; + public static final String EMAIL = "email"; + public static final String DESCRIPTION = "description"; + public static final String CONTACT_NAME = "contactName"; + public static final String CONTACT_URL = "contactURL"; + public static final String LICENSE_NAME = "licenseName"; + public static final String LICENSE_URL = "licenseURL"; + public static final String TERMS_OF_SERVICE = "termsOfService"; public static final String OPENAPI_ANNOTATION = "openapi:ServiceInfo"; //File extensions diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/InfoMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/InfoMapper.java index 9a41c27a0..ef2b91e2f 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/InfoMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/InfoMapper.java @@ -39,7 +39,9 @@ import io.ballerina.openapi.service.mapper.utils.MapperCommonUtils; import io.ballerina.tools.diagnostics.Location; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; import java.io.File; import java.io.IOException; @@ -51,10 +53,17 @@ import java.util.Locale; import java.util.Optional; +import static io.ballerina.openapi.service.mapper.Constants.CONTACT_NAME; +import static io.ballerina.openapi.service.mapper.Constants.CONTACT_URL; import static io.ballerina.openapi.service.mapper.Constants.CONTRACT; +import static io.ballerina.openapi.service.mapper.Constants.DESCRIPTION; +import static io.ballerina.openapi.service.mapper.Constants.EMAIL; +import static io.ballerina.openapi.service.mapper.Constants.LICENSE_NAME; +import static io.ballerina.openapi.service.mapper.Constants.LICENSE_URL; import static io.ballerina.openapi.service.mapper.Constants.OPENAPI_ANNOTATION; import static io.ballerina.openapi.service.mapper.Constants.SLASH; import static io.ballerina.openapi.service.mapper.Constants.SPECIAL_CHAR_REGEX; +import static io.ballerina.openapi.service.mapper.Constants.TERMS_OF_SERVICE; import static io.ballerina.openapi.service.mapper.Constants.TITLE; import static io.ballerina.openapi.service.mapper.Constants.VERSION; @@ -202,31 +211,66 @@ private static String normalizeTitle(String title) { private static OASResult parseServiceInfoAnnotationAttachmentDetails(List diagnostics, AnnotationNode annotation, Path ballerinaFilePath) { + Location location = annotation.location(); OpenAPI openAPI = new OpenAPI(); - Optional content = annotation.annotValue(); - // If contract path there - if (content.isPresent()) { - SeparatedNodeList fields = content.get().fields(); - if (!fields.isEmpty()) { - OpenAPIInfo openAPIInfo = updateOpenAPIInfoModel(fields); - // If in case ballerina file path is getting null, then openAPI specification will be generated for - // given services. - if (openAPIInfo.getContractPath().isPresent() && ballerinaFilePath != null) { - return updateExistingContractOpenAPI(diagnostics, location, openAPIInfo, ballerinaFilePath); - } else if (openAPIInfo.getTitle().isPresent() && openAPIInfo.getVersion().isPresent()) { - openAPI.setInfo(new Info().version(openAPIInfo.getVersion().get()).title(normalizeTitle - (openAPIInfo.getTitle().get()))); - } else if (openAPIInfo.getVersion().isPresent()) { - openAPI.setInfo(new Info().version(openAPIInfo.getVersion().get())); - } else if (openAPIInfo.getTitle().isPresent()) { - openAPI.setInfo(new Info().title(normalizeTitle(openAPIInfo.getTitle().get()))); - } - } + Optional svcInfoAnnotationValue = annotation.annotValue(); + + if (svcInfoAnnotationValue.isEmpty()) { + return new OASResult(openAPI, diagnostics); + } + + SeparatedNodeList fields = svcInfoAnnotationValue.get().fields(); + if (fields.isEmpty()) { + return new OASResult(openAPI, diagnostics); + } + + OpenAPIInfo openAPIInfo = updateOpenAPIInfoModel(fields); + + // Check for contract path and existing Ballerina file path + if (openAPIInfo.getContractPath().isPresent() && ballerinaFilePath != null) { + return updateExistingContractOpenAPI(diagnostics, location, openAPIInfo, ballerinaFilePath); } + populateOASInfo(openAPI, openAPIInfo); return new OASResult(openAPI, diagnostics); } + private static void populateOASInfo(OpenAPI openAPI, OpenAPIInfo openAPIInfo) { + // Populate OpenAPI Info object + Info info = openAPI.getInfo() == null ? new Info() : openAPI.getInfo(); + openAPIInfo.getTitle().ifPresent(title -> info.setTitle(normalizeTitle(title))); + openAPIInfo.getVersion().ifPresent(info::setVersion); + openAPIInfo.getEmail().ifPresent(email -> { + Contact contact = (info.getContact() != null) ? info.getContact() : new Contact(); + contact.setEmail(email); + info.setContact(contact); + }); + openAPIInfo.getContactName().ifPresent(name -> { + Contact contact = (info.getContact() != null) ? info.getContact() : new Contact(); + contact.setName(name); + info.setContact(contact); + }); + openAPIInfo.getContactURL().ifPresent(url -> { + Contact contact = (info.getContact() != null) ? info.getContact() : new Contact(); + contact.setUrl(url); + info.setContact(contact); + }); + openAPIInfo.getLicenseURL().ifPresent(url -> { + License license = (info.getLicense() != null) ? info.getLicense() : new License(); + license.setUrl(url); + info.setLicense(license); + }); + openAPIInfo.getLicenseName().ifPresent(name -> { + License license = (info.getLicense() != null) ? info.getLicense() : new License(); + license.setName(name); + info.setLicense(license); + }); + openAPIInfo.getTermsOfService().ifPresent(info::setTermsOfService); + openAPIInfo.getDescription().ifPresent(info::setDescription); + openAPI.setInfo(info); + } + + private static OASResult updateExistingContractOpenAPI(List diagnostics, Location location, OpenAPIInfo openAPIInfo, Path ballerinaFilePath) { @@ -237,21 +281,9 @@ private static OASResult updateExistingContractOpenAPI(List fields) { @@ -264,19 +296,21 @@ private static OpenAPIInfo updateOpenAPIInfoModel(SeparatedNodeList infoBuilder.contractPath(fieldValue); + case TITLE -> infoBuilder.title(fieldValue); + case VERSION -> infoBuilder.version(fieldValue); + case EMAIL -> infoBuilder.email(fieldValue); + case DESCRIPTION -> infoBuilder.description(fieldValue); + case CONTACT_NAME -> infoBuilder.contactName(fieldValue); + case CONTACT_URL -> infoBuilder.contactURL(fieldValue); + case TERMS_OF_SERVICE -> infoBuilder.termsOfService(fieldValue); + case LICENSE_NAME -> infoBuilder.licenseName(fieldValue); + case LICENSE_URL -> infoBuilder.licenseURL(fieldValue); + default -> { } } } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/OpenAPIInfo.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/OpenAPIInfo.java index f74faaf8b..38a3e9936 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/OpenAPIInfo.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/model/OpenAPIInfo.java @@ -30,11 +30,25 @@ public class OpenAPIInfo { private final String title; private final String version; private final String contractPath; + private final String description; + private final String email; + private final String contactName; + private final String contactURL; + private final String termsOfService; + private final String licenseName; + private final String licenseURL; public OpenAPIInfo(OpenAPIInfoBuilder openAPIInfoBuilder) { this.title = openAPIInfoBuilder.title; this.version = openAPIInfoBuilder.version; this.contractPath = openAPIInfoBuilder.contractPath; + this.description = openAPIInfoBuilder.description; + this.email = openAPIInfoBuilder.email; + this.contactName = openAPIInfoBuilder.contactName; + this.contactURL = openAPIInfoBuilder.contactURL; + this.termsOfService = openAPIInfoBuilder.termsOfService; + this.licenseName = openAPIInfoBuilder.licenseName; + this.licenseURL = openAPIInfoBuilder.licenseURL; } public Optional getTitle() { @@ -49,6 +63,34 @@ public Optional getContractPath() { return Optional.ofNullable(this.contractPath); } + public Optional getDescription() { + return Optional.ofNullable(this.description); + } + + public Optional getEmail() { + return Optional.ofNullable(this.email); + } + + public Optional getContactName() { + return Optional.ofNullable(this.contactName); + } + + public Optional getContactURL() { + return Optional.ofNullable(this.contactURL); + } + + public Optional getTermsOfService() { + return Optional.ofNullable(this.termsOfService); + } + + public Optional getLicenseName() { + return Optional.ofNullable(this.licenseName); + } + + public Optional getLicenseURL() { + return Optional.ofNullable(this.licenseURL); + } + /** * This is the builder class for the {@link OpenAPIInfo}. */ @@ -56,6 +98,13 @@ public static class OpenAPIInfoBuilder { private String title; private String version; private String contractPath; + private String description; + private String email; + private String contactName; + private String contactURL; + private String termsOfService; + private String licenseName; + private String licenseURL; public OpenAPIInfoBuilder title(String title) { this.title = title; @@ -67,8 +116,44 @@ public OpenAPIInfoBuilder version(String version) { return this; } - public void contractPath(String contractPath) { + public OpenAPIInfoBuilder contractPath(String contractPath) { this.contractPath = contractPath; + return this; + } + + public OpenAPIInfoBuilder description(String description) { + this.description = description; + return this; + } + + public OpenAPIInfoBuilder email(String email) { + this.email = email; + return this; + } + + public OpenAPIInfoBuilder contactName(String contactName) { + this.contactName = contactName; + return this; + } + + public OpenAPIInfoBuilder contactURL(String contactURL) { + this.contactURL = contactURL; + return this; + } + + public OpenAPIInfoBuilder termsOfService(String termsOfService) { + this.termsOfService = termsOfService; + return this; + } + + public OpenAPIInfoBuilder licenseName(String licenseName) { + this.licenseName = licenseName; + return this; + } + + public OpenAPIInfoBuilder licenseURL(String licenseURL) { + this.licenseURL = licenseURL; + return this; } public OpenAPIInfo build() { diff --git a/module-ballerina-openapi/annotation.bal b/module-ballerina-openapi/annotation.bal index 9eeccc451..533128ce9 100644 --- a/module-ballerina-openapi/annotation.bal +++ b/module-ballerina-openapi/annotation.bal @@ -24,6 +24,13 @@ # + embed - Enable auto-inject of OpenAPI documentation to current service # + title - Title for generated OpenAPI contract # + version - Version for generated OpenAPI contract +# + description - A brief description of the API, outlining its purpose, features, and any other relevant details that help users understand what the API does and how to use it. +# + email - The email address to contact the API provider or support. +# + contactName - The full name of the person or organization responsible for the API. +# + contactURL - The URL to a web page with more information about the API, the provider, or support. +# + termOfService - The URL to the terms of service for the API. +# + licenseName - The name of the license under which the API is provided. +# + licenseURL - The URL to the full text of the license. public type ServiceInformation record {| string contract = ""; string[]? tags = []; @@ -34,6 +41,13 @@ public type ServiceInformation record {| boolean embed = false; string title?; string version?; + string description?; + string email?; + string contactName?; + string contactURL?; + string termsOfService?; + string licenseName?; + string licenseURL?; |}; // # Client configurations code. 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 10d675d6b..37d0e6006 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 @@ -146,6 +146,12 @@ public void nonOpenAPIAnnotationWithWithoutBasePath() throws IOException, Interr "project_non_openapi_annotation_without_base_path/result.yaml"); } + @Test(description = "Service is with openapi annotation include all oas infor section details") + public void openAPInForSectionTest() throws IOException, InterruptedException { + executeCommand("project_openapi_info/service_file.bal", "info_openapi.yaml", + "project_openapi_info/result.yaml"); + } + @AfterClass public void cleanUp() throws IOException { TestUtil.cleanDistribution(); diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_10/result.yaml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_10/result.yaml index 95ff62862..2fdcdf1d1 100644 --- a/openapi-integration-tests/src/test/resources/ballerina_sources/project_10/result.yaml +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_10/result.yaml @@ -1,6 +1,15 @@ openapi: 3.0.1 info: title: Mock File + description: API system description + termsOfService: http://mock-api-doc + contact: + name: sumudu + url: http://mock-api-contact + email: sumudu@abc.com + license: + name: ABC + url: http://abc.com version: 0.1.0 servers: - url: "{server}:{port}/titleBase" diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_10/service.bal b/openapi-integration-tests/src/test/resources/ballerina_sources/project_10/service.bal index 9a55f502a..d08b901a7 100644 --- a/openapi-integration-tests/src/test/resources/ballerina_sources/project_10/service.bal +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_10/service.bal @@ -2,7 +2,14 @@ import ballerina/http; import ballerina/openapi; @openapi:ServiceInfo { - title: "Mock file" + title: "Mock file", + description: "API system description", + email: "sumudu@abc.com", + contactName: "sumudu", + contactURL: "http://mock-api-contact", + termsOfService: "http://mock-api-doc", + licenseName: "ABC", + licenseURL: "http://abc.com" } service /titleBase on new http:Listener(9090) { resource function get title() returns string { diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/Ballerina.toml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/Ballerina.toml new file mode 100644 index 000000000..a50ec9b4b --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org= "ballerina" +name= "openapi_test" +version= "2.0.0" diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/result.yaml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/result.yaml new file mode 100644 index 000000000..d3c971c10 --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/result.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.1 +info: + title: Pet Store + description: API system description + termsOfService: http://mock-api-doc + contact: + name: sumudu + url: http://mock-api-contact + email: sumudu@abc.com + license: + name: ABC + url: http://abc.com + version: 1.0.0 +servers: + - url: "{server}:{port}/info" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /pet: + get: + operationId: getPet + responses: + "202": + description: Accepted diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/service_file.bal b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/service_file.bal new file mode 100644 index 000000000..c9c559111 --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_info/service_file.bal @@ -0,0 +1,19 @@ +import ballerina/http; +import ballerina/openapi; + +@openapi:ServiceInfo { + version: "1.0.0", + title: "Pet store", + description: "API system description", + email: "sumudu@abc.com", + contactName: "sumudu", + contactURL: "http://mock-api-contact", + termsOfService: "http://mock-api-doc", + licenseName: "ABC", + licenseURL: "http://abc.com" +} +service /info on new http:Listener(9090) { + resource function get pet() { + + } +}