diff --git a/.github/workflows/build-with-bal-test-graalvm.yml b/.github/workflows/build-with-bal-test-graalvm.yml index 50746f4848..425ab94e8d 100644 --- a/.github/workflows/build-with-bal-test-graalvm.yml +++ b/.github/workflows/build-with-bal-test-graalvm.yml @@ -27,6 +27,8 @@ on: branches: - master - 2201.7.x + - 2201.8.x + - 2201.9.x types: [opened, synchronize, reopened, labeled, unlabeled] concurrency: diff --git a/ballerina-tests/http-advanced-tests/Ballerina.toml b/ballerina-tests/http-advanced-tests/Ballerina.toml index d30b81a7be..b06855a8a0 100644 --- a/ballerina-tests/http-advanced-tests/Ballerina.toml +++ b/ballerina-tests/http-advanced-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http-advanced-tests/Dependencies.toml b/ballerina-tests/http-advanced-tests/Dependencies.toml index c05fe4a76a..1873bc5553 100644 --- a/ballerina-tests/http-advanced-tests/Dependencies.toml +++ b/ballerina-tests/http-advanced-tests/Dependencies.toml @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/http-client-tests/Ballerina.toml b/ballerina-tests/http-client-tests/Ballerina.toml index e9e28a2301..e6d40fe481 100644 --- a/ballerina-tests/http-client-tests/Ballerina.toml +++ b/ballerina-tests/http-client-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http-client-tests/Dependencies.toml b/ballerina-tests/http-client-tests/Dependencies.toml index d5562e4b98..c2402bb721 100644 --- a/ballerina-tests/http-client-tests/Dependencies.toml +++ b/ballerina-tests/http-client-tests/Dependencies.toml @@ -47,7 +47,7 @@ modules = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/http-dispatching-tests/Ballerina.toml b/ballerina-tests/http-dispatching-tests/Ballerina.toml index 72d8bd01bb..ad3aaaf735 100644 --- a/ballerina-tests/http-dispatching-tests/Ballerina.toml +++ b/ballerina-tests/http-dispatching-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http-dispatching-tests/Dependencies.toml b/ballerina-tests/http-dispatching-tests/Dependencies.toml index ba8f1775c4..e13338aea4 100644 --- a/ballerina-tests/http-dispatching-tests/Dependencies.toml +++ b/ballerina-tests/http-dispatching-tests/Dependencies.toml @@ -47,7 +47,7 @@ modules = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/http-interceptor-tests/Ballerina.toml b/ballerina-tests/http-interceptor-tests/Ballerina.toml index 35a3d18626..7af1a5efc8 100644 --- a/ballerina-tests/http-interceptor-tests/Ballerina.toml +++ b/ballerina-tests/http-interceptor-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http-interceptor-tests/Dependencies.toml b/ballerina-tests/http-interceptor-tests/Dependencies.toml index 10a5f703b0..c1df476a08 100644 --- a/ballerina-tests/http-interceptor-tests/Dependencies.toml +++ b/ballerina-tests/http-interceptor-tests/Dependencies.toml @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/http-misc-tests/Ballerina.toml b/ballerina-tests/http-misc-tests/Ballerina.toml index 207cc0e9d2..5c793689bb 100644 --- a/ballerina-tests/http-misc-tests/Ballerina.toml +++ b/ballerina-tests/http-misc-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http-misc-tests/Dependencies.toml b/ballerina-tests/http-misc-tests/Dependencies.toml index 22cd357ec5..109c032793 100644 --- a/ballerina-tests/http-misc-tests/Dependencies.toml +++ b/ballerina-tests/http-misc-tests/Dependencies.toml @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/http-misc-tests/tests/http_introspection_resource_test.bal b/ballerina-tests/http-misc-tests/tests/http_introspection_resource_test.bal index 8a3bc6e9dc..a7fb25b9fb 100755 --- a/ballerina-tests/http-misc-tests/tests/http_introspection_resource_test.bal +++ b/ballerina-tests/http-misc-tests/tests/http_introspection_resource_test.bal @@ -80,12 +80,14 @@ function testIntrospectionResourceLink() returns error? { http:Response response = check httpIntroResTestClient->options("/hello"); test:assertEquals(response.statusCode, 204, msg = "Found unexpected statusCode"); test:assertEquals(check response.getHeader(common:ALLOW), "GET, OPTIONS", msg = "Found unexpected Header"); - common:assertHeaderValue(check response.getHeader(common:LINK), ";rel=\"service-desc\""); + common:assertHeaderValue(check response.getHeader(common:LINK), + ";rel=\"service-desc\", ;rel=\"swagger-ui\""); response = check httpIntroResTestClient->options("/hello/greeting"); test:assertEquals(response.statusCode, 204, msg = "Found unexpected statusCode"); test:assertEquals(check response.getHeader(common:ALLOW), "GET, OPTIONS", msg = "Found unexpected Header"); - common:assertHeaderValue(check response.getHeader(common:LINK), ";rel=\"service-desc\""); + common:assertHeaderValue(check response.getHeader(common:LINK), + ";rel=\"service-desc\", ;rel=\"swagger-ui\""); } @test:Config {} diff --git a/ballerina-tests/http-resiliency-tests/Ballerina.toml b/ballerina-tests/http-resiliency-tests/Ballerina.toml index e9e2e9fd5d..7966a101c7 100644 --- a/ballerina-tests/http-resiliency-tests/Ballerina.toml +++ b/ballerina-tests/http-resiliency-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http-resiliency-tests/Dependencies.toml b/ballerina-tests/http-resiliency-tests/Dependencies.toml index e0f7cb7f48..05b3f972b4 100644 --- a/ballerina-tests/http-resiliency-tests/Dependencies.toml +++ b/ballerina-tests/http-resiliency-tests/Dependencies.toml @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/http-security-tests/Ballerina.toml b/ballerina-tests/http-security-tests/Ballerina.toml index f465ccf431..dd9b0808b9 100644 --- a/ballerina-tests/http-security-tests/Ballerina.toml +++ b/ballerina-tests/http-security-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http-security-tests/Dependencies.toml b/ballerina-tests/http-security-tests/Dependencies.toml index 7b439cd343..44fcaf2354 100644 --- a/ballerina-tests/http-security-tests/Dependencies.toml +++ b/ballerina-tests/http-security-tests/Dependencies.toml @@ -47,7 +47,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/http-service-tests/Ballerina.toml b/ballerina-tests/http-service-tests/Ballerina.toml index 0d65071be0..d791a5dacf 100644 --- a/ballerina-tests/http-service-tests/Ballerina.toml +++ b/ballerina-tests/http-service-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http-service-tests/Dependencies.toml b/ballerina-tests/http-service-tests/Dependencies.toml index 201aa6b1c7..be03532b03 100644 --- a/ballerina-tests/http-service-tests/Dependencies.toml +++ b/ballerina-tests/http-service-tests/Dependencies.toml @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/http2-tests/Ballerina.toml b/ballerina-tests/http2-tests/Ballerina.toml index 79f8a8b379..3b1a939249 100644 --- a/ballerina-tests/http2-tests/Ballerina.toml +++ b/ballerina-tests/http2-tests/Ballerina.toml @@ -14,4 +14,4 @@ graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.11.2-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.2.jar" diff --git a/ballerina-tests/http2-tests/Dependencies.toml b/ballerina-tests/http2-tests/Dependencies.toml index 19fad42422..54f4bd3878 100644 --- a/ballerina-tests/http2-tests/Dependencies.toml +++ b/ballerina-tests/http2-tests/Dependencies.toml @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.7.1" +version = "2.7.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index c31f962c74..72331b2559 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -17,7 +17,7 @@ graalvmCompatible = true groupId = "io.ballerina.stdlib" artifactId = "http-native" version = "2.11.2" -path = "../native/build/libs/http-native-2.11.2-SNAPSHOT.jar" +path = "../native/build/libs/http-native-2.11.2.jar" [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 3cbad54670..d766246cdc 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "http-compiler-plugin" class = "io.ballerina.stdlib.http.compiler.HttpCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/http-compiler-plugin-2.11.2-SNAPSHOT.jar" +path = "../compiler-plugin/build/libs/http-compiler-plugin-2.11.2.jar" diff --git a/changelog.md b/changelog.md index 73dd811a5e..748357226c 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [Introduce default status code response record](https://github.com/ballerina-platform/ballerina-library/issues/6491) +## [2.11.2] - 2024-06-14 + +### Added + +- [Generate and host SwaggerUI for the generated OpenAPI specification as a built-in resource](https://github.com/ballerina-platform/ballerina-library/issues/6622) + +### Fixed + +- [Remove the resource level annotation restrictions](https://github.com/ballerina-platform/ballerina-library/issues/5831) + ## [2.11.1] - 2024-05-29 ### Fixed diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/CompilerPluginTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/CompilerPluginTest.java index 3d6fc2edda..e2f57d478f 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/CompilerPluginTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/CompilerPluginTest.java @@ -133,16 +133,6 @@ public void testInValidReturnTypes() { "'anydata|http:Response|http:StatusCodeResponse|error', but found 'readonly & error[]'", HTTP_102); } - @Test - public void testInValidAnnotations() { - Package currentPackage = loadPackage("sample_package_3"); - PackageCompilation compilation = currentPackage.getCompilation(); - DiagnosticResult diagnosticResult = compilation.diagnosticResult(); - Assert.assertEquals(diagnosticResult.errorCount(), 1); - assertError(diagnosticResult, 0, "invalid resource method annotation type: expected 'http:ResourceConfig', " + - "but found 'display '", CompilerPluginTestConstants.HTTP_103); - } - @Test public void testInValidInputPayloadArgs() { Package currentPackage = loadPackage("sample_package_4"); @@ -410,22 +400,21 @@ public void testResourceErrorPositions() { Package currentPackage = loadPackage("sample_package_15"); PackageCompilation compilation = currentPackage.getCompilation(); DiagnosticResult diagnosticResult = compilation.diagnosticResult(); - Assert.assertEquals(diagnosticResult.errorCount(), 14); + Assert.assertEquals(diagnosticResult.errorCount(), 13); // only testing the error locations assertErrorPosition(diagnosticResult, 0, "(29:44,29:60)"); - assertErrorPosition(diagnosticResult, 1, "(34:5,34:12)"); - assertErrorPosition(diagnosticResult, 2, "(42:86,42:87)"); - assertErrorPosition(diagnosticResult, 3, "(46:57,46:60)"); - assertErrorPosition(diagnosticResult, 4, "(50:63,50:66)"); - assertErrorPosition(diagnosticResult, 5, "(54:66,54:69)"); - assertErrorPosition(diagnosticResult, 6, "(58:77,58:80)"); - assertErrorPosition(diagnosticResult, 7, "(62:76,62:79)"); - assertErrorPosition(diagnosticResult, 8, "(66:76,66:82)"); - assertErrorPosition(diagnosticResult, 9, "(73:45,73:46)"); - assertErrorPosition(diagnosticResult, 10, "(81:43,81:46)"); - assertErrorPosition(diagnosticResult, 11, "(81:61,81:64)"); - assertErrorPosition(diagnosticResult, 12, "(81:79,81:82)"); - assertErrorPosition(diagnosticResult, 13, "(85:77,85:93)"); + assertErrorPosition(diagnosticResult, 1, "(42:86,42:87)"); + assertErrorPosition(diagnosticResult, 2, "(46:57,46:60)"); + assertErrorPosition(diagnosticResult, 3, "(50:63,50:66)"); + assertErrorPosition(diagnosticResult, 4, "(54:66,54:69)"); + assertErrorPosition(diagnosticResult, 5, "(58:77,58:80)"); + assertErrorPosition(diagnosticResult, 6, "(62:76,62:79)"); + assertErrorPosition(diagnosticResult, 7, "(66:76,66:82)"); + assertErrorPosition(diagnosticResult, 8, "(73:45,73:46)"); + assertErrorPosition(diagnosticResult, 9, "(81:43,81:46)"); + assertErrorPosition(diagnosticResult, 10, "(81:61,81:64)"); + assertErrorPosition(diagnosticResult, 11, "(81:79,81:82)"); + assertErrorPosition(diagnosticResult, 12, "(85:77,85:93)"); } @Test diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/CompilerPluginTestConstants.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/CompilerPluginTestConstants.java index 6eb81f72d8..555781b659 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/CompilerPluginTestConstants.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/CompilerPluginTestConstants.java @@ -26,7 +26,6 @@ private CompilerPluginTestConstants() {} public static final String HTTP_101 = "HTTP_101"; public static final String HTTP_102 = "HTTP_102"; - public static final String HTTP_103 = "HTTP_103"; public static final String HTTP_104 = "HTTP_104"; public static final String HTTP_105 = "HTTP_105"; public static final String HTTP_106 = "HTTP_106"; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java index 25f39a65c7..673fecf3bd 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java @@ -22,7 +22,6 @@ import static io.ballerina.stdlib.http.compiler.Constants.ALLOWED_INTERCEPTOR_RETURN_UNION; import static io.ballerina.stdlib.http.compiler.Constants.ALLOWED_RETURN_UNION; -import static io.ballerina.stdlib.http.compiler.Constants.RESOURCE_CONFIG_ANNOTATION; import static io.ballerina.tools.diagnostics.DiagnosticSeverity.ERROR; import static io.ballerina.tools.diagnostics.DiagnosticSeverity.INTERNAL; @@ -33,8 +32,6 @@ public enum HttpDiagnosticCodes { HTTP_101("HTTP_101", "remote methods are not allowed in http:Service", ERROR), HTTP_102("HTTP_102", "invalid resource method return type: expected '" + ALLOWED_RETURN_UNION + "', but found '%s'", ERROR), - HTTP_103("HTTP_103", "invalid resource method annotation type: expected 'http:" + RESOURCE_CONFIG_ANNOTATION + - "', but found '%s'", ERROR), HTTP_104("HTTP_104", "invalid annotation type on param '%s': expected one of the following types: " + "'http:Payload', 'http:CallerInfo', 'http:Header', 'http:Query'", ERROR), HTTP_105("HTTP_105", "invalid resource parameter '%s'", ERROR), diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceValidator.java index 8250cfc39e..e591774203 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpResourceValidator.java @@ -155,10 +155,8 @@ private static void extractResourceAnnotationAndValidate(SyntaxNodeAnalysisConte String[] strings = annotName.split(Constants.COLON); if (RESOURCE_CONFIG_ANNOTATION.equals(strings[strings.length - 1].trim())) { validateLinksInResourceConfig(ctx, member, annotation, linksMetaData); - continue; } } - reportInvalidResourceAnnotation(ctx, annotReference.location(), annotName); } } @@ -923,11 +921,6 @@ private static boolean isValidReturnTypeWithCaller(TypeSymbol returnTypeDescript } } - private static void reportInvalidResourceAnnotation(SyntaxNodeAnalysisContext ctx, Location location, - String annotName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_103, annotName); - } - private static void reportInvalidParameterAnnotation(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_104, paramName); diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 6c306cb58e..63cc4c3637 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -3,7 +3,7 @@ _Owners_: @shafreenAnfar @TharmiganK @ayeshLK @chamil321 _Reviewers_: @shafreenAnfar @bhashinee @TharmiganK @ldclakmal _Created_: 2021/12/23 -_Updated_: 2023/04/17 +_Updated_: 2024/06/13 _Edition_: Swan Lake @@ -42,7 +42,9 @@ The conforming implementation of the specification is released and included in t * 2.3.5.1. [Status Code Response](#2351-status-code-response) * 2.3.5.2. [Return nil](#2352-return-nil) * 2.3.5.3. [Default response status codes](#2353-default-response-status-codes) - * 2.3.6. [Introspection resource](#236-introspection-resource) + * 2.3.6. [OpenAPI specification resources](#236-openapi-specification-resources) + * 2.3.6.1. [Introspection resource](#2361-introspection-resource) + * 2.3.6.2. [SwaggerUI resource](#2362-swaggerui-resource) * 2.4. [Client](#24-client) * 2.4.1. [Client types](#241-client-types) * 2.4.1.1. [Security](#2411-security) @@ -944,18 +946,20 @@ response when returning `anydata` directly from a resource method. | HEAD | Retrieve headers | 200 OK | | OPTIONS | Retrieve permitted communication options | 200 OK | -#### 2.3.6. Introspection resource +#### 2.3.6. OpenAPI specification resources -The introspection resource is internally generated for each service and host the openAPI doc can be generated -(or retrieved) at runtime when requested from the hosted service itself. In order to get the openAPI doc hosted -resource path, user can send an OPTIONS request either to one of the resources or the service. The link header -in the 204 response specifies the location. Then user can send a GET request to the dynamically generated URL in the -link header with the relation openapi to get the openAPI definition for the service. +OAS resources are internally generated for each service and host the generated OpenAPI specification for the service in +different formats. In order to access these resources user can send an OPTIONS request either to one of the resources or +the service base-path. The link header in the 204 response specifies the location for the OAS resources. Sample service ```ballerina import ballerina/http; +import ballerina/openapi; +@openapi:ServiceInfo { + embed: true +} service /hello on new http:Listener(9090) { resource function get greeting() returns string { return "Hello world"; @@ -963,97 +967,95 @@ service /hello on new http:Listener(9090) { } ``` -Output of OPTIONS call to usual resource +Output of OPTIONS call to service base path ```ballerina -curl -v localhost:9090/hello/greeting -X OPTIONS -* Trying ::1... +curl -v localhost:9090/hello -X OPTIONS +* Trying 127.0.0.1:9090... * TCP_NODELAY set -* Connected to localhost (::1) port 9090 (#0) -> OPTIONS /hello/greeting HTTP/1.1 +* Connected to localhost (127.0.0.1) port 9090 (#0) +> OPTIONS /hello HTTP/1.1 > Host: localhost:9090 -> User-Agent: curl/7.64.1 +> User-Agent: curl/7.68.0 > Accept: */* -> +> < HTTP/1.1 204 No Content < allow: GET, OPTIONS -< link: ;rel="service-desc" -< server: ballerina/2.0.0-beta.2.1 -< date: Wed, 18 Aug 2021 14:09:40 +0530 +< link: ;rel="service-desc", ;rel="swagger-ui" +< server: ballerina +< date: Thu, 13 Jun 2024 20:04:11 +0530 < +* Connection #0 to host localhost left intact +* Closing connection 0 ``` +##### 2.3.6.1. Introspection resource + +The introspection resource is one of the generated OAS resources, and it hosts the OpenAPI specification for the service +in JSON format. The user can send a GET request to the resource path specified in the link header with the relation +attribute set to `service-desc`. + Output of GET call to introspection resource ```ballerina curl -v localhost:9090/hello/openapi-doc-dygixywsw -* Trying ::1... +* Trying 127.0.0.1:9090... * TCP_NODELAY set -* Connected to localhost (::1) port 9090 (#0) +* Connected to localhost (127.0.0.1) port 9090 (#0) > GET /hello/openapi-doc-dygixywsw HTTP/1.1 > Host: localhost:9090 -> User-Agent: curl/7.64.1 +> User-Agent: curl/7.68.0 > Accept: */* > +* Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < content-type: application/json -< content-length: 634 -< server: ballerina/2.0.0-beta.2.1 -< date: Wed, 18 Aug 2021 14:22:29 +0530 +< content-length: 675 +< server: ballerina +< date: Thu, 13 Jun 2024 20:05:03 +0530 < { - "openapi": "3.0.1", - "info": { - "title": " hello", - "version": "1.0.0" + "openapi" : "3.0.1", + "info" : { + "title" : "Hello", + "version" : "0.1.0" }, - "servers": [ - { - "url": "localhost:9090/hello" - } - ], - "paths": { - "/greeting": { - "get": { - "operationId": "operation1_get_/greeting", - "responses": { - "200": { - "description": "Ok", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } + "servers" : [ { + "url" : "{server}:{port}/hello", + "variables" : { + "server" : { + "default" : "http://localhost" + }, + "port" : { + "default" : "9090" + } + } + } ], + "paths" : { + "/greeting" : { + "get" : { + "operationId" : "getGreeting", + "responses" : { + "200" : { + "description" : "Ok", + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } } - } + } + } } - } - }, - "components": {} + } + } + } } ``` -Output of OPTIONS call to service base path +##### 2.3.6.2. SwaggerUI resource -```ballerina -curl -v localhost:9090/hello -X OPTIONS -* Trying ::1... -* TCP_NODELAY set -* Connected to localhost (::1) port 9090 (#0) -> OPTIONS /hello HTTP/1.1 -> Host: localhost:9090 -> User-Agent: curl/7.64.1 -> Accept: */* -> -< HTTP/1.1 204 No Content -< allow: GET, OPTIONS -< link: ;rel="service-desc" -< server: ballerina/2.0.0-beta.2.1 -< date: Thu, 19 Aug 2021 13:47:29 +0530 -< -* Connection #0 to host localhost left intact -* Closing connection 0 -``` +The swagger-ui resource is one of the generated OAS resources, and it hosts the OpenAPI specification for the service in +HTML format. The user can view it in a web browser by accessing the URL specified in the HTTP link header, which has +relation attribute set to `swagger-ui`. ### 2.4. Client A client allows the program to send network messages to a remote process according to the HTTP protocol. The fixed diff --git a/gradle.properties b/gradle.properties index c469d7d23c..fc66f4cb0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=2.11.2-SNAPSHOT +version=2.11.3-SNAPSHOT ballerinaLangVersion=2201.9.0 ballerinaTomlParserVersion=1.2.2 commonsLang3Version=3.12.0 diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpIntrospectionResource.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpIntrospectionResource.java index 7add899cce..09d42ec381 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpIntrospectionResource.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpIntrospectionResource.java @@ -18,53 +18,40 @@ package io.ballerina.stdlib.http.api; -import java.util.List; - -import static io.ballerina.stdlib.http.api.HttpConstants.SINGLE_SLASH; +import io.netty.handler.codec.http.HttpHeaderValues; /** * {@code HttpIntrospectionResource} is the resource which respond the service Open API JSON specification. * * @since SL beta 3 */ -public class HttpIntrospectionResource extends HttpResource { +public class HttpIntrospectionResource extends HttpOASResource { private static final String RESOURCE_NAME = "openapi-doc-dygixywsw"; - private static final String RESOURCE_METHOD = "$get$"; private static final String REL_PARAM = "rel=\"service-desc\""; private final byte[] payload; - protected HttpIntrospectionResource(HttpService httpService, byte[] payload) { - String path = (httpService.getBasePath() + SINGLE_SLASH + RESOURCE_NAME).replaceAll("/+", SINGLE_SLASH); - httpService.setIntrospectionResourcePathHeaderValue("<" + path + ">;" + REL_PARAM); - this.payload = payload.clone(); - } - - public String getName() { - return RESOURCE_METHOD + RESOURCE_NAME; - } - - public String getPath() { - return SINGLE_SLASH + RESOURCE_NAME; + public HttpIntrospectionResource(HttpService httpService, byte[] payload) { + super(httpService, REL_PARAM, RESOURCE_NAME); + this.payload = payload; } + @Override public byte[] getPayload() { return this.payload.clone(); } - public List getMethods() { - return List.of(HttpConstants.HTTP_METHOD_GET); - } - - public List getConsumes() { - return null; + @Override + protected String getResourceName() { + return RESOURCE_NAME; } - public List getProduces() { - return null; + @Override + public String getContentType() { + return HttpHeaderValues.APPLICATION_JSON.toString(); } - public static String getIntrospectionResourceId() { - return RESOURCE_METHOD + RESOURCE_NAME; + public static String getResourceId() { + return String.format("%s%s", RESOURCE_METHOD, RESOURCE_NAME); } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpOASResource.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpOASResource.java new file mode 100644 index 0000000000..d127a68895 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpOASResource.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.api; + +import java.util.Collections; +import java.util.List; + +import static io.ballerina.stdlib.http.api.HttpConstants.SINGLE_SLASH; + +/** + * {@code HttpOASResource} is the super class for the service introspection resources. + * + * @since v2.11.2 + */ +public abstract class HttpOASResource extends HttpResource { + protected static final String RESOURCE_METHOD = "$get$"; + + protected HttpOASResource(HttpService httpService, String rel, String resourcePath) { + String path = (httpService.getBasePath() + SINGLE_SLASH + resourcePath).replaceAll("/+", SINGLE_SLASH); + httpService.addOasResourceLink("<" + path + ">;" + rel); + } + + @Override + public String getName() { + return String.format("%s%s", RESOURCE_METHOD, getResourceName()); + } + + @Override + public String getPath() { + return String.format("%s%s", SINGLE_SLASH, getResourceName()); + } + + @Override + public List getMethods() { + return List.of(HttpConstants.HTTP_METHOD_GET); + } + + @Override + public List getConsumes() { + return Collections.emptyList(); + } + + @Override + public List getProduces() { + return Collections.emptyList(); + } + + protected abstract String getResourceName(); + + public abstract byte[] getPayload(); + + public abstract String getContentType(); +} diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpService.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpService.java index 86b0318411..0d3ba28c9d 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpService.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpService.java @@ -90,7 +90,7 @@ public class HttpService implements Service { private String hostName; private String chunkingConfig; private String mediaTypeSubtypePrefix; - private String introspectionResourcePath; + private List oasResourceLinks = new ArrayList<>(); private boolean treatNilableAsOptional = true; private List interceptorServicesRegistries; private BArray balInterceptorServicesArray; @@ -273,6 +273,8 @@ private static void processResources(HttpService httpService) { if (introspectionPayload.length > 0) { updateResourceTree(httpService, httpResources, new HttpIntrospectionResource(httpService, introspectionPayload)); + updateResourceTree( + httpService, httpResources, new HttpSwaggerUiResource(httpService, introspectionPayload)); } else { log.debug("OpenAPI definition is not available"); } @@ -406,12 +408,15 @@ protected static BMap getServiceConfigAnnotation(BObject service, String package } @Override - public String getIntrospectionResourcePathHeaderValue() { - return this.introspectionResourcePath; + public String getOasResourceLink() { + if (this.oasResourceLinks.isEmpty()) { + return null; + } + return String.join(", ", this.oasResourceLinks); } - protected void setIntrospectionResourcePathHeaderValue(String introspectionResourcePath) { - this.introspectionResourcePath = introspectionResourcePath; + protected void addOasResourceLink(String oasResourcePath) { + this.oasResourceLinks.add(oasResourcePath); } public void setInterceptorServicesRegistries(List interceptorServicesRegistries) { diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpSwaggerUiResource.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpSwaggerUiResource.java new file mode 100644 index 0000000000..6d7f0182d8 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpSwaggerUiResource.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.http.api; + +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.util.CharsetUtil; + +/** + * {@code HttpSwaggerUiResource} is the resource which respond the Swagger-UI for the generated Open API specification. + * + * @since v2.11.2 + */ +public class HttpSwaggerUiResource extends HttpOASResource { + + private static final String STATIC_HTML_PAGE = """ + + + + + + + SwaggerUI + + + +
+ + + + + """; + private static final String RESOURCE_NAME = "swagger-ui-dygixywsw"; + private static final String REL_PARAM = "rel=\"swagger-ui\""; + private static final String OAS_PLACEHOLDER = "OPENAPI_SPEC"; + private final byte[] payload; + + public HttpSwaggerUiResource(HttpService httpService, byte[] payload) { + super(httpService, REL_PARAM, RESOURCE_NAME); + String oasSpec = new String(payload.clone(), CharsetUtil.UTF_8); + String content = STATIC_HTML_PAGE.replace(OAS_PLACEHOLDER, oasSpec); + this.payload = content.getBytes(CharsetUtil.UTF_8); + } + + @Override + public byte[] getPayload() { + return this.payload.clone(); + } + + @Override + protected String getResourceName() { + return RESOURCE_NAME; + } + + @Override + public String getContentType() { + return HttpHeaderValues.TEXT_HTML.toString(); + } + + public static String getResourceId() { + return String.format("%s%s", RESOURCE_METHOD, RESOURCE_NAME); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/InterceptorService.java b/native/src/main/java/io/ballerina/stdlib/http/api/InterceptorService.java index 608613c4aa..0a2def95eb 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/InterceptorService.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/InterceptorService.java @@ -74,7 +74,7 @@ public List getAllAllowedMethods() { } @Override - public String getIntrospectionResourcePathHeaderValue() { + public String getOasResourceLink() { return null; } diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/ResourceDataElement.java b/native/src/main/java/io/ballerina/stdlib/http/api/ResourceDataElement.java index e189775e0b..f4962bdc62 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/ResourceDataElement.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/ResourceDataElement.java @@ -75,11 +75,16 @@ public void setData(Resource newResource) { this.resource.forEach(r -> { for (String newMethod : newMethods) { if (DispatcherUtil.isMatchingMethodExist(r, newMethod)) { - if (r.getName().equals(HttpIntrospectionResource.getIntrospectionResourceId())) { + if (r.getName().equals(HttpIntrospectionResource.getResourceId())) { String message = "Resources cannot have the accessor and name as same as the auto generated " + "Open API spec retrieval resource: '" + r.getName() + "'"; throw HttpUtil.createHttpError(message, GENERIC_LISTENER_ERROR); } + if (r.getName().equals(HttpSwaggerUiResource.getResourceId())) { + String message = "Resources cannot have the accessor and name as same as the auto generated " + + "Swagger-UI retrieval resource: '" + r.getName() + "'"; + throw HttpUtil.createHttpError(message, GENERIC_LISTENER_ERROR); + } throw HttpUtil.createHttpError("Two resources have the same addressable URI, " + r.getName() + " and " + newResource.getName(), GENERIC_LISTENER_ERROR); @@ -177,7 +182,7 @@ private void validateConsumes(Resource resource, HttpCarbonMessage cMsg) { String contentMediaType = extractContentMediaType(cMsg.getHeader(HttpHeaderNames.CONTENT_TYPE.toString())); List consumesList = resource.getConsumes(); - if (consumesList == null) { + if (consumesList == null || consumesList.isEmpty()) { return; } //when Content-Type header is not set, treat it as "application/octet-stream" @@ -205,7 +210,7 @@ private void validateProduces(Resource resource, HttpCarbonMessage cMsg) { List acceptMediaTypes = extractAcceptMediaTypes(cMsg.getHeader(HttpHeaderNames.ACCEPT.toString())); List producesList = resource.getProduces(); - if (producesList == null || acceptMediaTypes == null) { + if (producesList == null || producesList.isEmpty() || acceptMediaTypes == null) { return; } //If Accept header field is not present, then it is assumed that the client accepts all media types. diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/ResourceDispatcher.java b/native/src/main/java/io/ballerina/stdlib/http/api/ResourceDispatcher.java index a03322782b..75887eeb90 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/ResourceDispatcher.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/ResourceDispatcher.java @@ -25,7 +25,6 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; import static io.ballerina.stdlib.http.api.HttpErrorType.INTERNAL_RESOURCE_DISPATCHING_SERVER_ERROR; import static io.ballerina.stdlib.http.api.HttpErrorType.INTERNAL_RESOURCE_NOT_FOUND_ERROR; @@ -43,8 +42,12 @@ public static Resource findResource(Service service, HttpCarbonMessage inboundRe HttpResourceArguments resourceArgumentValues = new HttpResourceArguments(); try { Resource resource = service.getUriTemplate().matches(subPath, resourceArgumentValues, inboundRequest); - if (resource instanceof HttpIntrospectionResource) { - handleIntrospectionRequest(inboundRequest, (HttpIntrospectionResource) resource); + if (resource instanceof HttpIntrospectionResource introspectionResource) { + handleOasResourceRequest(inboundRequest, introspectionResource); + return null; + } + if (resource instanceof HttpSwaggerUiResource swaggerUiResource) { + handleOasResourceRequest(inboundRequest, swaggerUiResource); return null; } if (resource != null) { @@ -91,7 +94,7 @@ private static void handleOptionsRequest(HttpCarbonMessage cMsg, Service service throw HttpUtil.createHttpStatusCodeError(INTERNAL_RESOURCE_NOT_FOUND_ERROR, message); } CorsHeaderGenerator.process(cMsg, response, false); - String introspectionResourcePathHeaderValue = service.getIntrospectionResourcePathHeaderValue(); + String introspectionResourcePathHeaderValue = service.getOasResourceLink(); if (introspectionResourcePathHeaderValue != null) { response.setHeader(HttpConstants.LINK_HEADER, introspectionResourcePathHeaderValue); } @@ -101,11 +104,11 @@ private static void handleOptionsRequest(HttpCarbonMessage cMsg, Service service cMsg.waitAndReleaseAllEntities(); } - private static void handleIntrospectionRequest(HttpCarbonMessage cMsg, HttpIntrospectionResource resource) { + private static void handleOasResourceRequest(HttpCarbonMessage cMsg, HttpOASResource resource) { HttpCarbonMessage response = HttpUtil.createHttpCarbonMessage(false); response.waitAndReleaseAllEntities(); response.addHttpContent(new DefaultLastHttpContent(Unpooled.wrappedBuffer(resource.getPayload()))); - response.setHeader(HttpHeaderNames.CONTENT_TYPE.toString(), HttpHeaderValues.APPLICATION_JSON.toString()); + response.setHeader(HttpHeaderNames.CONTENT_TYPE.toString(), resource.getContentType()); response.setHttpStatusCode(200); PipeliningHandler.sendPipelinedResponse(cMsg, response); cMsg.waitAndReleaseAllEntities(); diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/Service.java b/native/src/main/java/io/ballerina/stdlib/http/api/Service.java index 11e4d38f55..2c559f40c1 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/Service.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/Service.java @@ -73,9 +73,9 @@ public interface Service { List getAllAllowedMethods(); /** - * Returns introspection resource path header configured in the service. + * Returns OAS resource path header configured in the service. * * @return the introspection resource path header string */ - String getIntrospectionResourcePathHeaderValue(); + String getOasResourceLink(); } diff --git a/native/src/test/java/io/ballerina/stdlib/http/api/HttpServiceTest.java b/native/src/test/java/io/ballerina/stdlib/http/api/HttpServiceTest.java index 82e748954b..9d1c776721 100644 --- a/native/src/test/java/io/ballerina/stdlib/http/api/HttpServiceTest.java +++ b/native/src/test/java/io/ballerina/stdlib/http/api/HttpServiceTest.java @@ -74,7 +74,7 @@ public void testNotNullServiceBasePath() { @Test public void testGetIntrospectionResourceIdOfIntrospectionResource() { - Assert.assertEquals(HttpIntrospectionResource.getIntrospectionResourceId(), "$get$openapi-doc-dygixywsw"); + Assert.assertEquals(HttpIntrospectionResource.getResourceId(), "$get$openapi-doc-dygixywsw"); } @Test @@ -85,6 +85,19 @@ public void testGetNameOfIntrospectionResource() { Assert.assertEquals(introspectionResource.getName(), "$get$openapi-doc-dygixywsw"); } + @Test + public void testGetSwaggerUiResourceIdOfIntrospectionResource() { + Assert.assertEquals(HttpSwaggerUiResource.getResourceId(), "$get$swagger-ui-dygixywsw"); + } + + @Test + public void testGetNameOfSwaggerUiResource() { + BObject service = TestUtils.getNewServiceObject("hello"); + HttpService httpService = new HttpService(service); + HttpSwaggerUiResource swaggerUiResource = new HttpSwaggerUiResource(httpService, "abc".getBytes()); + Assert.assertEquals(swaggerUiResource.getName(), "$get$swagger-ui-dygixywsw"); + } + @Test public void testGetAbsoluteResourcePath() { HttpService httpService = new HttpService(TestUtils.getNewServiceObject("hello"));