diff --git a/ballerina-tests/http-advanced-tests/Dependencies.toml b/ballerina-tests/http-advanced-tests/Dependencies.toml index cc8f694fec..874342ae83 100644 --- a/ballerina-tests/http-advanced-tests/Dependencies.toml +++ b/ballerina-tests/http-advanced-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -128,7 +128,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina-tests/http-advanced-tests/tests/service_contract_tests.bal b/ballerina-tests/http-advanced-tests/tests/service_contract_tests.bal new file mode 100644 index 0000000000..81a48e24f0 --- /dev/null +++ b/ballerina-tests/http-advanced-tests/tests/service_contract_tests.bal @@ -0,0 +1,116 @@ +// 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. + +import ballerina/crypto; +import ballerina/http; +import ballerina/http_test_common as common; +import ballerina/test; + +final http:Client serviceContractClient = check new (string `localhost:${serviceContractTestPort}/socialMedia`); + +service common:Service on new http:Listener(serviceContractTestPort) { + + resource function get users() returns common:User[]|error { + return [{id: 1, name: "Alice", email: "alice@gmail.com"}, {id: 2, name: "Bob", email: "bob@gmail.com"}]; + } + + resource function get users/[int id]() returns common:User|common:UserNotFound|error { + return {id: 1, name: "Alice", email: "alice@gmail.com"}; + } + + resource function post users(common:NewUser newUser) returns http:Created|error { + return { + body: { + message: "User created successfully" + } + }; + } + + resource function delete users/[int id]() returns http:NoContent|error { + return http:NO_CONTENT; + } + + resource function get users/[int id]/posts() returns common:PostWithMeta[]|common:UserNotFound|error { + return [{id: 1, content: "Content 1", createdAt: "2020-01-01T10:00:00Z"}, {id: 2, content: "Content 2", createdAt: "2020-01-01T10:00:00Z"}]; + } + + resource function post users/[int id]/posts(common:NewPost newPost) returns http:Created|common:UserNotFound|common:PostForbidden|error { + return error("invalid post"); + } + + public function createInterceptors() returns common:ErrorInterceptor { + return new (); + } +} + +@test:Config {} +function testCachingWithServiceContract() returns error? { + http:Response response = check serviceContractClient->/users; + json payload = [{id: 1, name: "Alice", email: "alice@gmail.com"}, {id: 2, name: "Bob", email: "bob@gmail.com"}]; + test:assertTrue(response.hasHeader(common:LAST_MODIFIED)); + common:assertHeaderValue(check response.getHeader(common:CACHE_CONTROL), "must-revalidate,public,max-age=10"); + common:assertHeaderValue(check response.getHeader(common:ETAG), crypto:crc32b(payload.toString().toBytes())); + common:assertHeaderValue(check response.getHeader(common:CONTENT_TYPE), "application/vnd.socialMedia+json"); + common:assertJsonPayload(response.getJsonPayload(), payload); +} + +@test:Config {} +function testLinksInServiceContract() returns error? { + record{*http:Links; *common:User;} response = check serviceContractClient->/users/'2; + map expectedLinks = { + "delete-user": { + href: "/socialMedia/users/{id}", + types: ["application/vnd.socialMedia+json"], + methods: [http:DELETE] + }, + "create-posts": { + href: "/socialMedia/users/{id}/posts", + types: ["text/vnd.socialMedia+plain"], + methods: [http:POST] + }, + "get-posts": { + href: "/socialMedia/users/{id}/posts", + types: ["application/vnd.socialMedia+json", "text/vnd.socialMedia+plain"], + methods: [http:GET] + } + }; + record{} payload = {"id": 1, "name": "Alice", "email": "alice@gmail.com", "_links": expectedLinks}; + test:assertEquals(response, payload); +} + +@test:Config {} +function testPayloadAnnotationWithServiceContract() returns error? { + common:NewUser newUser = {name: "Alice", email: "alice@gmail.com"}; + http:Response response = check serviceContractClient->/users.post(newUser); + test:assertEquals(response.statusCode, 415); + common:assertTextPayload(response.getTextPayload(), "content-type : application/json is not supported"); + + record{string message;} result = check serviceContractClient->/users.post(newUser, mediaType = "application/vnd.socialMedia+json"); + test:assertEquals(result.message, "User created successfully"); +} + +@test:Config {} +function testInterceptorWithServiceContract() returns error? { + common:NewPost newPost = {content: "sample content"}; + http:Response response = check serviceContractClient->/users/'1/posts.post(newPost, mediaType = "application/vnd.socialMedia+json"); + test:assertEquals(response.statusCode, 500); + common:assertJsonPayload(response.getTextPayload(), "invalid post"); + + json invalidPost = {message: "sample content"}; + response = check serviceContractClient->/users/'1/posts.post(invalidPost, mediaType = "application/vnd.socialMedia+json"); + test:assertEquals(response.statusCode, 400); + common:assertTrueTextPayload(response.getTextPayload(), "data binding failed"); +} diff --git a/ballerina-tests/http-advanced-tests/tests/test_service_ports.bal b/ballerina-tests/http-advanced-tests/tests/test_service_ports.bal index 1d6a9cda66..e6b955084c 100644 --- a/ballerina-tests/http-advanced-tests/tests/test_service_ports.bal +++ b/ballerina-tests/http-advanced-tests/tests/test_service_ports.bal @@ -48,3 +48,5 @@ const int cookieTestPort2 = 9254; const int http1SsePort = 9094; const int http2SsePort = 9095; + +const int serviceContractTestPort = 9096; diff --git a/ballerina-tests/http-client-tests/Dependencies.toml b/ballerina-tests/http-client-tests/Dependencies.toml index 17ebb0a219..ecef451d88 100644 --- a/ballerina-tests/http-client-tests/Dependencies.toml +++ b/ballerina-tests/http-client-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -124,7 +124,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina-tests/http-dispatching-tests/Dependencies.toml b/ballerina-tests/http-dispatching-tests/Dependencies.toml index c961d36156..49a169a3ed 100644 --- a/ballerina-tests/http-dispatching-tests/Dependencies.toml +++ b/ballerina-tests/http-dispatching-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -127,7 +127,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina-tests/http-interceptor-tests/Dependencies.toml b/ballerina-tests/http-interceptor-tests/Dependencies.toml index f683df447e..c724994724 100644 --- a/ballerina-tests/http-interceptor-tests/Dependencies.toml +++ b/ballerina-tests/http-interceptor-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -118,7 +118,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina-tests/http-misc-tests/Dependencies.toml b/ballerina-tests/http-misc-tests/Dependencies.toml index e04fbba65e..efc7f1262b 100644 --- a/ballerina-tests/http-misc-tests/Dependencies.toml +++ b/ballerina-tests/http-misc-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -121,7 +121,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina-tests/http-resiliency-tests/Dependencies.toml b/ballerina-tests/http-resiliency-tests/Dependencies.toml index 7a3fb8745f..ab3a54da27 100644 --- a/ballerina-tests/http-resiliency-tests/Dependencies.toml +++ b/ballerina-tests/http-resiliency-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -119,7 +119,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina-tests/http-security-tests/Dependencies.toml b/ballerina-tests/http-security-tests/Dependencies.toml index 83fb155b6e..3da196359d 100644 --- a/ballerina-tests/http-security-tests/Dependencies.toml +++ b/ballerina-tests/http-security-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -123,7 +123,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina-tests/http-service-tests/Dependencies.toml b/ballerina-tests/http-service-tests/Dependencies.toml index 60e494b42e..b6a071eda3 100644 --- a/ballerina-tests/http-service-tests/Dependencies.toml +++ b/ballerina-tests/http-service-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -124,7 +124,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina-tests/http-test-common/Dependencies.toml b/ballerina-tests/http-test-common/Dependencies.toml index ebbef3ddd6..f7db575b4d 100644 --- a/ballerina-tests/http-test-common/Dependencies.toml +++ b/ballerina-tests/http-test-common/Dependencies.toml @@ -5,14 +5,99 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.11.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.8.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.7.2" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] [[package]] org = "ballerina" name = "http_test_common" version = "2.12.0" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, @@ -36,6 +121,20 @@ org = "ballerina" name = "jballerina.java" version = "0.0.0" +[[package]] +org = "ballerina" +name = "jwt" +version = "2.12.1" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + [[package]] org = "ballerina" name = "lang.__internal" @@ -54,6 +153,14 @@ dependencies = [ {org = "ballerina", name = "lang.__internal"} ] +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + [[package]] org = "ballerina" name = "lang.error" @@ -85,6 +192,14 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + [[package]] org = "ballerina" name = "lang.string" @@ -105,6 +220,20 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "log" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerina", packageName = "log", moduleName = "log"} +] + [[package]] org = "ballerina" name = "mime" @@ -118,6 +247,45 @@ modules = [ {org = "ballerina", packageName = "mime", moduleName = "mime"} ] +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.11.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.2.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + [[package]] org = "ballerina" name = "test" diff --git a/ballerina-tests/http-test-common/types.bal b/ballerina-tests/http-test-common/types.bal new file mode 100644 index 0000000000..571f681f1d --- /dev/null +++ b/ballerina-tests/http-test-common/types.bal @@ -0,0 +1,177 @@ +// 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. + +import ballerina/http; +import ballerina/log; + +# The service type that handles the social media API +@http:ServiceConfig { + mediaTypeSubtypePrefix: "vnd.socialMedia", + basePath: "/socialMedia" +} +public type Service service object { + *http:ServiceContract; + *http:InterceptableService; + + public function createInterceptors() returns ErrorInterceptor; + + # Get all users + # + # + return - List of users(`User[]`) or an error + @http:ResourceConfig { + name: "users" + } + resource function get users() returns @http:Cache {maxAge: 10} User[]|error; + + # Get a user by ID + # + # + id - User ID + # + return - `User` or NotFound response(`UserNotFound`) when the user is not found or an error + @http:ResourceConfig { + name: "user", + linkedTo: [ + {name: "user", method: http:DELETE, relation: "delete-user"}, + {name: "posts", method: http:POST, relation: "create-posts"}, + {name: "posts", method: http:GET, relation: "get-posts"} + ] + } + resource function get users/[int id]() returns User|UserNotFound|error; + + # Create a new user + # + # + newUser - New user details(`NewUser`) as payload + # + return - Created(`http:Created`) response or an error + @http:ResourceConfig { + name: "users", + linkedTo: [ + {name: "user", method: http:GET, relation: "get-user"}, + {name: "user", method: http:DELETE, relation: "delete-user"}, + {name: "posts", method: http:POST, relation: "create-posts"}, + {name: "posts", method: http:GET, relation: "get-posts"} + ], + consumes: ["application/vnd.socialMedia+json"] + } + resource function post users(NewUser newUser) returns http:Created|error; + + # Delete a user by ID + # + # + id - User ID + # + return - NoContent response(`http:NoContent`) or an error + @http:ResourceConfig { + name: "user" + } + resource function delete users/[int id]() returns http:NoContent|error; + + # Get all posts of a user + # + # + id - User ID + # + return - List of posts with metadata(`PostWithMeta[]`) or NotFound response(`UserNotFound`) when the user is not found or an error + @http:ResourceConfig { + name: "posts" + } + resource function get users/[int id]/posts() returns @http:Cache {maxAge: 25} PostWithMeta[]|UserNotFound|error; + + # Create a new post for a user + # + # + id - User ID + # + newPost - New post details(`NewPost`) as payload + # + return - Created(`http:Created`) response or an error + @http:ResourceConfig { + name: "posts", + linkedTo: [ + {name: "posts", method: http:POST, relation: "create-posts"} + ], + consumes: ["application/vnd.socialMedia+json"] + } + resource function post users/[int id]/posts(@http:Payload NewPost newPost) returns http:Created|UserNotFound|PostForbidden|error; +}; + +public isolated service class ErrorInterceptor { + *http:ResponseErrorInterceptor; + + isolated remote function interceptResponseError(error err, http:Response res, http:RequestContext ctx) returns DefaultResponse { + log:printError("error occurred", err); + return { + body: err.message(), + status: new (res.statusCode) + }; + } +} + +# Represents a user in the system +# +# + id - user ID +# + name - user name +# + email - user email +public type User record { + int id; + string name; + string email; +}; + +# Represents a new user +# +# + name - user name +# + email - user email +public type NewUser record { + string name; + string email; +}; + +# Represents a user not found error +# +# + body - error message +public type UserNotFound record {| + *http:NotFound; + ErrorMessage body; +|}; + +# Represents a new post +# +# + content - post content +public type NewPost record { + string content; +}; + +# Represents a post with metadata +# +# + id - post ID +# + content - post content +# + createdAt - post creation time +public type PostWithMeta record { + int id; + string content; + string createdAt; +}; + +# Represents a post forbidden error +# +# + body - error message +public type PostForbidden record {| + *http:Forbidden; + ErrorMessage body; +|}; + +# Represents a default response +# +# + body - response body +public type DefaultResponse record {| + *http:DefaultStatusCodeResponse; + ErrorMessage body; +|}; + +# Represents a error message +public type ErrorMessage string; diff --git a/ballerina-tests/http2-tests/Dependencies.toml b/ballerina-tests/http2-tests/Dependencies.toml index dcd3efb0ed..7692dbd048 100644 --- a/ballerina-tests/http2-tests/Dependencies.toml +++ b/ballerina-tests/http2-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" @@ -124,7 +124,9 @@ name = "http_test_common" version = "2.12.0" scope = "testOnly" dependencies = [ + {org = "ballerina", name = "http"}, {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 462ed1fc55..a98a03c92a 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -7,7 +7,7 @@ keywords = ["http", "network", "service", "listener", "client"] repository = "https://github.com/ballerina-platform/module-ballerina-http" icon = "icon.png" license = ["Apache-2.0"] -distribution = "2201.9.0" +distribution = "2201.10.0" export = ["http", "http.httpscerr"] [platform.java17] diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index fc0dcc7e77..bd4522d9b1 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -4,3 +4,6 @@ class = "io.ballerina.stdlib.http.compiler.HttpCompilerPlugin" [[dependency]] path = "../compiler-plugin/build/libs/http-compiler-plugin-2.12.0-SNAPSHOT.jar" + +[[dependency]] +path = "../compiler-plugin/build/libs/ballerina-to-openapi-2.1.0-20240801-125916-f3852a9.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 90f947b41a..abc1a52610 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0" +distribution-version = "2201.10.0-20240801-104200-87df251c" [[package]] org = "ballerina" diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 8a3d06ae47..36617a6b8c 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -164,6 +164,7 @@ task updateTomlFiles { def stdlibDependentLz4Version = project.lz4Version def stdlibDependentMarshallingVersion = project.marshallingVersion def stdlibDependentProtobufVersion = project.protobufVersion + def ballerinaToOpenApiVersion = project.ballerinaToOpenApiVersion def newBallerinaToml = ballerinaTomlFilePlaceHolder.text.replace("@project.version@", project.version) newBallerinaToml = newBallerinaToml.replace("@toml.version@", tomlVersion) @@ -183,6 +184,7 @@ task updateTomlFiles { ballerinaTomlFile.text = newBallerinaToml def newCompilerPluginToml = compilerPluginTomlFilePlaceHolder.text.replace("@project.version@", project.version) + newCompilerPluginToml = newCompilerPluginToml.replace("@ballerinaToOpenApiVersion.version@", ballerinaToOpenApiVersion) compilerPluginTomlFile.text = newCompilerPluginToml } } diff --git a/ballerina/http_annotation.bal b/ballerina/http_annotation.bal index 9eb58f8b3c..d22a63c7f2 100644 --- a/ballerina/http_annotation.bal +++ b/ballerina/http_annotation.bal @@ -24,7 +24,9 @@ # + mediaTypeSubtypePrefix - Service specific media-type subtype prefix # + treatNilableAsOptional - Treat Nilable parameters as optional # + openApiDefinition - The generated OpenAPI definition for the HTTP service. This is auto-generated at compile-time if OpenAPI doc auto generation is enabled -# + validation - Enables the inbound payload validation functionalty which provided by the constraint package. Enabled by default +# + validation - Enables the inbound payload validation functionality which provided by the constraint package. Enabled by default +# + serviceType - The service object type which defines the service contract. This is auto-generated at compile-time +# + basePath - Base path to be used with the service implementation. This is only allowed on service contract types public type HttpServiceConfig record {| string host = "b7a.default"; CompressionConfig compression = {}; @@ -33,8 +35,11 @@ public type HttpServiceConfig record {| ListenerAuthConfig[] auth?; string mediaTypeSubtypePrefix?; boolean treatNilableAsOptional = true; + @deprecated byte[] openApiDefinition = []; boolean validation = true; + typedesc serviceType?; + string basePath?; |}; # Configurations for CORS support. @@ -55,7 +60,7 @@ public type CorsConfig record {| |}; # The annotation which is used to configure an HTTP service. -public annotation HttpServiceConfig ServiceConfig on service; +public annotation HttpServiceConfig ServiceConfig on service, type; # Configuration for an HTTP resource. # @@ -108,13 +113,13 @@ public type HttpHeader record {| |}; # The annotation which is used to define the Header resource signature parameter. -public annotation HttpHeader Header on parameter; +public const annotation HttpHeader Header on parameter; # Defines the query resource signature parameter. public type HttpQuery record {||}; # The annotation which is used to define the query resource signature parameter. -public annotation HttpQuery Query on parameter; +public const annotation HttpQuery Query on parameter; # Defines the HTTP response cache configuration. By default the `no-cache` directive is setted to the `cache-control` # header. In addition to that `etag` and `last-modified` headers are also added for cache validation. diff --git a/ballerina/http_types.bal b/ballerina/http_types.bal index db54dc70cd..a96b48ec4a 100644 --- a/ballerina/http_types.bal +++ b/ballerina/http_types.bal @@ -29,6 +29,11 @@ public type Service distinct service object { }; +# The HTTP service contract type. +public type ServiceContract distinct service object { + *Service; +}; + # The types of data values that are expected by the HTTP `client` to return after the data binding operation. public type TargetType typedesc>; diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index efc9e20756..2bb22346b2 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -7,7 +7,7 @@ keywords = ["http", "network", "service", "listener", "client"] repository = "https://github.com/ballerina-platform/module-ballerina-http" icon = "icon.png" license = ["Apache-2.0"] -distribution = "2201.9.0" +distribution = "2201.10.0" export = ["http", "http.httpscerr"] [platform.java17] diff --git a/build-config/resources/CompilerPlugin.toml b/build-config/resources/CompilerPlugin.toml index 0f419b7db0..e822802983 100644 --- a/build-config/resources/CompilerPlugin.toml +++ b/build-config/resources/CompilerPlugin.toml @@ -4,3 +4,6 @@ class = "io.ballerina.stdlib.http.compiler.HttpCompilerPlugin" [[dependency]] path = "../compiler-plugin/build/libs/http-compiler-plugin-@project.version@.jar" + +[[dependency]] +path = "../compiler-plugin/build/libs/ballerina-to-openapi-@ballerinaToOpenApiVersion.version@.jar" diff --git a/changelog.md b/changelog.md index dc4b86aecb..5134e6ddba 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ 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) - [Add connection eviction feature to handle connections that receive GO_AWAY from the client](https://github.com/ballerina-platform/ballerina-library/issues/6734) - [Enhanced the configurability of Ballerina access logging by introducing multiple configuration options.](https://github.com/ballerina-platform/ballerina-library/issues/6111) +- [Introduce HTTP service contract object type](https://github.com/ballerina-platform/ballerina-library/issues/6378) ### Fixed diff --git a/codecov.yml b/codecov.yml index a24e1d32da..e23fd2e114 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,6 +5,7 @@ fixes: ignore: - "ballerina-tests" - "test-utils" + - "compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/oas" # This is covered in OpenAPI tool coverage: precision: 2 diff --git a/compiler-plugin-tests/build.gradle b/compiler-plugin-tests/build.gradle index bda6b47a25..73c3f3cd25 100644 --- a/compiler-plugin-tests/build.gradle +++ b/compiler-plugin-tests/build.gradle @@ -101,3 +101,4 @@ jacocoTestReport { } test.dependsOn ":http-ballerina:build" +compileTestJava.dependsOn ":http-compiler-plugin:copyOpenApiJar" diff --git a/compiler-plugin-tests/spotbugs-exclude.xml b/compiler-plugin-tests/spotbugs-exclude.xml index 6f10e573e5..7ac289191f 100644 --- a/compiler-plugin-tests/spotbugs-exclude.xml +++ b/compiler-plugin-tests/spotbugs-exclude.xml @@ -36,6 +36,10 @@ + + + + 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 8fe978f0eb..92e1aafe3e 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 @@ -60,6 +60,14 @@ import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_150; import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_151; import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_152; +import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_153; +import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_154; +import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_155; +import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_156; +import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_157; +import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_158; +import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_159; +import static io.ballerina.stdlib.http.compiler.CompilerPluginTestConstants.HTTP_160; /** * This class includes tests for Ballerina Http compiler plugin. @@ -884,4 +892,41 @@ public void testInterceptableServiceInterceptors() { Assert.assertFalse(diagnosticResult.hasWarnings()); Assert.assertFalse(diagnosticResult.hasErrors()); } + + @Test + public void testServiceContractValidations() { + Package currentPackage = loadPackage("sample_package_40"); + PackageCompilation compilation = currentPackage.getCompilation(); + DiagnosticResult diagnosticResult = compilation.diagnosticResult(); + Assert.assertEquals(diagnosticResult.errorCount(), 8); + assertError(diagnosticResult, 0, "base path not allowed in the service declaration which is" + + " implemented via the 'http:ServiceContract' type. The base path is inferred from the service " + + "contract type", HTTP_154); + assertError(diagnosticResult, 1, "'http:ServiceConfig' annotation is not allowed for service " + + "declaration implemented via the 'http:ServiceContract' type. The HTTP annotations are inferred" + + " from the service contract type", HTTP_153); + assertError(diagnosticResult, 2, "configuring base path in the 'http:ServiceConfig' annotation" + + " is not allowed for non service contract types", HTTP_155); + assertError(diagnosticResult, 3, "invalid service type descriptor found in 'http:ServiceConfig' " + + "annotation. Expected service type: 'ContractService' but found: 'ContractServiceWithoutServiceConfig'", + HTTP_156); + assertError(diagnosticResult, 4, "'serviceType' is not allowed in the service which is not implemented" + + " via the 'http:ServiceContract' type", HTTP_157); + assertError(diagnosticResult, 5, "resource function which is not defined in the service contract type:" + + " 'ContractServiceWithResource', is not allowed", HTTP_158); + assertError(diagnosticResult, 6, "'http:ResourceConfig' annotation is not allowed for resource function " + + "implemented via the 'http:ServiceContract' type. The HTTP annotations are inferred from the service" + + " contract type", HTTP_159); + assertError(diagnosticResult, 7, "'http:Header' annotation is not allowed for resource function implemented" + + " via the 'http:ServiceContract' type. The HTTP annotations are inferred from the service contract" + + " type", HTTP_160); + } + + @Test + public void testServiceContractSuccess() { + Package currentPackage = loadPackage("sample_package_41"); + PackageCompilation compilation = currentPackage.getCompilation(); + DiagnosticResult diagnosticResult = compilation.diagnosticResult(); + Assert.assertEquals(diagnosticResult.errorCount(), 0); + } } 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 f7559fd76e..68004d801e 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 @@ -74,4 +74,12 @@ private CompilerPluginTestConstants() {} public static final String HTTP_150 = "HTTP_150"; public static final String HTTP_151 = "HTTP_151"; public static final String HTTP_152 = "HTTP_152"; + public static final String HTTP_153 = "HTTP_153"; + public static final String HTTP_154 = "HTTP_154"; + public static final String HTTP_155 = "HTTP_155"; + public static final String HTTP_156 = "HTTP_156"; + public static final String HTTP_157 = "HTTP_157"; + public static final String HTTP_158 = "HTTP_158"; + public static final String HTTP_159 = "HTTP_159"; + public static final String HTTP_160 = "HTTP_160"; } diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContractTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContractTest.java new file mode 100644 index 0000000000..3d178c41aa --- /dev/null +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContractTest.java @@ -0,0 +1,71 @@ +/* + * 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.stdlib.http.compiler.codeaction; + +import io.ballerina.projects.plugins.codeaction.CodeActionArgument; +import io.ballerina.projects.plugins.codeaction.CodeActionInfo; +import io.ballerina.tools.text.LinePosition; +import io.ballerina.tools.text.LineRange; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; + +/** + * Tests the implement service contract resources code action when a service is implemented via + * the service contract type. + */ +public class ImplementServiceContractTest extends AbstractCodeActionTest { + @Test(dataProvider = "testDataProvider") + public void testCodeActions(String srcFile, int line, int offset, CodeActionInfo expected, String resultFile) + throws IOException { + Path filePath = RESOURCE_PATH.resolve("ballerina_sources") + .resolve("sample_codeaction_package_4") + .resolve(srcFile); + Path resultPath = RESOURCE_PATH.resolve("codeaction") + .resolve(getConfigDir()) + .resolve(resultFile); + + performTest(filePath, LinePosition.from(line, offset), expected, resultPath); + } + + @DataProvider + private Object[][] testDataProvider() { + return new Object[][]{ + {"service.bal", 183, 12, getExpectedCodeAction(), "result1.bal"} + }; + } + + private CodeActionInfo getExpectedCodeAction() { + LineRange lineRange = LineRange.from("service.bal", LinePosition.from(183, 0), + LinePosition.from(185, 1)); + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, lineRange); + CodeActionInfo codeAction = CodeActionInfo.from("Implement service contract resources", + List.of(locationArg)); + codeAction.setProviderName("HTTP_HINT_105/ballerina/http/IMPLEMENT_SERVICE_CONTRACT"); + return codeAction; + } + + protected String getConfigDir() { + return "implement_service_contract_resources"; + } +} diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_codeaction_package_4/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_codeaction_package_4/Ballerina.toml new file mode 100644 index 0000000000..0fba9fca75 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_codeaction_package_4/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "http_test" +name = "codeaction_sample_4" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_codeaction_package_4/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_codeaction_package_4/service.bal new file mode 100644 index 0000000000..a7c0cea0bc --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_codeaction_package_4/service.bal @@ -0,0 +1,186 @@ +// 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. + +// This is added to test some auto generated code segments. +// Please ignore the indentation. + +import ballerina/http; + +import ballerina/http; +import ballerina/log; + +# The service type that handles the social media API +@http:ServiceConfig { + mediaTypeSubtypePrefix: "vnd.socialMedia", + basePath: "/socialMedia" +} +public type Service service object { + *http:ServiceContract; + *http:InterceptableService; + + public function createInterceptors() returns ErrorInterceptor; + + # Get all users + # + # + return - List of users(`User[]`) or an error + @http:ResourceConfig { + name: "users" + } + resource function get users() returns @http:Cache {maxAge: 10} User[]|error; + + # Get a user by ID + # + # + id - User ID + # + return - `User` or NotFound response(`UserNotFound`) when the user is not found or an error + @http:ResourceConfig { + name: "user", + linkedTo: [ + {name: "user", method: http:DELETE, relation: "delete-user"}, + {name: "posts", method: http:POST, relation: "create-posts"}, + {name: "posts", method: http:GET, relation: "get-posts"} + ] + } + resource function get users/[int id]() returns User|UserNotFound|error; + + # Create a new user + # + # + newUser - New user details(`NewUser`) as payload + # + return - Created(`http:Created`) response or an error + @http:ResourceConfig { + name: "users", + linkedTo: [ + {name: "user", method: http:GET, relation: "get-user"}, + {name: "user", method: http:DELETE, relation: "delete-user"}, + {name: "posts", method: http:POST, relation: "create-posts"}, + {name: "posts", method: http:GET, relation: "get-posts"} + ], + consumes: ["application/vnd.socialMedia+json"] + } + resource function post users(NewUser newUser) returns http:Created|error; + + # Delete a user by ID + # + # + id - User ID + # + return - NoContent response(`http:NoContent`) or an error + @http:ResourceConfig { + name: "user" + } + resource function delete users/[int id]() returns http:NoContent|error; + + # Get all posts of a user + # + # + id - User ID + # + return - List of posts with metadata(`PostWithMeta[]`) or NotFound response(`UserNotFound`) when the user is not found or an error + @http:ResourceConfig { + name: "posts" + } + resource function get users/[int id]/posts() returns @http:Cache {maxAge: 25} PostWithMeta[]|UserNotFound|error; + + # Create a new post for a user + # + # + id - User ID + # + newPost - New post details(`NewPost`) as payload + # + return - Created(`http:Created`) response or an error + @http:ResourceConfig { + name: "posts", + linkedTo: [ + {name: "posts", method: http:POST, relation: "create-posts"} + ], + consumes: ["application/vnd.socialMedia+json"] + } + resource function post users/[int id]/posts(@http:Payload NewPost newPost) returns http:Created|UserNotFound|PostForbidden|error; +}; + +public isolated service class ErrorInterceptor { + *http:ResponseErrorInterceptor; + + isolated remote function interceptResponseError(error err, http:Response res, http:RequestContext ctx) returns DefaultResponse { + log:printError("error occurred", err); + return { + body: err.message(), + status: new (res.statusCode) + }; + } +} + +# Represents a user in the system +# +# + id - user ID +# + name - user name +# + email - user email +public type User record { + int id; + string name; + string email; +}; + +# Represents a new user +# +# + name - user name +# + email - user email +public type NewUser record { + string name; + string email; +}; + +# Represents a user not found error +# +# + body - error message +public type UserNotFound record {| + *http:NotFound; + ErrorMessage body; +|}; + +# Represents a new post +# +# + content - post content +public type NewPost record { + string content; +}; + +# Represents a post with metadata +# +# + id - post ID +# + content - post content +# + createdAt - post creation time +public type PostWithMeta record { + int id; + string content; + string createdAt; +}; + +# Represents a post forbidden error +# +# + body - error message +public type PostForbidden record {| + *http:Forbidden; + ErrorMessage body; +|}; + +# Represents a default response +# +# + body - response body +public type DefaultResponse record {| + *http:DefaultStatusCodeResponse; + ErrorMessage body; +|}; + +# Represents a error message +public type ErrorMessage string; + +service Service on new http:Listener(9090) { + +} diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_40/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_40/Ballerina.toml new file mode 100644 index 0000000000..b7b8157477 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_40/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "http_test" +name = "sample_40" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_40/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_40/service.bal new file mode 100644 index 0000000000..6cfcbdb41c --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_40/service.bal @@ -0,0 +1,74 @@ +import ballerina/http; + +type ContractServiceWithoutServiceConfig service object { + *http:ServiceContract; +}; + +@http:ServiceConfig { + basePath: "/api" +} +type ContractService service object { + *http:ServiceContract; +}; + +@http:ServiceConfig { +} +service ContractService /api on new http:Listener(9090) { +}; + +@http:ServiceConfig { + serviceType: ContractService +} +service ContractService on new http:Listener(9090) { +}; + +@http:ServiceConfig { + basePath: "/api" +} +type NonContractService service object { + *http:Service; +}; + +@http:ServiceConfig { + serviceType: ContractServiceWithoutServiceConfig +} +service ContractService on new http:Listener(9090) { +}; + +@http:ServiceConfig { + serviceType: ContractServiceWithoutServiceConfig +} +service NonContractService on new http:Listener(9090) { +}; + + +@http:ServiceConfig { + basePath: "/api" +} +type ContractServiceWithResource service object { + *http:ServiceContract; + + resource function get greeting(@http:Header string? header) returns string; +}; + +service ContractServiceWithResource on new http:Listener(9090) { + resource function get greeting(string? header) returns string { + return "Hello, World!"; + } +}; + +service ContractServiceWithResource on new http:Listener(9090) { + resource function get greeting(string? header) returns string { + return "Hello, World!"; + } + + resource function get newGreeting() {} +}; + +service ContractServiceWithResource on new http:Listener(9090) { + + @http:ResourceConfig {} + resource function get greeting(@http:Header string? header) returns string { + return "Hello, World!"; + } +}; diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_41/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_41/Ballerina.toml new file mode 100644 index 0000000000..78b28feb26 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_41/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "http_test" +name = "sample_41" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_41/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_41/service.bal new file mode 100644 index 0000000000..ce73b5ac41 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_41/service.bal @@ -0,0 +1,104 @@ +import ballerina/http; +import ballerina/log; + +@http:ServiceConfig { + mediaTypeSubtypePrefix: "vnd.socialMedia", + basePath: "/socialMedia" +} +public type Service service object { + *http:ServiceContract; + *http:InterceptableService; + + public function createInterceptors() returns ErrorInterceptor; + + resource function get users() returns @http:Cache {maxAge: 10} User[]|error; + + resource function get users/[int id]() returns User|UserNotFound|error; + + resource function post users(NewUser newUser) returns http:Created|error; +}; + +public isolated service class ErrorInterceptor { + *http:ResponseErrorInterceptor; + + isolated remote function interceptResponseError(error err, http:Response res, http:RequestContext ctx) returns DefaultResponse { + log:printError("error occurred", err); + return { + body: err.message(), + status: new (res.statusCode) + }; + } +} + +public type User record { + int id; + string name; + string email; +}; + +public type NewUser record { + string name; + string email; +}; + + +public type UserNotFound record {| + *http:NotFound; + ErrorMessage body; +|}; + +public type DefaultResponse record {| + *http:DefaultStatusCodeResponse; + ErrorMessage body; +|}; + +public type ErrorMessage string; + +service Service on new http:Listener(9090) { + + resource function get users() returns User[]|error { + return [{id: 1, name: "Alice", email: "alice@gmail.com"}, {id: 2, name: "Bob", email: "bob@gmail.com"}]; + } + + resource function get users/[int id]() returns User|UserNotFound|error { + return {id: 1, name: "Alice", email: "alice@gmail.com"}; + } + + resource function post users(NewUser newUser) returns http:Created|error { + return { + body: { + message: "User created successfully" + } + }; + } + + public function createInterceptors() returns ErrorInterceptor { + return new (); + } +} + +@http:ServiceConfig { + serviceType: Service +} +service Service on new http:Listener(9091) { + + resource function get users() returns User[]|error { + return [{id: 1, name: "Alice", email: "alice@gmail.com"}, {id: 2, name: "Bob", email: "bob@gmail.com"}]; + } + + resource function get users/[int id]() returns User|UserNotFound|error { + return {id: 1, name: "Alice", email: "alice@gmail.com"}; + } + + resource function post users(NewUser newUser) returns http:Created|error { + return { + body: { + message: "User created successfully" + } + }; + } + + public function createInterceptors() returns ErrorInterceptor { + return new (); + } +} diff --git a/compiler-plugin-tests/src/test/resources/codeaction/implement_service_contract_resources/result1.bal b/compiler-plugin-tests/src/test/resources/codeaction/implement_service_contract_resources/result1.bal new file mode 100644 index 0000000000..e132b5262f --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/codeaction/implement_service_contract_resources/result1.bal @@ -0,0 +1,210 @@ +// 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. + +// This is added to test some auto generated code segments. +// Please ignore the indentation. + +import ballerina/http; + +import ballerina/http; +import ballerina/log; + +# The service type that handles the social media API +@http:ServiceConfig { + mediaTypeSubtypePrefix: "vnd.socialMedia", + basePath: "/socialMedia" +} +public type Service service object { + *http:ServiceContract; + *http:InterceptableService; + + public function createInterceptors() returns ErrorInterceptor; + + # Get all users + # + # + return - List of users(`User[]`) or an error + @http:ResourceConfig { + name: "users" + } + resource function get users() returns @http:Cache {maxAge: 10} User[]|error; + + # Get a user by ID + # + # + id - User ID + # + return - `User` or NotFound response(`UserNotFound`) when the user is not found or an error + @http:ResourceConfig { + name: "user", + linkedTo: [ + {name: "user", method: http:DELETE, relation: "delete-user"}, + {name: "posts", method: http:POST, relation: "create-posts"}, + {name: "posts", method: http:GET, relation: "get-posts"} + ] + } + resource function get users/[int id]() returns User|UserNotFound|error; + + # Create a new user + # + # + newUser - New user details(`NewUser`) as payload + # + return - Created(`http:Created`) response or an error + @http:ResourceConfig { + name: "users", + linkedTo: [ + {name: "user", method: http:GET, relation: "get-user"}, + {name: "user", method: http:DELETE, relation: "delete-user"}, + {name: "posts", method: http:POST, relation: "create-posts"}, + {name: "posts", method: http:GET, relation: "get-posts"} + ], + consumes: ["application/vnd.socialMedia+json"] + } + resource function post users(NewUser newUser) returns http:Created|error; + + # Delete a user by ID + # + # + id - User ID + # + return - NoContent response(`http:NoContent`) or an error + @http:ResourceConfig { + name: "user" + } + resource function delete users/[int id]() returns http:NoContent|error; + + # Get all posts of a user + # + # + id - User ID + # + return - List of posts with metadata(`PostWithMeta[]`) or NotFound response(`UserNotFound`) when the user is not found or an error + @http:ResourceConfig { + name: "posts" + } + resource function get users/[int id]/posts() returns @http:Cache {maxAge: 25} PostWithMeta[]|UserNotFound|error; + + # Create a new post for a user + # + # + id - User ID + # + newPost - New post details(`NewPost`) as payload + # + return - Created(`http:Created`) response or an error + @http:ResourceConfig { + name: "posts", + linkedTo: [ + {name: "posts", method: http:POST, relation: "create-posts"} + ], + consumes: ["application/vnd.socialMedia+json"] + } + resource function post users/[int id]/posts(@http:Payload NewPost newPost) returns http:Created|UserNotFound|PostForbidden|error; +}; + +public isolated service class ErrorInterceptor { + *http:ResponseErrorInterceptor; + + isolated remote function interceptResponseError(error err, http:Response res, http:RequestContext ctx) returns DefaultResponse { + log:printError("error occurred", err); + return { + body: err.message(), + status: new (res.statusCode) + }; + } +} + +# Represents a user in the system +# +# + id - user ID +# + name - user name +# + email - user email +public type User record { + int id; + string name; + string email; +}; + +# Represents a new user +# +# + name - user name +# + email - user email +public type NewUser record { + string name; + string email; +}; + +# Represents a user not found error +# +# + body - error message +public type UserNotFound record {| + *http:NotFound; + ErrorMessage body; +|}; + +# Represents a new post +# +# + content - post content +public type NewPost record { + string content; +}; + +# Represents a post with metadata +# +# + id - post ID +# + content - post content +# + createdAt - post creation time +public type PostWithMeta record { + int id; + string content; + string createdAt; +}; + +# Represents a post forbidden error +# +# + body - error message +public type PostForbidden record {| + *http:Forbidden; + ErrorMessage body; +|}; + +# Represents a default response +# +# + body - response body +public type DefaultResponse record {| + *http:DefaultStatusCodeResponse; + ErrorMessage body; +|}; + +# Represents a error message +public type ErrorMessage string; + +service Service on new http:Listener(9090) { + + + resource function get users () returns User[]|error { + + } + + resource function get users/[int id] () returns User|UserNotFound|error { + + } + + resource function post users (NewUser newUser) returns http:Created|error { + + } + + resource function delete users/[int id] () returns http:NoContent|error { + + } + + resource function get users/[int id]/posts () returns PostWithMeta[]|UserNotFound|error { + + } + + resource function post users/[int id]/posts (NewPost newPost) returns http:Created|UserNotFound|PostForbidden|error { + + } +} diff --git a/compiler-plugin/build.gradle b/compiler-plugin/build.gradle index b529b6b1c2..8998d1b357 100644 --- a/compiler-plugin/build.gradle +++ b/compiler-plugin/build.gradle @@ -24,13 +24,22 @@ plugins { description = 'Ballerina - HTTP Compiler Plugin' +configurations { + externalJars +} + dependencies { checkstyle project(':checkstyle') checkstyle "com.puppycrawl.tools:checkstyle:${puppycrawlCheckstyleVersion}" + implementation group: 'io.swagger.core.v3', name: 'swagger-core', version: "${swaggerVersion}" + implementation group: 'io.swagger.core.v3', name: 'swagger-models', version: "${swaggerVersion}" implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" implementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}" implementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}" + implementation group: 'io.ballerina.openapi', name: 'ballerina-to-openapi', version: "${ballerinaToOpenApiVersion}" + + externalJars group: 'io.ballerina.openapi', name: 'ballerina-to-openapi', version: "${ballerinaToOpenApiVersion}" } def excludePattern = '**/module-info.java' @@ -68,3 +77,12 @@ compileJava { classpath = files() } } + +task copyOpenApiJar(type: Copy) { + from { + configurations.externalJars.collect { it } + } + into "${buildDir}/libs" +} + +build.dependsOn copyOpenApiJar diff --git a/compiler-plugin/spotbugs-exclude.xml b/compiler-plugin/spotbugs-exclude.xml index 3c56c8cc23..0bdc8fcdd7 100644 --- a/compiler-plugin/spotbugs-exclude.xml +++ b/compiler-plugin/spotbugs-exclude.xml @@ -42,13 +42,53 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java index 52f607f7e7..640e9c37d9 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/Constants.java @@ -26,6 +26,9 @@ private Constants() {} public static final String BALLERINA = "ballerina"; public static final String HTTP = "http"; + public static final String SERVICE_CONTRACT_TYPE = "ServiceContract"; + public static final String HTTP_SERVICE_TYPE = "Service"; + public static final String SERVICE_TYPE = "serviceType"; public static final String SERVICE_KEYWORD = "service"; public static final String REMOTE_KEYWORD = "remote"; public static final String RESOURCE_KEYWORD = "resource"; @@ -72,10 +75,13 @@ private Constants() {} public static final String OBJECT = "object"; public static final String HEADER_OBJ_NAME = "Headers"; public static final String PAYLOAD_ANNOTATION = "Payload"; + public static final String HEADER_ANNOTATION = "Header"; + public static final String QUERY_ANNOTATION = "Query"; + public static final String CALLER_ANNOTATION = "Caller"; public static final String CACHE_ANNOTATION = "Cache"; public static final String SERVICE_CONFIG_ANNOTATION = "ServiceConfig"; public static final String MEDIA_TYPE_SUBTYPE_PREFIX = "mediaTypeSubtypePrefix"; - public static final String INTERCEPTABLE_SERVICE = "InterceptableService"; + public static final String BASE_PATH = "basePath"; public static final String RESOURCE_CONFIG_ANNOTATION = "ResourceConfig"; public static final String PAYLOAD_ANNOTATION_TYPE = "HttpPayload"; public static final String CALLER_ANNOTATION_TYPE = "HttpCallerInfo"; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java index 38e59beadb..5936bfa6a1 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPlugin.java @@ -32,10 +32,12 @@ import io.ballerina.stdlib.http.compiler.codeaction.ChangeHeaderParamTypeToStringArrayCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.ChangeHeaderParamTypeToStringCodeAction; import io.ballerina.stdlib.http.compiler.codeaction.ChangeReturnTypeWithCallerCodeAction; +import io.ballerina.stdlib.http.compiler.codeaction.ImplementServiceContract; import io.ballerina.stdlib.http.compiler.codemodifier.HttpServiceModifier; import io.ballerina.stdlib.http.compiler.completion.HttpServiceBodyContextProvider; import java.util.List; +import java.util.Map; /** * The compiler plugin implementation for Ballerina Http package. @@ -44,8 +46,10 @@ public class HttpCompilerPlugin extends CompilerPlugin { @Override public void init(CompilerPluginContext context) { - context.addCodeModifier(new HttpServiceModifier()); - context.addCodeAnalyzer(new HttpServiceAnalyzer()); + Map ctxData = context.userData(); + ctxData.put("HTTP_CODE_MODIFIER_EXECUTED", false); + context.addCodeModifier(new HttpServiceModifier(ctxData)); + context.addCodeAnalyzer(new HttpServiceAnalyzer(ctxData)); getCodeActions().forEach(context::addCodeAction); getCompletionProviders().forEach(context::addCompletionProvider); } @@ -60,7 +64,8 @@ private List getCodeActions() { new AddResponseContentTypeCodeAction(), new AddResponseCacheConfigCodeAction(), new AddInterceptorResourceMethodCodeAction(), - new AddInterceptorRemoteMethodCodeAction() + new AddInterceptorRemoteMethodCodeAction(), + new ImplementServiceContract() ); } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPluginUtil.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPluginUtil.java index bb8d59bafa..2db9d66682 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPluginUtil.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpCompilerPluginUtil.java @@ -18,24 +18,33 @@ package io.ballerina.stdlib.http.compiler; +import io.ballerina.compiler.api.SemanticModel; import io.ballerina.compiler.api.Types; import io.ballerina.compiler.api.symbols.FunctionSymbol; import io.ballerina.compiler.api.symbols.FunctionTypeSymbol; import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; 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.api.symbols.TypeDefinitionSymbol; import io.ballerina.compiler.api.symbols.TypeDescKind; +import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.compiler.api.symbols.UnionTypeSymbol; import io.ballerina.compiler.syntax.tree.AnnotationNode; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.ReturnTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.tools.diagnostics.Diagnostic; import io.ballerina.tools.diagnostics.DiagnosticFactory; import io.ballerina.tools.diagnostics.DiagnosticInfo; import io.ballerina.tools.diagnostics.DiagnosticProperty; +import io.ballerina.tools.diagnostics.DiagnosticSeverity; import io.ballerina.tools.diagnostics.Location; import java.util.HashMap; @@ -59,6 +68,7 @@ import static io.ballerina.stdlib.http.compiler.Constants.FLOAT_ARRAY; import static io.ballerina.stdlib.http.compiler.Constants.HEADER_OBJ_NAME; import static io.ballerina.stdlib.http.compiler.Constants.HTTP; +import static io.ballerina.stdlib.http.compiler.Constants.HTTP_SERVICE_TYPE; import static io.ballerina.stdlib.http.compiler.Constants.INT; import static io.ballerina.stdlib.http.compiler.Constants.INTERCEPTOR_RESOURCE_RETURN_TYPE; import static io.ballerina.stdlib.http.compiler.Constants.INT_ARRAY; @@ -98,25 +108,25 @@ public final class HttpCompilerPluginUtil { private HttpCompilerPluginUtil() {} public static void updateDiagnostic(SyntaxNodeAnalysisContext ctx, Location location, - HttpDiagnosticCodes httpDiagnosticCodes) { + HttpDiagnostic httpDiagnosticCodes) { DiagnosticInfo diagnosticInfo = getDiagnosticInfo(httpDiagnosticCodes); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, location)); } public static void updateDiagnostic(SyntaxNodeAnalysisContext ctx, Location location, - HttpDiagnosticCodes httpDiagnosticCodes, Object... argName) { + HttpDiagnostic httpDiagnosticCodes, Object... argName) { DiagnosticInfo diagnosticInfo = getDiagnosticInfo(httpDiagnosticCodes, argName); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, location)); } public static void updateDiagnostic(SyntaxNodeAnalysisContext ctx, Location location, - HttpDiagnosticCodes httpDiagnosticCodes, + HttpDiagnostic httpDiagnosticCodes, List> diagnosticProperties, String argName) { DiagnosticInfo diagnosticInfo = getDiagnosticInfo(httpDiagnosticCodes, argName); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, location, diagnosticProperties)); } - public static DiagnosticInfo getDiagnosticInfo(HttpDiagnosticCodes diagnostic, Object... args) { + public static DiagnosticInfo getDiagnosticInfo(HttpDiagnostic diagnostic, Object... args) { return new DiagnosticInfo(diagnostic.getCode(), String.format(diagnostic.getMessage(), args), diagnostic.getSeverity()); } @@ -128,7 +138,7 @@ public static String getReturnTypeDescription(ReturnTypeDescriptorNode returnTyp public static void extractInterceptorReturnTypeAndValidate(SyntaxNodeAnalysisContext ctx, Map typeSymbols, FunctionDefinitionNode member, - HttpDiagnosticCodes httpDiagnosticCode) { + HttpDiagnostic httpDiagnosticCode) { Optional returnTypeDescriptorNode = member.functionSignature().returnTypeDesc(); if (returnTypeDescriptorNode.isEmpty()) { return; @@ -154,7 +164,7 @@ public static void extractInterceptorReturnTypeAndValidate(SyntaxNodeAnalysisCon public static void validateResourceReturnType(SyntaxNodeAnalysisContext ctx, Node node, Map typeSymbols, String returnTypeStringValue, - TypeSymbol returnTypeSymbol, HttpDiagnosticCodes diagnosticCode, + TypeSymbol returnTypeSymbol, HttpDiagnostic diagnosticCode, boolean isInterceptorType) { if (subtypeOf(typeSymbols, returnTypeSymbol, isInterceptorType ? INTERCEPTOR_RESOURCE_RETURN_TYPE : RESOURCE_RETURN_TYPE)) { @@ -200,16 +210,16 @@ public static TypeDescKind retrieveEffectiveTypeDesc(TypeSymbol descriptor) { } private static void reportInvalidReturnType(SyntaxNodeAnalysisContext ctx, Node node, - String returnType, HttpDiagnosticCodes diagnosticCode) { + String returnType, HttpDiagnostic diagnosticCode) { HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), diagnosticCode, returnType); } private static void reportReturnTypeAnnotationsAreNotAllowed(SyntaxNodeAnalysisContext ctx, Node node) { - HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_142); + HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_142); } public static void reportMissingParameterError(SyntaxNodeAnalysisContext ctx, Location location, String method) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_143, method); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_143, method); } public static String getNodeString(Node node, boolean isCaseSensitive) { @@ -309,4 +319,72 @@ private static void populateNilableBasicArrayTypes(Map typeS typeSymbols.put(NILABLE_MAP_OF_ANYDATA_ARRAY, types.builder().UNION_TYPE.withMemberTypes( typeSymbols.get(ARRAY_OF_MAP_OF_ANYDATA), types.NIL).build()); } + + public static boolean diagnosticContainsErrors(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { + List diagnostics = syntaxNodeAnalysisContext.semanticModel().diagnostics(); + return diagnostics.stream() + .anyMatch(d -> DiagnosticSeverity.ERROR.equals(d.diagnosticInfo().severity())); + } + + public static ServiceDeclarationNode getServiceDeclarationNode(SyntaxNodeAnalysisContext context) { + if (!(context.node() instanceof ServiceDeclarationNode serviceDeclarationNode)) { + return null; + } + return getServiceDeclarationNode(serviceDeclarationNode, context.semanticModel()); + } + + public static ServiceDeclarationNode getServiceDeclarationNode(Node node, SemanticModel semanticModel) { + if (!(node instanceof ServiceDeclarationNode serviceDeclarationNode)) { + return null; + } + + Optional serviceSymOptional = semanticModel.symbol(node); + if (serviceSymOptional.isPresent()) { + List listenerTypes = ((ServiceDeclarationSymbol) serviceSymOptional.get()).listenerTypes(); + if (listenerTypes.stream().noneMatch(HttpCompilerPluginUtil::isListenerBelongsToHttpModule)) { + return null; + } + } + return serviceDeclarationNode; + } + + private static boolean isListenerBelongsToHttpModule(TypeSymbol listenerType) { + if (listenerType.typeKind() == TypeDescKind.UNION) { + return ((UnionTypeSymbol) listenerType).memberTypeDescriptors().stream() + .filter(typeDescriptor -> typeDescriptor instanceof TypeReferenceTypeSymbol) + .map(typeReferenceTypeSymbol -> (TypeReferenceTypeSymbol) typeReferenceTypeSymbol) + .anyMatch(typeReferenceTypeSymbol -> isHttpModule(typeReferenceTypeSymbol.getModule().get())); + } + + if (listenerType.typeKind() == TypeDescKind.TYPE_REFERENCE) { + return isHttpModule(((TypeReferenceTypeSymbol) listenerType).typeDescriptor().getModule().get()); + } + return false; + } + + public static boolean isServiceObjectType(ObjectTypeDescriptorNode typeNode) { + return typeNode.objectTypeQualifiers().stream().anyMatch( + qualifier -> qualifier.kind().equals(SyntaxKind.SERVICE_KEYWORD)); + } + + public static boolean isHttpServiceType(SemanticModel semanticModel, Node typeNode) { + if (!(typeNode instanceof ObjectTypeDescriptorNode serviceObjType) || !isServiceObjectType(serviceObjType)) { + return false; + } + + Optional serviceObjSymbol = semanticModel.symbol(serviceObjType.parent()); + if (serviceObjSymbol.isEmpty() || + (!(serviceObjSymbol.get() instanceof TypeDefinitionSymbol serviceObjTypeDef))) { + return false; + } + + Optional serviceContractType = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, + HTTP_SERVICE_TYPE); + if (serviceContractType.isEmpty() || + !(serviceContractType.get() instanceof TypeDefinitionSymbol serviceContractTypeDef)) { + return false; + } + + return serviceObjTypeDef.typeDescriptor().subtypeOf(serviceContractTypeDef.typeDescriptor()); + } } 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/HttpDiagnostic.java similarity index 79% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnosticCodes.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpDiagnostic.java index 673fecf3bd..6beff9f045 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/HttpDiagnostic.java @@ -24,11 +24,12 @@ import static io.ballerina.stdlib.http.compiler.Constants.ALLOWED_RETURN_UNION; import static io.ballerina.tools.diagnostics.DiagnosticSeverity.ERROR; import static io.ballerina.tools.diagnostics.DiagnosticSeverity.INTERNAL; +import static io.ballerina.tools.diagnostics.DiagnosticSeverity.WARNING; /** * {@code DiagnosticCodes} is used to hold diagnostic codes. */ -public enum HttpDiagnosticCodes { +public enum HttpDiagnostic { 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), @@ -107,17 +108,38 @@ public enum HttpDiagnosticCodes { HTTP_151("HTTP_151", "ambiguous types for parameter '%s' and '%s'. Use annotations to avoid ambiguity", ERROR), HTTP_152("HTTP_152", "invalid union type for default payload param: '%s'. Use basic structured anydata types", ERROR), + HTTP_153("HTTP_153", "'http:ServiceConfig' annotation is not allowed for service declaration implemented via the " + + "'http:ServiceContract' type. The HTTP annotations are inferred from the service contract type", ERROR), + HTTP_154("HTTP_154", "base path not allowed in the service declaration which is implemented via the " + + "'http:ServiceContract' type. The base path is inferred from the service contract type", ERROR), + HTTP_155("HTTP_155", "configuring base path in the 'http:ServiceConfig' annotation is not allowed for non service" + + " contract types", ERROR), + HTTP_156("HTTP_156", "invalid service type descriptor found in 'http:ServiceConfig' annotation. " + + "Expected service type: '%s' but found: '%s'", ERROR), + HTTP_157("HTTP_157", "'serviceType' is not allowed in the service which is not implemented " + + "via the 'http:ServiceContract' type", ERROR), + HTTP_158("HTTP_158", "resource function which is not defined in the service contract type: '%s'," + + " is not allowed", ERROR), + HTTP_159("HTTP_159", "'http:ResourceConfig' annotation is not allowed for resource function implemented via the " + + "'http:ServiceContract' type. The HTTP annotations are inferred from the service contract type", ERROR), + HTTP_160("HTTP_160", "'%s' annotation is not allowed for resource function implemented via the " + + "'http:ServiceContract' type. The HTTP annotations are inferred from the service contract type", ERROR), + + HTTP_WARNING_101("HTTP_WARNING_101", "generated open-api definition is empty due to the errors " + + "in the generation", WARNING), + HTTP_WARNING_102("HTTP_WARNING_102", "The openapi definition is overridden by the `embed: true` option", WARNING), HTTP_HINT_101("HTTP_HINT_101", "Payload annotation can be added", INTERNAL), HTTP_HINT_102("HTTP_HINT_102", "Header annotation can be added", INTERNAL), HTTP_HINT_103("HTTP_HINT_103", "Response content-type can be added", INTERNAL), - HTTP_HINT_104("HTTP_HINT_104", "Response cache configuration can be added", INTERNAL); + HTTP_HINT_104("HTTP_HINT_104", "Response cache configuration can be added", INTERNAL), + HTTP_HINT_105("HTTP_HINT_105", "Service contract: '%s', can be implemented", INTERNAL); private final String code; private final String message; private final DiagnosticSeverity severity; - HttpDiagnosticCodes(String code, String message, DiagnosticSeverity severity) { + HttpDiagnostic(String code, String message, DiagnosticSeverity severity) { this.code = code; this.message = message; this.severity = severity; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorResourceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorResourceValidator.java index 7c54ae8bc2..950c5973a0 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorResourceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorResourceValidator.java @@ -44,10 +44,11 @@ public static void validateResource(SyntaxNodeAnalysisContext ctx, FunctionDefin if (isRequestErrorInterceptor(type)) { extractAndValidateMethodAndPath(ctx, member); } - HttpResourceValidator.extractInputParamTypeAndValidate(ctx, member, isRequestErrorInterceptor(type), + ResourceFunction functionNode = new ResourceFunctionDefinition(member); + HttpResourceValidator.extractInputParamTypeAndValidate(ctx, functionNode, isRequestErrorInterceptor(type), typeSymbols); HttpCompilerPluginUtil.extractInterceptorReturnTypeAndValidate(ctx, typeSymbols, member, - HttpDiagnosticCodes.HTTP_126); + HttpDiagnostic.HTTP_126); } private static boolean isRequestErrorInterceptor(String type) { @@ -78,16 +79,16 @@ private static void checkResourceAnnotation(SyntaxNodeAnalysisContext ctx, } private static void reportResourceAnnotationNotAllowed(SyntaxNodeAnalysisContext ctx, AnnotationNode node) { - updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_125, + updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_125, node.annotReference().toString()); } private static void reportInvalidResourcePath(SyntaxNodeAnalysisContext ctx, Node node) { - updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_127, node.toString()); + updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_127, node.toString()); } private static void reportInvalidResourceMethod(SyntaxNodeAnalysisContext ctx, IdentifierToken identifierToken) { - updateDiagnostic(ctx, identifierToken.location(), HttpDiagnosticCodes.HTTP_128, + updateDiagnostic(ctx, identifierToken.location(), HttpDiagnostic.HTTP_128, identifierToken.toString().strip()); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorServiceValidator.java index f58f856757..eb2290741b 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpInterceptorServiceValidator.java @@ -218,7 +218,7 @@ private static void validateRemoteMethod(SyntaxNodeAnalysisContext ctx, Function String type, Map typeSymbols) { validateInputParamType(ctx, member, type, typeSymbols); HttpCompilerPluginUtil.extractInterceptorReturnTypeAndValidate(ctx, typeSymbols, member, - HttpDiagnosticCodes.HTTP_141); + HttpDiagnostic.HTTP_141); } private static void validateInputParamType(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, @@ -251,19 +251,19 @@ private static void validateInputParamType(SyntaxNodeAnalysisContext ctx, Functi TypeSymbol typeSymbol = param.typeDescriptor(); if (subtypeOf(typeSymbols, typeSymbol, CALLER_OBJ_NAME)) { callerPresent = isObjectPresent(ctx, paramLocation, callerPresent, paramName, - HttpDiagnosticCodes.HTTP_115); + HttpDiagnostic.HTTP_115); } else if (subtypeOf(typeSymbols, typeSymbol, REQUEST_OBJ_NAME)) { requestPresent = isObjectPresent(ctx, paramLocation, requestPresent, paramName, - HttpDiagnosticCodes.HTTP_116); + HttpDiagnostic.HTTP_116); } else if (subtypeOf(typeSymbols, typeSymbol, RESPONSE_OBJ_NAME)) { responsePresent = isObjectPresent(ctx, paramLocation, responsePresent, paramName, - HttpDiagnosticCodes.HTTP_139); + HttpDiagnostic.HTTP_139); } else if (subtypeOf(typeSymbols, typeSymbol, REQUEST_CONTEXT_OBJ_NAME)) { requestCtxPresent = isObjectPresent(ctx, paramLocation, requestCtxPresent, paramName, - HttpDiagnosticCodes.HTTP_121); + HttpDiagnostic.HTTP_121); } else if (isResponseErrorInterceptor(type) && kind == TypeDescKind.ERROR) { errorPresent = isObjectPresent(ctx, paramLocation, errorPresent, paramName, - HttpDiagnosticCodes.HTTP_122); + HttpDiagnostic.HTTP_122); } else { reportInvalidParameterType(ctx, paramLocation, paramType, isResponseErrorInterceptor(type)); } @@ -274,7 +274,7 @@ private static void validateInputParamType(SyntaxNodeAnalysisContext ctx, Functi } private static boolean isObjectPresent(SyntaxNodeAnalysisContext ctx, Location location, - boolean objectPresent, String paramName, HttpDiagnosticCodes code) { + boolean objectPresent, String paramName, HttpDiagnostic code) { if (objectPresent) { HttpCompilerPluginUtil.updateDiagnostic(ctx, location, code, paramName); } @@ -285,44 +285,44 @@ private static void reportInvalidParameterType(SyntaxNodeAnalysisContext ctx, Lo String typeName, boolean isResponseErrorInterceptor) { String functionName = isResponseErrorInterceptor ? Constants.INTERCEPT_RESPONSE_ERROR : Constants.INTERCEPT_RESPONSE; - HttpCompilerPluginUtil.updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_140, typeName, functionName); + HttpCompilerPluginUtil.updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_140, typeName, functionName); } private static void reportMultipleReferencesFound(SyntaxNodeAnalysisContext ctx, TypeReferenceNode node) { - HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_123, + HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_123, node.typeName().toString()); } private static void reportMultipleResourceFunctionsFound(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode node) { - DiagnosticInfo diagnosticInfo = new DiagnosticInfo(HttpDiagnosticCodes.HTTP_124.getCode(), - HttpDiagnosticCodes.HTTP_124.getMessage(), HttpDiagnosticCodes.HTTP_124.getSeverity()); + DiagnosticInfo diagnosticInfo = new DiagnosticInfo(HttpDiagnostic.HTTP_124.getCode(), + HttpDiagnostic.HTTP_124.getMessage(), HttpDiagnostic.HTTP_124.getSeverity()); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, node.location())); } private static void reportResourceFunctionNotFound(SyntaxNodeAnalysisContext ctx, String type) { - HttpCompilerPluginUtil.updateDiagnostic(ctx, ctx.node().location(), HttpDiagnosticCodes.HTTP_132, type); + HttpCompilerPluginUtil.updateDiagnostic(ctx, ctx.node().location(), HttpDiagnostic.HTTP_132, type); } private static void reportRemoteFunctionNotFound(SyntaxNodeAnalysisContext ctx, String type) { String requiredFunctionName = isResponseErrorInterceptor(type) ? Constants.INTERCEPT_RESPONSE_ERROR : Constants.INTERCEPT_RESPONSE; - DiagnosticInfo diagnosticInfo = HttpCompilerPluginUtil.getDiagnosticInfo(HttpDiagnosticCodes.HTTP_135, + DiagnosticInfo diagnosticInfo = HttpCompilerPluginUtil.getDiagnosticInfo(HttpDiagnostic.HTTP_135, type, requiredFunctionName); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, ctx.node().location())); } private static void reportResourceFunctionNotAllowed(SyntaxNodeAnalysisContext ctx, Node node, String type) { - HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_136, type); + HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_136, type); } private static void reportRemoteFunctionNotAllowed(SyntaxNodeAnalysisContext ctx, Node node, String type) { - HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_137, type); + HttpCompilerPluginUtil.updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_137, type); } private static void reportInvalidRemoteFunction(SyntaxNodeAnalysisContext ctx, Node node, String functionName, String interceptorType, String requiredFunctionName) { - DiagnosticInfo diagnosticInfo = HttpCompilerPluginUtil.getDiagnosticInfo(HttpDiagnosticCodes.HTTP_138, + DiagnosticInfo diagnosticInfo = HttpCompilerPluginUtil.getDiagnosticInfo(HttpDiagnostic.HTTP_138, functionName, interceptorType, requiredFunctionName); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, node.location())); } 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 77d40558c7..d2fac3e9a8 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 @@ -43,6 +43,7 @@ import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; import io.ballerina.compiler.syntax.tree.NameReferenceNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; @@ -55,8 +56,8 @@ import io.ballerina.compiler.syntax.tree.SpecificFieldNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; -import io.ballerina.stdlib.http.compiler.codemodifier.context.ParamAvailability; -import io.ballerina.stdlib.http.compiler.codemodifier.context.ParamData; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.PayloadParamAvailability; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.PayloadParamData; import io.ballerina.tools.diagnostics.Location; import org.wso2.ballerinalang.compiler.diagnostic.properties.BSymbolicProperty; @@ -122,8 +123,8 @@ import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.retrieveEffectiveTypeDesc; import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.subtypeOf; import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.updateDiagnostic; -import static io.ballerina.stdlib.http.compiler.codemodifier.HttpPayloadParamIdentifier.validateAnnotatedParams; -import static io.ballerina.stdlib.http.compiler.codemodifier.HttpPayloadParamIdentifier.validateNonAnnotatedParams; +import static io.ballerina.stdlib.http.compiler.codemodifier.payload.HttpPayloadParamIdentifier.validateAnnotatedParams; +import static io.ballerina.stdlib.http.compiler.codemodifier.payload.HttpPayloadParamIdentifier.validateNonAnnotatedParams; /** * Validates a ballerina http resource. @@ -134,14 +135,23 @@ private HttpResourceValidator() {} static void validateResource(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, LinksMetaData linksMetaData, Map typeSymbols) { - extractResourceAnnotationAndValidate(ctx, member, linksMetaData); - extractInputParamTypeAndValidate(ctx, member, false, typeSymbols); - extractReturnTypeAndValidate(ctx, member, typeSymbols); + ResourceFunction functionNode = new ResourceFunctionDefinition(member); + extractResourceAnnotationAndValidate(ctx, functionNode, linksMetaData); + extractInputParamTypeAndValidate(ctx, functionNode, false, typeSymbols); + extractReturnTypeAndValidate(ctx, functionNode, typeSymbols); validateHttpCallerUsage(ctx, member); } + static void validateResource(SyntaxNodeAnalysisContext ctx, MethodDeclarationNode member, + LinksMetaData linksMetaData, Map typeSymbols) { + ResourceFunction functionNode = new ResourceFunctionDeclaration(member); + extractResourceAnnotationAndValidate(ctx, functionNode, linksMetaData); + extractInputParamTypeAndValidate(ctx, functionNode, false, typeSymbols); + extractReturnTypeAndValidate(ctx, functionNode, typeSymbols); + } + private static void extractResourceAnnotationAndValidate(SyntaxNodeAnalysisContext ctx, - FunctionDefinitionNode member, + ResourceFunction member, LinksMetaData linksMetaData) { Optional metadataNodeOptional = member.metadata(); if (metadataNodeOptional.isEmpty()) { @@ -160,7 +170,7 @@ private static void extractResourceAnnotationAndValidate(SyntaxNodeAnalysisConte } } - private static void validateLinksInResourceConfig(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + private static void validateLinksInResourceConfig(SyntaxNodeAnalysisContext ctx, ResourceFunction member, AnnotationNode annotation, LinksMetaData linksMetaData) { Optional optionalMapping = annotation.annotValue(); if (optionalMapping.isEmpty()) { @@ -183,7 +193,7 @@ private static void validateLinksInResourceConfig(SyntaxNodeAnalysisContext ctx, } } - private static void validateResourceNameField(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + private static void validateResourceNameField(SyntaxNodeAnalysisContext ctx, ResourceFunction member, SpecificFieldNode field, LinksMetaData linksMetaData) { Optional fieldValueExpression = field.valueExpr(); if (fieldValueExpression.isEmpty()) { @@ -204,7 +214,7 @@ private static void validateResourceNameField(SyntaxNodeAnalysisContext ctx, Fun } } - private static String getRelativePathFromFunctionNode(FunctionDefinitionNode member) { + private static String getRelativePathFromFunctionNode(ResourceFunction member) { NodeList nodes = member.relativeResourcePath(); String path = EMPTY; for (Node node : nodes) { @@ -280,7 +290,7 @@ private static void populateLinkedToResources(SyntaxNodeAnalysisContext ctx, } } - public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, ResourceFunction member, boolean isErrorInterceptor, Map typeSymbols) { boolean callerPresent = false; @@ -290,7 +300,7 @@ public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ct boolean errorPresent = false; boolean payloadAnnotationPresent = false; boolean headerAnnotationPresent = false; - Optional resourceMethodSymbolOptional = ctx.semanticModel().symbol(member); + Optional resourceMethodSymbolOptional = member.getSymbol(ctx.semanticModel()); Location paramLocation = member.location(); if (resourceMethodSymbolOptional.isEmpty()) { return; @@ -332,22 +342,22 @@ public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ct if (kind == TypeDescKind.ERROR) { errorPresent = isObjectPresent(ctx, paramLocation, errorPresent, paramName, - HttpDiagnosticCodes.HTTP_122); + HttpDiagnostic.HTTP_122); } else if (subtypeOf(typeSymbols, typeSymbol, OBJECT)) { if (kind == TypeDescKind.INTERSECTION) { reportInvalidIntersectionObjectType(ctx, paramLocation, paramName, typeName); } else if (subtypeOf(typeSymbols, typeSymbol, CALLER_OBJ_NAME)) { callerPresent = isObjectPresent(ctx, paramLocation, callerPresent, paramName, - HttpDiagnosticCodes.HTTP_115); + HttpDiagnostic.HTTP_115); } else if (subtypeOf(typeSymbols, typeSymbol, REQUEST_OBJ_NAME)) { requestPresent = isObjectPresent(ctx, paramLocation, requestPresent, paramName, - HttpDiagnosticCodes.HTTP_116); + HttpDiagnostic.HTTP_116); } else if (subtypeOf(typeSymbols, typeSymbol, REQUEST_CONTEXT_OBJ_NAME)) { requestCtxPresent = isObjectPresent(ctx, paramLocation, requestCtxPresent, paramName, - HttpDiagnosticCodes.HTTP_121); + HttpDiagnostic.HTTP_121); } else if (subtypeOf(typeSymbols, typeSymbol, HEADER_OBJ_NAME)) { headersPresent = isObjectPresent(ctx, paramLocation, headersPresent, paramName, - HttpDiagnosticCodes.HTTP_117); + HttpDiagnostic.HTTP_117); } else { reportInvalidParameterType(ctx, paramLocation, paramType); } @@ -420,10 +430,13 @@ public static void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ct annotated = true; if (subtypeOf(typeSymbols, typeDescriptor, CALLER_OBJ_NAME)) { if (callerPresent) { - updateDiagnostic(ctx, paramLocation, HttpDiagnosticCodes.HTTP_115, paramName); + updateDiagnostic(ctx, paramLocation, HttpDiagnostic.HTTP_115, paramName); } else { callerPresent = true; - extractCallerInfoValueAndValidate(ctx, member, paramIndex); + Optional functionDefNode = member.getFunctionDefinitionNode(); + if (functionDefNode.isPresent()) { + extractCallerInfoValueAndValidate(ctx, functionDefNode.get(), paramIndex); + } } } else { reportInvalidCallerParameterType(ctx, paramLocation, paramName); @@ -487,30 +500,30 @@ private static void validatePathParam(SyntaxNodeAnalysisContext ctx, Map mockCodeModifier(SyntaxNodeAnalysisContext ctx, Map typeSymbols, Optional> parametersOptional) { - List nonAnnotatedParams = new ArrayList<>(); - List annotatedParams = new ArrayList<>(); + List nonAnnotatedParams = new ArrayList<>(); + List annotatedParams = new ArrayList<>(); List analyzedParams = new ArrayList<>(); - ParamAvailability paramAvailability = new ParamAvailability(); + PayloadParamAvailability paramAvailability = new PayloadParamAvailability(); int index = 0; for (ParameterSymbol param : parametersOptional.get()) { List annotations = param.annotations().stream() .filter(annotationSymbol -> annotationSymbol.typeDescriptor().isPresent()) .collect(Collectors.toList()); if (annotations.isEmpty()) { - nonAnnotatedParams.add(new ParamData(param, index++)); + nonAnnotatedParams.add(new PayloadParamData(param, index++)); } else { - annotatedParams.add(new ParamData(param, index++)); + annotatedParams.add(new PayloadParamData(param, index++)); } } - for (ParamData annotatedParam : annotatedParams) { + for (PayloadParamData annotatedParam : annotatedParams) { validateAnnotatedParams(annotatedParam.getParameterSymbol(), paramAvailability); if (paramAvailability.isAnnotatedPayloadParam()) { return analyzedParams; } } - for (ParamData nonAnnotatedParam : nonAnnotatedParams) { + for (PayloadParamData nonAnnotatedParam : nonAnnotatedParams) { ParameterSymbol parameterSymbol = nonAnnotatedParam.getParameterSymbol(); if (validateNonAnnotatedParams(ctx, parameterSymbol.typeDescriptor(), paramAvailability, @@ -694,23 +707,23 @@ private static void validateRecordFieldsOfHeaderParam(SyntaxNodeAnalysisContext private static void enableAddPayloadParamCodeAction(SyntaxNodeAnalysisContext ctx, Location location, String methodName) { if (!methodName.equals(GET) && !methodName.equals(HEAD) && !methodName.equals(OPTIONS)) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_HINT_101); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_HINT_101); } } private static void enableAddHeaderParamCodeAction(SyntaxNodeAnalysisContext ctx, Location location) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_HINT_102); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_HINT_102); } private static void validatePayloadAnnotationUsage(SyntaxNodeAnalysisContext ctx, Location location, String methodName) { if (methodName.equals(GET) || methodName.equals(HEAD) || methodName.equals(OPTIONS)) { - reportInvalidUsageOfPayloadAnnotation(ctx, location, methodName, HttpDiagnosticCodes.HTTP_129); + reportInvalidUsageOfPayloadAnnotation(ctx, location, methodName, HttpDiagnostic.HTTP_129); } } private static boolean isObjectPresent(SyntaxNodeAnalysisContext ctx, Location location, - boolean objectPresent, String paramName, HttpDiagnosticCodes code) { + boolean objectPresent, String paramName, HttpDiagnostic code) { if (objectPresent) { updateDiagnostic(ctx, location, code, paramName); } @@ -763,7 +776,7 @@ private static List getRespondParamNode(SyntaxNodeAnalys return respondNodeVisitor.getRespondStatementNodes(); } - private static void extractReturnTypeAndValidate(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + private static void extractReturnTypeAndValidate(SyntaxNodeAnalysisContext ctx, ResourceFunction member, Map typeSymbols) { Optional returnTypeDescriptorNode = member.functionSignature().returnTypeDesc(); if (returnTypeDescriptorNode.isEmpty()) { @@ -771,7 +784,7 @@ private static void extractReturnTypeAndValidate(SyntaxNodeAnalysisContext ctx, } Node returnTypeNode = returnTypeDescriptorNode.get().type(); String returnTypeStringValue = HttpCompilerPluginUtil.getReturnTypeDescription(returnTypeDescriptorNode.get()); - Optional functionSymbol = ctx.semanticModel().symbol(member); + Optional functionSymbol = member.getSymbol(ctx.semanticModel()); if (functionSymbol.isEmpty()) { return; } @@ -781,7 +794,7 @@ private static void extractReturnTypeAndValidate(SyntaxNodeAnalysisContext ctx, return; } HttpCompilerPluginUtil.validateResourceReturnType(ctx, returnTypeNode, typeSymbols, returnTypeStringValue, - returnTypeSymbol.get(), HttpDiagnosticCodes.HTTP_102, false); + returnTypeSymbol.get(), HttpDiagnostic.HTTP_102, false); validateAnnotationsAndEnableCodeActions(ctx, returnTypeNode, returnTypeSymbol.get(), returnTypeStringValue, returnTypeDescriptorNode.get()); } @@ -814,7 +827,7 @@ private static void validateAnnotationsAndEnableCodeActions(SyntaxNodeAnalysisCo } else { if (payloadAnnotationPresent) { reportInvalidUsageOfPayloadAnnotation(ctx, returnTypeNode.location(), returnTypeString, - HttpDiagnosticCodes.HTTP_131); + HttpDiagnostic.HTTP_131); } if (cacheAnnotationPresent) { reportInvalidUsageOfCacheAnnotation(ctx, returnTypeNode.location(), returnTypeString); @@ -823,11 +836,11 @@ private static void validateAnnotationsAndEnableCodeActions(SyntaxNodeAnalysisCo } private static void enableConfigureReturnMediaTypeCodeAction(SyntaxNodeAnalysisContext ctx, Node node) { - updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_HINT_103); + updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_HINT_103); } private static void enableResponseCacheConfigCodeAction(SyntaxNodeAnalysisContext ctx, Node node) { - updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_HINT_104); + updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_HINT_104); } private static boolean checkForSupportedReturnTypes(TypeSymbol returnTypeSymbol) { @@ -887,7 +900,7 @@ public static void validateHttpCallerUsage(SyntaxNodeAnalysisContext ctx, Functi if (isValidReturnTypeWithCaller(typeSymbol)) { return; } - updateDiagnostic(ctx, returnTypeLocation, HttpDiagnosticCodes.HTTP_118, returnTypeDescription); + updateDiagnostic(ctx, returnTypeLocation, HttpDiagnostic.HTTP_118, returnTypeDescription); } public static boolean isHttpCaller(ParameterSymbol param) { @@ -921,91 +934,91 @@ private static boolean isValidReturnTypeWithCaller(TypeSymbol returnTypeDescript private static void reportInvalidParameter(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_105, paramName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_105, paramName); } private static void reportInvalidParameterType(SyntaxNodeAnalysisContext ctx, Location location, String typeName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_106, typeName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_106, typeName); } public static void reportInvalidPayloadParameterType(SyntaxNodeAnalysisContext ctx, Location location, String typeName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_107, typeName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_107, typeName); } private static void reportInvalidMultipleAnnotation(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_108, paramName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_108, paramName); } private static void reportInvalidHeaderParameterType(SyntaxNodeAnalysisContext ctx, Location location, String paramName, Symbol parameterSymbol) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_109, List.of(new BSymbolicProperty(parameterSymbol)) + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_109, List.of(new BSymbolicProperty(parameterSymbol)) , paramName); } private static void reportInvalidUnionHeaderType(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_110, paramName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_110, paramName); } private static void reportInvalidCallerParameterType(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_111, paramName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_111, paramName); } private static void reportInvalidQueryParameterType(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_112, paramName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_112, paramName); } private static void reportInvalidPathParameterType(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_145, paramName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_145, paramName); } private static void reportInvalidUnionQueryType(SyntaxNodeAnalysisContext ctx, Location location, String paramName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_113, paramName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_113, paramName); } private static void reportInCompatibleCallerInfoType(SyntaxNodeAnalysisContext ctx, PositionalArgumentNode node, String paramName) { - updateDiagnostic(ctx, node.location(), HttpDiagnosticCodes.HTTP_114, paramName); + updateDiagnostic(ctx, node.location(), HttpDiagnostic.HTTP_114, paramName); } private static void reportInvalidUsageOfPayloadAnnotation(SyntaxNodeAnalysisContext ctx, Location location, - String name, HttpDiagnosticCodes code) { + String name, HttpDiagnostic code) { updateDiagnostic(ctx, location, code, name); } private static void reportInvalidUsageOfCacheAnnotation(SyntaxNodeAnalysisContext ctx, Location location, String returnType) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_130, returnType); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_130, returnType); } private static void reportInvalidIntersectionType(SyntaxNodeAnalysisContext ctx, Location location, String typeName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_133, typeName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_133, typeName); } private static void reportInvalidIntersectionObjectType(SyntaxNodeAnalysisContext ctx, Location location, String paramName, String typeName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_134, paramName, typeName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_134, paramName, typeName); } private static void reportInvalidHeaderRecordRestFieldType(SyntaxNodeAnalysisContext ctx, Location location) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_144); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_144); } private static void reportInvalidResourceName(SyntaxNodeAnalysisContext ctx, Location location, String resourceName) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_146, resourceName); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_146, resourceName); } private static void reportInvalidLinkRelation(SyntaxNodeAnalysisContext ctx, Location location, String relation) { - updateDiagnostic(ctx, location, HttpDiagnosticCodes.HTTP_147, relation); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_147, relation); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceAnalyzer.java index e7d1041ac2..a5c13ca0e9 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceAnalyzer.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceAnalyzer.java @@ -21,15 +21,35 @@ import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.projects.plugins.CodeAnalysisContext; import io.ballerina.projects.plugins.CodeAnalyzer; +import io.ballerina.stdlib.http.compiler.oas.ServiceContractOasGenerator; +import io.ballerina.stdlib.http.compiler.oas.ServiceOasGenerator; + +import java.util.Map; /** * The {@code CodeAnalyzer} for Ballerina Http services. */ public class HttpServiceAnalyzer extends CodeAnalyzer { + private final Map ctxData; + + public HttpServiceAnalyzer(Map ctxData) { + this.ctxData = ctxData; + } + @Override public void init(CodeAnalysisContext codeAnalysisContext) { + codeAnalysisContext.addSyntaxNodeAnalysisTask(new HttpServiceObjTypeAnalyzer(), SyntaxKind.OBJECT_TYPE_DESC); codeAnalysisContext.addSyntaxNodeAnalysisTask(new HttpServiceValidator(), SyntaxKind.SERVICE_DECLARATION); + + boolean httpCodeModifierExecuted = (boolean) ctxData.getOrDefault("HTTP_CODE_MODIFIER_EXECUTED", false); + if (httpCodeModifierExecuted) { + codeAnalysisContext.addSyntaxNodeAnalysisTask(new ServiceContractOasGenerator(), + SyntaxKind.OBJECT_TYPE_DESC); + codeAnalysisContext.addSyntaxNodeAnalysisTask(new ServiceOasGenerator(), + SyntaxKind.SERVICE_DECLARATION); + } + codeAnalysisContext.addSyntaxNodeAnalysisTask(new HttpInterceptorServiceValidator(), - SyntaxKind.CLASS_DEFINITION); + SyntaxKind.CLASS_DEFINITION); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceContractResourceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceContractResourceValidator.java new file mode 100644 index 0000000000..203292cbd5 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceContractResourceValidator.java @@ -0,0 +1,207 @@ +/* + * 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.stdlib.http.compiler; + +import io.ballerina.compiler.api.symbols.ResourceMethodSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.resourcepath.PathSegmentList; +import io.ballerina.compiler.api.symbols.resourcepath.ResourcePath; +import io.ballerina.compiler.api.symbols.resourcepath.util.PathSegment; +import io.ballerina.compiler.syntax.tree.AbstractNodeFactory; +import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.IncludedRecordParameterNode; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ParameterNode; +import io.ballerina.compiler.syntax.tree.RequiredParameterNode; +import io.ballerina.compiler.syntax.tree.RestParameterNode; +import io.ballerina.compiler.syntax.tree.ReturnTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.SeparatedNodeList; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.tools.diagnostics.Location; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static io.ballerina.stdlib.http.compiler.Constants.CACHE_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.CALLER_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.COLON; +import static io.ballerina.stdlib.http.compiler.Constants.HEADER_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.HTTP; +import static io.ballerina.stdlib.http.compiler.Constants.PAYLOAD_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.QUERY_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.RESOURCE_CONFIG_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.updateDiagnostic; + +/** + * Validates a ballerina http resource implemented via the service contract type. + * + * @since 2.12.0 + */ +public final class HttpServiceContractResourceValidator { + + private HttpServiceContractResourceValidator() { + } + + public static void validateResource(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, + Set resourcesFromServiceType, String serviceTypeName) { + Optional functionDefinitionSymbol = ctx.semanticModel().symbol(member); + if (functionDefinitionSymbol.isEmpty() || + !(functionDefinitionSymbol.get() instanceof ResourceMethodSymbol resourceMethodSymbol)) { + return; + } + + ResourcePath resourcePath = resourceMethodSymbol.resourcePath(); + String resourceName = resourceMethodSymbol.getName().orElse("") + " " + constructResourcePathName(resourcePath); + if (!resourcesFromServiceType.contains(resourceName)) { + reportResourceFunctionNotAllowed(ctx, serviceTypeName, member.location()); + } + + validateAnnotationUsages(ctx, member); + } + + public static void validateAnnotationUsages(SyntaxNodeAnalysisContext ctx, + FunctionDefinitionNode resourceFunction) { + validateAnnotationUsagesOnResourceFunction(ctx, resourceFunction); + validateAnnotationUsagesOnInputParams(ctx, resourceFunction); + validateAnnotationUsagesOnReturnType(ctx, resourceFunction); + } + + public static void validateAnnotationUsagesOnResourceFunction(SyntaxNodeAnalysisContext ctx, + FunctionDefinitionNode resourceFunction) { + Optional metadataNodeOptional = resourceFunction.metadata(); + if (metadataNodeOptional.isEmpty()) { + return; + } + NodeList annotations = metadataNodeOptional.get().annotations(); + for (AnnotationNode annotation : annotations) { + Node annotReference = annotation.annotReference(); + String annotName = annotReference.toString(); + if (annotReference.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE) { + String[] annotStrings = annotName.split(Constants.COLON); + if (RESOURCE_CONFIG_ANNOTATION.equals(annotStrings[annotStrings.length - 1].trim()) + && HTTP.equals(annotStrings[0].trim())) { + reportResourceConfigAnnotationNotAllowed(ctx, annotation.location()); + } + } + } + } + + public static void validateAnnotationUsagesOnInputParams(SyntaxNodeAnalysisContext ctx, + FunctionDefinitionNode resourceFunction) { + SeparatedNodeList parameters = resourceFunction.functionSignature().parameters(); + for (ParameterNode parameter : parameters) { + NodeList annotations = getAnnotationsFromParameter(parameter); + for (AnnotationNode annotation : annotations) { + Node annotReference = annotation.annotReference(); + String annotName = annotReference.toString(); + if (annotReference.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE) { + String[] annotationStrings = annotName.split(COLON); + String annotationName = annotationStrings[annotationStrings.length - 1].trim(); + if (HTTP.equals(annotationStrings[0].trim()) && + (annotationName.equals(PAYLOAD_ANNOTATION) || annotationName.equals(HEADER_ANNOTATION) || + annotationName.equals(QUERY_ANNOTATION) || annotationName.equals(CALLER_ANNOTATION))) { + reportAnnotationNotAllowed(ctx, annotation.location(), HTTP + COLON + annotationName); + } + } + } + } + } + + public static NodeList getAnnotationsFromParameter(ParameterNode parameter) { + if (parameter instanceof RequiredParameterNode parameterNode) { + return parameterNode.annotations(); + } else if (parameter instanceof DefaultableParameterNode parameterNode) { + return parameterNode.annotations(); + } else if (parameter instanceof IncludedRecordParameterNode parameterNode) { + return parameterNode.annotations(); + } else if (parameter instanceof RestParameterNode parameterNode) { + return parameterNode.annotations(); + } else { + return AbstractNodeFactory.createEmptyNodeList(); + } + } + + public static void validateAnnotationUsagesOnReturnType(SyntaxNodeAnalysisContext ctx, + FunctionDefinitionNode resourceFunction) { + Optional returnTypeDescriptorNode = resourceFunction.functionSignature(). + returnTypeDesc(); + if (returnTypeDescriptorNode.isEmpty()) { + return; + } + + NodeList annotations = returnTypeDescriptorNode.get().annotations(); + for (AnnotationNode annotation : annotations) { + Node annotReference = annotation.annotReference(); + String annotName = annotReference.toString(); + if (annotReference.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE) { + String[] annotationStrings = annotName.split(COLON); + String annotationName = annotationStrings[annotationStrings.length - 1].trim(); + if (HTTP.equals(annotationStrings[0].trim()) && + (annotationName.equals(PAYLOAD_ANNOTATION) || annotationName.equals(CACHE_ANNOTATION))) { + reportAnnotationNotAllowed(ctx, annotation.location(), HTTP + COLON + annotationName); + } + } + } + } + + public static String constructResourcePathName(ResourcePath resourcePath) { + return switch (resourcePath.kind()) { + case DOT_RESOURCE_PATH -> "."; + case PATH_SEGMENT_LIST -> constructResourcePathNameFromSegList((PathSegmentList) resourcePath); + default -> "^^"; + }; + } + + public static String constructResourcePathNameFromSegList(PathSegmentList pathSegmentList) { + List resourcePaths = new ArrayList<>(); + for (PathSegment pathSegment : pathSegmentList.list()) { + switch (pathSegment.pathSegmentKind()) { + case NAMED_SEGMENT: + resourcePaths.add(pathSegment.getName().orElse("")); + break; + case PATH_PARAMETER: + resourcePaths.add("^"); + break; + default: + resourcePaths.add("^^"); + } + } + return resourcePaths.isEmpty() ? "" : String.join("/", resourcePaths); + } + + private static void reportResourceFunctionNotAllowed(SyntaxNodeAnalysisContext ctx, String serviceContractType, + Location location) { + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_158, serviceContractType); + } + + private static void reportResourceConfigAnnotationNotAllowed(SyntaxNodeAnalysisContext ctx, Location location) { + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_159); + } + + private static void reportAnnotationNotAllowed(SyntaxNodeAnalysisContext ctx, Location location, + String annotationName) { + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_160, annotationName); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceObjTypeAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceObjTypeAnalyzer.java new file mode 100644 index 0000000000..38f611dd7b --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceObjTypeAnalyzer.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.stdlib.http.compiler; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; + +import java.util.Optional; + +import static io.ballerina.stdlib.http.compiler.Constants.BALLERINA; +import static io.ballerina.stdlib.http.compiler.Constants.EMPTY; +import static io.ballerina.stdlib.http.compiler.Constants.HTTP; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONTRACT_TYPE; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.diagnosticContainsErrors; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.isHttpServiceType; + +/** + * Validates the HTTP service object type. + * + * @since 2.12.0 + */ +public class HttpServiceObjTypeAnalyzer extends HttpServiceValidator { + + @Override + public void perform(SyntaxNodeAnalysisContext context) { + if (diagnosticContainsErrors(context)) { + return; + } + + Node typeNode = context.node(); + if (!isHttpServiceType(context.semanticModel(), typeNode)) { + return; + } + + ObjectTypeDescriptorNode serviceObjectType = (ObjectTypeDescriptorNode) typeNode; + Optional metadataNodeOptional = ((TypeDefinitionNode) serviceObjectType.parent()).metadata(); + metadataNodeOptional.ifPresent(metadataNode -> validateServiceAnnotation(context, metadataNode, null, + isServiceContractType(context.semanticModel(), serviceObjectType))); + + NodeList members = serviceObjectType.members(); + validateResources(context, members); + } + + private static boolean isServiceContractType(SemanticModel semanticModel, + ObjectTypeDescriptorNode serviceObjType) { + Optional serviceObjSymbol = semanticModel.symbol(serviceObjType.parent()); + if (serviceObjSymbol.isEmpty() || + (!(serviceObjSymbol.get() instanceof TypeDefinitionSymbol serviceObjTypeDef))) { + return false; + } + + Optional serviceContractType = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, + SERVICE_CONTRACT_TYPE); + if (serviceContractType.isEmpty() || + !(serviceContractType.get() instanceof TypeDefinitionSymbol serviceContractTypeDef)) { + return false; + } + + return serviceObjTypeDef.typeDescriptor().subtypeOf(serviceContractTypeDef.typeDescriptor()); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceValidator.java index 4e1caa9dad..2cacde8bbc 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/HttpServiceValidator.java @@ -18,52 +18,62 @@ package io.ballerina.stdlib.http.compiler; -import io.ballerina.compiler.api.symbols.ServiceDeclarationSymbol; +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.ObjectTypeSymbol; import io.ballerina.compiler.api.symbols.Symbol; -import io.ballerina.compiler.api.symbols.TypeDescKind; +import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; import io.ballerina.compiler.api.symbols.TypeSymbol; -import io.ballerina.compiler.api.symbols.UnionTypeSymbol; import io.ballerina.compiler.syntax.tree.AnnotationNode; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; -import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; +import io.ballerina.compiler.syntax.tree.NodeLocation; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.Token; +import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; import io.ballerina.projects.plugins.AnalysisTask; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.tools.diagnostics.Diagnostic; import io.ballerina.tools.diagnostics.DiagnosticFactory; import io.ballerina.tools.diagnostics.DiagnosticInfo; -import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import io.ballerina.tools.diagnostics.Location; +import org.wso2.ballerinalang.compiler.diagnostic.BLangDiagnosticLocation; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import static io.ballerina.stdlib.http.compiler.Constants.BALLERINA; +import static io.ballerina.stdlib.http.compiler.Constants.BASE_PATH; import static io.ballerina.stdlib.http.compiler.Constants.COLON; import static io.ballerina.stdlib.http.compiler.Constants.DEFAULT; +import static io.ballerina.stdlib.http.compiler.Constants.EMPTY; import static io.ballerina.stdlib.http.compiler.Constants.HTTP; -import static io.ballerina.stdlib.http.compiler.Constants.INTERCEPTABLE_SERVICE; import static io.ballerina.stdlib.http.compiler.Constants.MEDIA_TYPE_SUBTYPE_PREFIX; import static io.ballerina.stdlib.http.compiler.Constants.MEDIA_TYPE_SUBTYPE_REGEX; import static io.ballerina.stdlib.http.compiler.Constants.PLUS; import static io.ballerina.stdlib.http.compiler.Constants.REMOTE_KEYWORD; import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONFIG_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONTRACT_TYPE; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_TYPE; import static io.ballerina.stdlib.http.compiler.Constants.SUFFIX_SEPARATOR_REGEX; import static io.ballerina.stdlib.http.compiler.Constants.UNNECESSARY_CHARS_REGEX; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.diagnosticContainsErrors; import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.getCtxTypes; -import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.isHttpModule; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.getServiceDeclarationNode; import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.updateDiagnostic; -import static io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes.HTTP_101; -import static io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes.HTTP_119; -import static io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes.HTTP_120; +import static io.ballerina.stdlib.http.compiler.HttpDiagnostic.HTTP_101; +import static io.ballerina.stdlib.http.compiler.HttpDiagnostic.HTTP_119; +import static io.ballerina.stdlib.http.compiler.HttpDiagnostic.HTTP_120; /** * Validates a Ballerina Http Service. @@ -72,6 +82,7 @@ public class HttpServiceValidator implements AnalysisTask serviceTypeDesc = getServiceContractTypeDesc( + syntaxNodeAnalysisContext.semanticModel(), serviceDeclarationNode); + + serviceTypeDesc.ifPresent(typeDescriptorNode -> + checkBasePathExistence(syntaxNodeAnalysisContext, serviceDeclarationNode)); + + Optional metadataNodeOptional = serviceDeclarationNode.metadata(); + metadataNodeOptional.ifPresent(metadataNode -> validateServiceAnnotation(syntaxNodeAnalysisContext, + metadataNode, serviceTypeDesc.orElse(null), false)); - LinksMetaData linksMetaData = new LinksMetaData(); NodeList members = serviceDeclarationNode.members(); + if (serviceTypeDesc.isPresent()) { + Set resourcesFromServiceType = extractMethodsFromServiceType(serviceTypeDesc.get(), + syntaxNodeAnalysisContext.semanticModel()); + validateServiceContractResources(syntaxNodeAnalysisContext, resourcesFromServiceType, members, + serviceTypeDesc.get().toString().trim()); + } else { + validateResources(syntaxNodeAnalysisContext, members); + } + } + + public static boolean isServiceContractImplementation(SemanticModel semanticModel, ServiceDeclarationNode node) { + ServiceDeclarationNode serviceDeclarationNode = getServiceDeclarationNode(node, semanticModel); + if (serviceDeclarationNode == null) { + return false; + } + + return getServiceContractTypeDesc(semanticModel, serviceDeclarationNode).isPresent(); + } + + private static Optional getServiceContractTypeDesc(SemanticModel semanticModel, Node node) { + ServiceDeclarationNode serviceDeclarationNode = getServiceDeclarationNode(node, semanticModel); + if (serviceDeclarationNode == null) { + return Optional.empty(); + } + + return getServiceContractTypeDesc(semanticModel, serviceDeclarationNode); + } + + public static Optional getServiceContractTypeDesc(SemanticModel semanticModel, + ServiceDeclarationNode serviceDeclaration) { + Optional serviceTypeDesc = serviceDeclaration.typeDescriptor(); + if (serviceTypeDesc.isEmpty()) { + return Optional.empty(); + } + + Optional serviceTypeSymbol = semanticModel.symbol(serviceTypeDesc.get()); + if (serviceTypeSymbol.isEmpty() || + !(serviceTypeSymbol.get() instanceof TypeReferenceTypeSymbol serviceTypeRef)) { + return Optional.empty(); + } + + Optional serviceContractType = semanticModel.types().getTypeByName(BALLERINA, HTTP, EMPTY, + SERVICE_CONTRACT_TYPE); + if (serviceContractType.isEmpty() || + !(serviceContractType.get() instanceof TypeDefinitionSymbol serviceContractTypeDef)) { + return Optional.empty(); + } + + if (serviceTypeRef.subtypeOf(serviceContractTypeDef.typeDescriptor())) { + return serviceTypeDesc; + } + return Optional.empty(); + } + + private static Set extractMethodsFromServiceType(TypeDescriptorNode serviceTypeDesc, + SemanticModel semanticModel) { + Optional serviceTypeSymbol = semanticModel.symbol(serviceTypeDesc); + if (serviceTypeSymbol.isEmpty() || + !(serviceTypeSymbol.get() instanceof TypeReferenceTypeSymbol serviceTypeRef)) { + return Collections.emptySet(); + } + + TypeSymbol serviceTypeRefSymbol = serviceTypeRef.typeDescriptor(); + if (!(serviceTypeRefSymbol instanceof ObjectTypeSymbol serviceObjTypeSymbol)) { + return Collections.emptySet(); + } + + return serviceObjTypeSymbol.methods().keySet(); + } + + private static void checkBasePathExistence(SyntaxNodeAnalysisContext ctx, + ServiceDeclarationNode serviceDeclarationNode) { + NodeList nodes = serviceDeclarationNode.absoluteResourcePath(); + if (!nodes.isEmpty()) { + reportBasePathNotAllowed(ctx, nodes); + } + } + + protected static void validateResources(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + NodeList members) { + LinksMetaData linksMetaData = new LinksMetaData(); for (Node member : members) { if (member.kind() == SyntaxKind.OBJECT_METHOD_DEFINITION) { FunctionDefinitionNode node = (FunctionDefinitionNode) member; @@ -99,54 +198,61 @@ public void perform(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { HttpResourceValidator.validateResource(syntaxNodeAnalysisContext, (FunctionDefinitionNode) member, linksMetaData, getCtxTypes(syntaxNodeAnalysisContext)); + } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DECLARATION) { + HttpResourceValidator.validateResource(syntaxNodeAnalysisContext, (MethodDeclarationNode) member, + linksMetaData, getCtxTypes(syntaxNodeAnalysisContext)); } } validateResourceLinks(syntaxNodeAnalysisContext, linksMetaData); } - public static boolean diagnosticContainsErrors(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { - List diagnostics = syntaxNodeAnalysisContext.semanticModel().diagnostics(); - return diagnostics.stream() - .anyMatch(d -> DiagnosticSeverity.ERROR.equals(d.diagnosticInfo().severity())); - } - - public static ServiceDeclarationNode getServiceDeclarationNode(SyntaxNodeAnalysisContext context) { - ServiceDeclarationNode serviceDeclarationNode = (ServiceDeclarationNode) context.node(); - Optional serviceSymOptional = context.semanticModel().symbol(serviceDeclarationNode); - if (serviceSymOptional.isPresent()) { - List listenerTypes = ((ServiceDeclarationSymbol) serviceSymOptional.get()).listenerTypes(); - if (listenerTypes.stream().noneMatch(HttpServiceValidator::isListenerBelongsToHttpModule)) { - return null; + private static void validateServiceContractResources(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + Set resourcesFromServiceType, NodeList members, + String serviceTypeName) { + for (Node member : members) { + if (member.kind() == SyntaxKind.OBJECT_METHOD_DEFINITION) { + FunctionDefinitionNode node = (FunctionDefinitionNode) member; + NodeList tokens = node.qualifierList(); + if (tokens.isEmpty()) { + // Object methods are allowed. + continue; + } + if (tokens.stream().anyMatch(token -> token.text().equals(REMOTE_KEYWORD))) { + reportInvalidFunctionType(syntaxNodeAnalysisContext, node); + } + } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { + // Only resources defined in the serviceTypeDes is allowed + // No annotations are allowed in either in resource function or in the parameters + HttpServiceContractResourceValidator.validateResource(syntaxNodeAnalysisContext, + (FunctionDefinitionNode) member, resourcesFromServiceType, serviceTypeName); } } - return serviceDeclarationNode; } - private static boolean isListenerBelongsToHttpModule(TypeSymbol listenerType) { - if (listenerType.typeKind() == TypeDescKind.UNION) { - return ((UnionTypeSymbol) listenerType).memberTypeDescriptors().stream() - .filter(typeDescriptor -> typeDescriptor instanceof TypeReferenceTypeSymbol) - .map(typeReferenceTypeSymbol -> (TypeReferenceTypeSymbol) typeReferenceTypeSymbol) - .anyMatch(typeReferenceTypeSymbol -> isHttpModule(typeReferenceTypeSymbol.getModule().get())); + private static void checkForServiceImplementationErrors(SyntaxNodeAnalysisContext context) { + Node node = context.node(); + Optional serviceContractTypeDesc = getServiceContractTypeDesc(context.semanticModel(), + node); + if (serviceContractTypeDesc.isEmpty()) { + return; } + String serviceType = serviceContractTypeDesc.get().toString().trim(); - if (listenerType.typeKind() == TypeDescKind.TYPE_REFERENCE) { - return isHttpModule(((TypeReferenceTypeSymbol) listenerType).typeDescriptor().getModule().get()); - } - return false; - } + NodeLocation location = node.location(); + for (Diagnostic diagnostic : context.semanticModel().diagnostics()) { + Location diagnosticLocation = diagnostic.location(); - public static TypeDescKind getReferencedTypeDescKind(TypeSymbol typeSymbol) { - TypeDescKind kind = typeSymbol.typeKind(); - if (kind == TypeDescKind.TYPE_REFERENCE) { - TypeSymbol typeDescriptor = ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(); - kind = getReferencedTypeDescKind(typeDescriptor); + if (diagnostic.message().contains("no implementation found for the method 'resource function") + && diagnosticLocation.textRange().equals(location.textRange()) + && diagnosticLocation.lineRange().equals(location.lineRange())) { + enableImplementServiceContractCodeAction(context, serviceType, location); + return; + } } - return kind; } - private void validateResourceLinks(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + private static void validateResourceLinks(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, LinksMetaData linksMetaData) { if (!linksMetaData.hasNameReferenceObjects()) { for (Map linkedToResourceMap : linksMetaData.getLinkedToResourceMaps()) { @@ -159,7 +265,7 @@ private void validateResourceLinks(SyntaxNodeAnalysisContext syntaxNodeAnalysisC } } - private void checkLinkedResourceExistence(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + private static void checkLinkedResourceExistence(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, LinksMetaData linksMetaData, LinkedToResource linkedToResource) { if (linksMetaData.getLinkedResourcesMap().containsKey(linkedToResource.getName())) { List linkedResources = @@ -195,14 +301,10 @@ private void checkLinkedResourceExistence(SyntaxNodeAnalysisContext syntaxNodeAn } } - private static void extractServiceAnnotationAndValidate(SyntaxNodeAnalysisContext ctx, - ServiceDeclarationNode serviceDeclarationNode) { - Optional metadataNodeOptional = serviceDeclarationNode.metadata(); - - if (metadataNodeOptional.isEmpty()) { - return; - } - NodeList annotations = metadataNodeOptional.get().annotations(); + protected static void validateServiceAnnotation(SyntaxNodeAnalysisContext ctx, MetadataNode metadataNode, + TypeDescriptorNode serviceTypeDesc, + boolean isServiceContractType) { + NodeList annotations = metadataNode.annotations(); for (AnnotationNode annotation : annotations) { Node annotReference = annotation.annotReference(); String annotName = annotReference.toString(); @@ -212,25 +314,52 @@ private static void extractServiceAnnotationAndValidate(SyntaxNodeAnalysisContex } String[] annotStrings = annotName.split(COLON); if (SERVICE_CONFIG_ANNOTATION.equals(annotStrings[annotStrings.length - 1].trim()) - && (annotValue.isPresent())) { - boolean isInterceptableService = false; - for (Node child:serviceDeclarationNode.children()) { - if (child.kind() == SyntaxKind.QUALIFIED_NAME_REFERENCE && - ((QualifiedNameReferenceNode) child).modulePrefix().text().equals(HTTP) && - ((QualifiedNameReferenceNode) child).identifier().text().equals(INTERCEPTABLE_SERVICE)) { - isInterceptableService = true; - break; + && HTTP.equals(annotStrings[0].trim())) { + if (Objects.nonNull(serviceTypeDesc)) { + validateAnnotationUsageForServiceContractType(ctx, annotation, annotValue.orElse(null), + serviceTypeDesc); + return; + } + annotValue.ifPresent(mappingConstructorExpressionNode -> + validateServiceConfigAnnotation(ctx, mappingConstructorExpressionNode, isServiceContractType)); + } + } + } + + private static void validateAnnotationUsageForServiceContractType(SyntaxNodeAnalysisContext ctx, + AnnotationNode annotation, + MappingConstructorExpressionNode annotValue, + TypeDescriptorNode typeDescriptorNode) { + // TODO: Change annotValue.fields().size() > 1 after resource migration + if (Objects.isNull(annotValue) || annotValue.fields().isEmpty() || annotValue.fields().size() > 2) { + reportInvalidServiceConfigAnnotationUsage(ctx, annotation.location()); + return; + } + + for (MappingFieldNode field : annotValue.fields()) { + String fieldString = field.toString(); + fieldString = fieldString.trim().replaceAll(UNNECESSARY_CHARS_REGEX, ""); + if (field.kind().equals(SyntaxKind.SPECIFIC_FIELD)) { + String[] strings = fieldString.split(COLON, 2); + if (SERVICE_TYPE.equals(strings[0].trim())) { + String expectedServiceType = typeDescriptorNode.toString().trim(); + String actualServiceType = strings[1].trim(); + if (!actualServiceType.equals(expectedServiceType)) { + reportInvalidServiceContractType(ctx, expectedServiceType, actualServiceType, + field.location()); + return; } + } else if (!("openApiDefinition".equals(strings[0].trim()))) { + reportInvalidServiceConfigAnnotationUsage(ctx, annotation.location()); + return; } - validateServiceConfigAnnotation(ctx, annotValue, isInterceptableService); } } } - private static void validateServiceConfigAnnotation(SyntaxNodeAnalysisContext ctx, - Optional maps, - boolean isInterceptableService) { - MappingConstructorExpressionNode mapping = maps.get(); + protected static void validateServiceConfigAnnotation(SyntaxNodeAnalysisContext ctx, + MappingConstructorExpressionNode mapping, + boolean isServiceContractType) { for (MappingFieldNode field : mapping.fields()) { String fieldName = field.toString(); fieldName = fieldName.trim().replaceAll(UNNECESSARY_CHARS_REGEX, ""); @@ -239,19 +368,22 @@ private static void validateServiceConfigAnnotation(SyntaxNodeAnalysisContext ct if (MEDIA_TYPE_SUBTYPE_PREFIX.equals(strings[0].trim())) { if (!(strings[1].trim().matches(MEDIA_TYPE_SUBTYPE_REGEX))) { reportInvalidMediaTypeSubtype(ctx, strings[1].trim(), field); - break; + continue; } if (strings[1].trim().contains(PLUS)) { String suffix = strings[1].trim().split(SUFFIX_SEPARATOR_REGEX, 2)[1]; reportErrorMediaTypeSuffix(ctx, suffix.trim(), field); - break; } + } else if (SERVICE_TYPE.equals(strings[0].trim())) { + reportServiceTypeNotAllowedFound(ctx, field.location()); + } else if (BASE_PATH.equals(strings[0].trim()) && !isServiceContractType) { + reportBasePathFieldNotAllowed(ctx, field.location()); } } } } - private void reportInvalidFunctionType(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode node) { + private static void reportInvalidFunctionType(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode node) { DiagnosticInfo diagnosticInfo = new DiagnosticInfo(HTTP_101.getCode(), HTTP_101.getMessage(), HTTP_101.getSeverity()); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, node.location())); @@ -272,16 +404,47 @@ private static void reportErrorMediaTypeSuffix(SyntaxNodeAnalysisContext ctx, St } private static void reportResourceNameDoesNotExist(SyntaxNodeAnalysisContext ctx, LinkedToResource resource) { - updateDiagnostic(ctx, resource.getNode().location(), HttpDiagnosticCodes.HTTP_148, resource.getName()); + updateDiagnostic(ctx, resource.getNode().location(), HttpDiagnostic.HTTP_148, resource.getName()); } private static void reportUnresolvedLinkedResource(SyntaxNodeAnalysisContext ctx, LinkedToResource resource) { - updateDiagnostic(ctx, resource.getNode().location(), HttpDiagnosticCodes.HTTP_149); + updateDiagnostic(ctx, resource.getNode().location(), HttpDiagnostic.HTTP_149); } private static void reportUnresolvedLinkedResourceWithMethod(SyntaxNodeAnalysisContext ctx, LinkedToResource resource) { - updateDiagnostic(ctx, resource.getNode().location(), HttpDiagnosticCodes.HTTP_150, resource.getMethod(), + updateDiagnostic(ctx, resource.getNode().location(), HttpDiagnostic.HTTP_150, resource.getMethod(), resource.getName()); } + + private static void reportInvalidServiceConfigAnnotationUsage(SyntaxNodeAnalysisContext ctx, Location location) { + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_153); + } + + private static void reportInvalidServiceContractType(SyntaxNodeAnalysisContext ctx, String expectedServiceType, + String actualServiceType, Location location) { + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_156, expectedServiceType, actualServiceType); + } + + private static void reportBasePathNotAllowed(SyntaxNodeAnalysisContext ctx, NodeList nodes) { + Location startLocation = nodes.get(0).location(); + Location endLocation = nodes.get(nodes.size() - 1).location(); + BLangDiagnosticLocation location = new BLangDiagnosticLocation(startLocation.lineRange().fileName(), + startLocation.lineRange().startLine().line(), startLocation.lineRange().endLine().line(), + startLocation.lineRange().startLine().offset(), endLocation.lineRange().endLine().offset(), 0, 0); + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_154); + } + + private static void reportBasePathFieldNotAllowed(SyntaxNodeAnalysisContext ctx, Location location) { + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_155); + } + + private static void reportServiceTypeNotAllowedFound(SyntaxNodeAnalysisContext ctx, NodeLocation location) { + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_157); + } + + private static void enableImplementServiceContractCodeAction(SyntaxNodeAnalysisContext ctx, String serviceType, + NodeLocation location) { + updateDiagnostic(ctx, location, HttpDiagnostic.HTTP_HINT_105, serviceType); + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunction.java new file mode 100644 index 0000000000..445d9ec73e --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunction.java @@ -0,0 +1,56 @@ +/* + * 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.stdlib.http.compiler; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.FunctionSignatureNode; +import io.ballerina.compiler.syntax.tree.IdentifierToken; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.tools.diagnostics.Location; + +import java.util.Optional; + +/** + * Represents an HTTP resource function node interface. + * + * @since 2.12.0 + */ +public interface ResourceFunction { + + Optional metadata(); + + NodeList relativeResourcePath(); + + FunctionSignatureNode functionSignature(); + + IdentifierToken functionName(); + + Location location(); + + Optional getFunctionDefinitionNode(); + + Optional getSymbol(SemanticModel semanticModel); + + Node modifyWithSignature(FunctionSignatureNode updatedFunctionNode); + + int getResourceIdentifierCode(); +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunctionDeclaration.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunctionDeclaration.java new file mode 100644 index 0000000000..f60f307a97 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunctionDeclaration.java @@ -0,0 +1,90 @@ +/* + * 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.stdlib.http.compiler; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.FunctionSignatureNode; +import io.ballerina.compiler.syntax.tree.IdentifierToken; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.tools.diagnostics.Location; + +import java.util.Optional; + +/** + * Represents an HTTP resource method declaration node adapter. + * + * @since 2.12.0 + */ +public class ResourceFunctionDeclaration implements ResourceFunction { + + MethodDeclarationNode methodDeclarationNode; + FunctionSignatureNode functionSignatureNode; + IdentifierToken functionName; + int hashCode; + + public ResourceFunctionDeclaration(MethodDeclarationNode methodNode) { + methodDeclarationNode = new MethodDeclarationNode(methodNode.internalNode(), + methodNode.position(), methodNode.parent()); + functionSignatureNode = methodNode.methodSignature(); + functionName = methodNode.methodName(); + hashCode = methodNode.hashCode(); + } + + public Optional metadata() { + return methodDeclarationNode.metadata(); + } + + public NodeList relativeResourcePath() { + return methodDeclarationNode.relativeResourcePath(); + } + + public FunctionSignatureNode functionSignature() { + return new FunctionSignatureNode(functionSignatureNode.internalNode(), functionSignatureNode.position(), + functionSignatureNode.parent()); + } + + public IdentifierToken functionName() { + return new IdentifierToken(functionName.internalNode(), functionName.position(), functionName.parent()); + } + + public Location location() { + return methodDeclarationNode.location(); + } + + public Optional getFunctionDefinitionNode() { + return Optional.empty(); + } + + public Optional getSymbol(SemanticModel semanticModel) { + return semanticModel.symbol(methodDeclarationNode); + } + + public Node modifyWithSignature(FunctionSignatureNode updatedFunctionNode) { + MethodDeclarationNode.MethodDeclarationNodeModifier resourceModifier = methodDeclarationNode.modify(); + resourceModifier.withMethodSignature(updatedFunctionNode); + return resourceModifier.apply(); + } + + public int getResourceIdentifierCode() { + return hashCode; + }} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunctionDefinition.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunctionDefinition.java new file mode 100644 index 0000000000..4b05ed11de --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/ResourceFunctionDefinition.java @@ -0,0 +1,90 @@ +/* + * 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.stdlib.http.compiler; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.FunctionSignatureNode; +import io.ballerina.compiler.syntax.tree.IdentifierToken; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.tools.diagnostics.Location; + +import java.util.Optional; + +/** + * Represents an HTTP resource function definition node adapter. + * + * @since 2.12.0 + */ +public class ResourceFunctionDefinition implements ResourceFunction { + + FunctionDefinitionNode functionDefinitionNode; + FunctionSignatureNode functionSignatureNode; + IdentifierToken functionName; + int hashCode; + + public ResourceFunctionDefinition(FunctionDefinitionNode functionNode) { + functionDefinitionNode = new FunctionDefinitionNode(functionNode.internalNode(), + functionNode.position(), functionNode.parent()); + functionSignatureNode = functionNode.functionSignature(); + functionName = functionNode.functionName(); + hashCode = functionNode.hashCode(); + } + + public Optional metadata() { + return functionDefinitionNode.metadata(); + } + + public NodeList relativeResourcePath() { + return functionDefinitionNode.relativeResourcePath(); + } + + public FunctionSignatureNode functionSignature() { + return new FunctionSignatureNode(functionSignatureNode.internalNode(), functionSignatureNode.position(), + functionSignatureNode.parent()); + } + + public IdentifierToken functionName() { + return new IdentifierToken(functionName.internalNode(), functionName.position(), functionName.parent()); + } + + public Location location() { + return functionDefinitionNode.location(); + } + + public Optional getFunctionDefinitionNode() { + return Optional.of(functionDefinitionNode); + } + + public Optional getSymbol(SemanticModel semanticModel) { + return semanticModel.symbol(functionDefinitionNode); + } + + public Node modifyWithSignature(FunctionSignatureNode updatedFunctionNode) { + FunctionDefinitionNode.FunctionDefinitionNodeModifier resourceModifier = functionDefinitionNode.modify(); + resourceModifier.withFunctionSignature(updatedFunctionNode); + return resourceModifier.apply(); + } + + public int getResourceIdentifierCode() { + return hashCode; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddHeaderParameterCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddHeaderParameterCodeAction.java index a6362919be..37effb71ca 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddHeaderParameterCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddHeaderParameterCodeAction.java @@ -18,7 +18,7 @@ package io.ballerina.stdlib.http.compiler.codeaction; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; /** * CodeAction to add the annotated header parameter. @@ -27,7 +27,7 @@ public class AddHeaderParameterCodeAction extends AddResourceParameterCodeAction @Override protected String diagnosticCode() { - return HttpDiagnosticCodes.HTTP_HINT_102.getCode(); + return HttpDiagnostic.HTTP_HINT_102.getCode(); } protected String paramKind() { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddInterceptorRemoteMethodCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddInterceptorRemoteMethodCodeAction.java index 888acec1b3..88d1a332a8 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddInterceptorRemoteMethodCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddInterceptorRemoteMethodCodeAction.java @@ -18,7 +18,7 @@ package io.ballerina.stdlib.http.compiler.codeaction; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; import static io.ballerina.stdlib.http.compiler.codeaction.Constants.LS; import static io.ballerina.stdlib.http.compiler.codeaction.Constants.REMOTE; @@ -29,7 +29,7 @@ public class AddInterceptorRemoteMethodCodeAction extends AddInterceptorMethodCodeAction { @Override protected String diagnosticCode() { - return HttpDiagnosticCodes.HTTP_135.getCode(); + return HttpDiagnostic.HTTP_135.getCode(); } @Override diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddInterceptorResourceMethodCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddInterceptorResourceMethodCodeAction.java index 99c50e8a4d..c8fc25c5b3 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddInterceptorResourceMethodCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddInterceptorResourceMethodCodeAction.java @@ -18,7 +18,7 @@ package io.ballerina.stdlib.http.compiler.codeaction; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; import static io.ballerina.stdlib.http.compiler.codeaction.Constants.LS; import static io.ballerina.stdlib.http.compiler.codeaction.Constants.RESOURCE; @@ -29,7 +29,7 @@ public class AddInterceptorResourceMethodCodeAction extends AddInterceptorMethodCodeAction { @Override protected String diagnosticCode() { - return HttpDiagnosticCodes.HTTP_132.getCode(); + return HttpDiagnostic.HTTP_132.getCode(); } @Override diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddPayloadParameterCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddPayloadParameterCodeAction.java index 197eee2141..4853fcb2bd 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddPayloadParameterCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddPayloadParameterCodeAction.java @@ -18,7 +18,7 @@ package io.ballerina.stdlib.http.compiler.codeaction; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; /** * CodeAction to add the annotated payload parameter. @@ -27,7 +27,7 @@ public class AddPayloadParameterCodeAction extends AddResourceParameterCodeActio @Override protected String diagnosticCode() { - return HttpDiagnosticCodes.HTTP_HINT_101.getCode(); + return HttpDiagnostic.HTTP_HINT_101.getCode(); } protected String paramKind() { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResourceParameterCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResourceParameterCodeAction.java index 4f1fdf9a3a..8bf6e5f869 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResourceParameterCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResourceParameterCodeAction.java @@ -24,7 +24,6 @@ import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.SyntaxTree; import io.ballerina.projects.plugins.codeaction.CodeAction; -import io.ballerina.projects.plugins.codeaction.CodeActionArgument; import io.ballerina.projects.plugins.codeaction.CodeActionContext; import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; import io.ballerina.projects.plugins.codeaction.CodeActionInfo; @@ -43,7 +42,8 @@ import java.util.List; import java.util.Optional; -import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getCodeActionInfoWithLocation; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getLineRangeFromLocationKey; /** * Abstract implementation of code action to add a parameter to the resource signature. @@ -71,25 +71,19 @@ public Optional codeActionInfo(CodeActionContext context) { cursorPosition.get())) { return Optional.empty(); } - CodeActionArgument arg = CodeActionArgument.from(NODE_LOCATION_KEY, node.lineRange()); - CodeActionInfo info = CodeActionInfo.from(String.format("Add %s parameter", paramKind()), List.of(arg)); - return Optional.of(info); + return getCodeActionInfoWithLocation(node, String.format("Add %s parameter", paramKind())); } @Override public List execute(CodeActionExecutionContext context) { - LineRange lineRange = null; - for (CodeActionArgument arg : context.arguments()) { - if (NODE_LOCATION_KEY.equals(arg.key())) { - lineRange = arg.valueAs(LineRange.class); - } - } - if (lineRange == null) { + Optional lineRange = getLineRangeFromLocationKey(context); + + if (lineRange.isEmpty()) { return Collections.emptyList(); } SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); - NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange); + NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange.get()); if (!node.kind().equals(SyntaxKind.FUNCTION_SIGNATURE)) { return Collections.emptyList(); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResponseCacheConfigCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResponseCacheConfigCodeAction.java index ccde54a8a5..7072e08a54 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResponseCacheConfigCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResponseCacheConfigCodeAction.java @@ -22,12 +22,11 @@ import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.SyntaxTree; import io.ballerina.projects.plugins.codeaction.CodeAction; -import io.ballerina.projects.plugins.codeaction.CodeActionArgument; import io.ballerina.projects.plugins.codeaction.CodeActionContext; import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; import io.ballerina.projects.plugins.codeaction.CodeActionInfo; import io.ballerina.projects.plugins.codeaction.DocumentEdit; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; import io.ballerina.tools.text.LineRange; import io.ballerina.tools.text.TextDocument; import io.ballerina.tools.text.TextDocumentChange; @@ -39,7 +38,8 @@ import java.util.List; import java.util.Optional; -import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getCodeActionInfoWithLocation; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getLineRangeFromLocationKey; /** * CodeAction to add response cache configuration. @@ -47,7 +47,7 @@ public class AddResponseCacheConfigCodeAction implements CodeAction { @Override public List supportedDiagnosticCodes() { - return List.of(HttpDiagnosticCodes.HTTP_HINT_104.getCode()); + return List.of(HttpDiagnostic.HTTP_HINT_104.getCode()); } @Override @@ -58,24 +58,19 @@ public Optional codeActionInfo(CodeActionContext context) { return Optional.empty(); } - CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, node.location().lineRange()); - return Optional.of(CodeActionInfo.from("Add response cache configuration", List.of(locationArg))); + return getCodeActionInfoWithLocation(node, "Add response cache configuration"); } @Override public List execute(CodeActionExecutionContext context) { - LineRange lineRange = null; - for (CodeActionArgument arg : context.arguments()) { - if (NODE_LOCATION_KEY.equals(arg.key())) { - lineRange = arg.valueAs(LineRange.class); - } - } - if (lineRange == null) { + Optional lineRange = getLineRangeFromLocationKey(context); + + if (lineRange.isEmpty()) { return Collections.emptyList(); } SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); - NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange); + NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange.get()); String cacheConfig = "@http:Cache "; int start = node.position(); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResponseContentTypeCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResponseContentTypeCodeAction.java index 13d8532be8..9652c233f7 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResponseContentTypeCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/AddResponseContentTypeCodeAction.java @@ -22,12 +22,11 @@ import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.SyntaxTree; import io.ballerina.projects.plugins.codeaction.CodeAction; -import io.ballerina.projects.plugins.codeaction.CodeActionArgument; import io.ballerina.projects.plugins.codeaction.CodeActionContext; import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; import io.ballerina.projects.plugins.codeaction.CodeActionInfo; import io.ballerina.projects.plugins.codeaction.DocumentEdit; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; import io.ballerina.tools.text.LineRange; import io.ballerina.tools.text.TextDocument; import io.ballerina.tools.text.TextDocumentChange; @@ -39,7 +38,8 @@ import java.util.List; import java.util.Optional; -import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getCodeActionInfoWithLocation; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getLineRangeFromLocationKey; /** * CodeAction to add response content-type. @@ -47,7 +47,7 @@ public class AddResponseContentTypeCodeAction implements CodeAction { @Override public List supportedDiagnosticCodes() { - return List.of(HttpDiagnosticCodes.HTTP_HINT_103.getCode()); + return List.of(HttpDiagnostic.HTTP_HINT_103.getCode()); } @Override @@ -58,24 +58,19 @@ public Optional codeActionInfo(CodeActionContext context) { return Optional.empty(); } - CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, node.location().lineRange()); - return Optional.of(CodeActionInfo.from("Add response content-type", List.of(locationArg))); + return getCodeActionInfoWithLocation(node, "Add response content-type"); } @Override public List execute(CodeActionExecutionContext context) { - LineRange lineRange = null; - for (CodeActionArgument arg : context.arguments()) { - if (NODE_LOCATION_KEY.equals(arg.key())) { - lineRange = arg.valueAs(LineRange.class); - } - } - if (lineRange == null) { + Optional lineRange = getLineRangeFromLocationKey(context); + + if (lineRange.isEmpty()) { return Collections.emptyList(); } SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); - NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange); + NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange.get()); String mediaTypedPayload = "@http:Payload{mediaType: \"\"} "; int start = node.position(); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeHeaderParamTypeCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeHeaderParamTypeCodeAction.java index 132746efb0..20aeb134e2 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeHeaderParamTypeCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeHeaderParamTypeCodeAction.java @@ -25,12 +25,11 @@ import io.ballerina.compiler.syntax.tree.RestParameterNode; import io.ballerina.compiler.syntax.tree.SyntaxTree; import io.ballerina.projects.plugins.codeaction.CodeAction; -import io.ballerina.projects.plugins.codeaction.CodeActionArgument; import io.ballerina.projects.plugins.codeaction.CodeActionContext; import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; import io.ballerina.projects.plugins.codeaction.CodeActionInfo; import io.ballerina.projects.plugins.codeaction.DocumentEdit; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; import io.ballerina.tools.diagnostics.DiagnosticProperty; import io.ballerina.tools.text.LineRange; import io.ballerina.tools.text.TextDocument; @@ -44,7 +43,8 @@ import java.util.List; import java.util.Optional; -import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getCodeActionInfoWithLocation; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getLineRangeFromLocationKey; /** * Abstract implementation of code action to change a resource header param's type. @@ -53,7 +53,7 @@ public abstract class ChangeHeaderParamTypeCodeAction implements CodeAction { @Override public List supportedDiagnosticCodes() { - return List.of(HttpDiagnosticCodes.HTTP_109.getCode()); + return List.of(HttpDiagnostic.HTTP_109.getCode()); } @Override @@ -66,38 +66,27 @@ public Optional codeActionInfo(CodeActionContext context) { DiagnosticProperty diagnosticProperty = properties.get(0); if (!(diagnosticProperty instanceof BSymbolicProperty) || - !(diagnosticProperty.value() instanceof ParameterSymbol)) { + !(diagnosticProperty.value() instanceof ParameterSymbol parameterSymbol)) { return Optional.empty(); } - ParameterSymbol parameterSymbol = (ParameterSymbol) diagnosticProperty.value(); Optional nonTerminalNode = parameterSymbol.getLocation() .flatMap(location -> Optional.ofNullable(CodeActionUtil.findNode(syntaxTree, parameterSymbol))); - if (nonTerminalNode.isEmpty()) { - return Optional.empty(); - } + return nonTerminalNode.flatMap(terminalNode -> getCodeActionInfoWithLocation(terminalNode, + String.format("Change header param to '%s'", headerParamType()))); - CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, - nonTerminalNode.get().location().lineRange()); - return Optional.of(CodeActionInfo.from(String.format("Change header param to '%s'", headerParamType()), - List.of(locationArg))); } @Override public List execute(CodeActionExecutionContext context) { - LineRange lineRange = null; - for (CodeActionArgument argument : context.arguments()) { - if (NODE_LOCATION_KEY.equals(argument.key())) { - lineRange = argument.valueAs(LineRange.class); - } - } + Optional lineRange = getLineRangeFromLocationKey(context); - if (lineRange == null) { + if (lineRange.isEmpty()) { return Collections.emptyList(); } SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); - NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange); + NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange.get()); TextRange typeNodeTextRange; switch (node.kind()) { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeReturnTypeWithCallerCodeAction.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeReturnTypeWithCallerCodeAction.java index 776583b777..cc19b3e212 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeReturnTypeWithCallerCodeAction.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ChangeReturnTypeWithCallerCodeAction.java @@ -20,12 +20,11 @@ import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.SyntaxTree; import io.ballerina.projects.plugins.codeaction.CodeAction; -import io.ballerina.projects.plugins.codeaction.CodeActionArgument; import io.ballerina.projects.plugins.codeaction.CodeActionContext; import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; import io.ballerina.projects.plugins.codeaction.CodeActionInfo; import io.ballerina.projects.plugins.codeaction.DocumentEdit; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; import io.ballerina.tools.text.LineRange; import io.ballerina.tools.text.TextDocument; import io.ballerina.tools.text.TextDocumentChange; @@ -37,7 +36,8 @@ import java.util.List; import java.util.Optional; -import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getCodeActionInfoWithLocation; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getLineRangeFromLocationKey; /** * Codeaction to change the return type to
error?
if the resource function has
http:Caller
as a @@ -47,7 +47,7 @@ public class ChangeReturnTypeWithCallerCodeAction implements CodeAction { @Override public List supportedDiagnosticCodes() { - return List.of(HttpDiagnosticCodes.HTTP_118.getCode()); + return List.of(HttpDiagnostic.HTTP_118.getCode()); } @Override @@ -58,28 +58,22 @@ public Optional codeActionInfo(CodeActionContext context) { return Optional.empty(); } - CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, node.location().lineRange()); - return Optional.of(CodeActionInfo.from("Change return type to 'error?'", List.of(locationArg))); + return getCodeActionInfoWithLocation(node, "Change return type to 'error?'"); } @Override public List execute(CodeActionExecutionContext context) { - LineRange lineRange = null; - for (CodeActionArgument argument : context.arguments()) { - if (NODE_LOCATION_KEY.equals(argument.key())) { - lineRange = argument.valueAs(LineRange.class); - } - } + Optional lineRange = getLineRangeFromLocationKey(context); - if (lineRange == null) { + if (lineRange.isEmpty()) { return Collections.emptyList(); } SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); TextDocument textDocument = syntaxTree.textDocument(); - int start = textDocument.textPositionFrom(lineRange.startLine()); - int end = textDocument.textPositionFrom(lineRange.endLine()); + int start = textDocument.textPositionFrom(lineRange.get().startLine()); + int end = textDocument.textPositionFrom(lineRange.get().endLine()); List textEdits = new ArrayList<>(); textEdits.add(TextEdit.from(TextRange.from(start, end - start), "error?")); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/CodeActionUtil.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/CodeActionUtil.java index 74caf905f4..a996b1108e 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/CodeActionUtil.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/CodeActionUtil.java @@ -20,11 +20,19 @@ import io.ballerina.compiler.syntax.tree.ModulePartNode; import io.ballerina.compiler.syntax.tree.NonTerminalNode; import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.projects.plugins.codeaction.CodeActionArgument; +import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; +import io.ballerina.projects.plugins.codeaction.CodeActionInfo; import io.ballerina.tools.text.LinePosition; import io.ballerina.tools.text.LineRange; import io.ballerina.tools.text.TextDocument; import io.ballerina.tools.text.TextRange; +import java.util.List; +import java.util.Optional; + +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.NODE_LOCATION_KEY; + /** * Utilities for code actions. */ @@ -90,4 +98,31 @@ public static boolean isWithinRange(LineRange lineRange, LinePosition pos) { pos.line() == sLine && pos.offset() >= sCol )); } + + /** + * Get code action info with location. + * + * @param node Node + * @param title Title + * @return Code action info with location + */ + public static Optional getCodeActionInfoWithLocation(NonTerminalNode node, String title) { + CodeActionArgument locationArg = CodeActionArgument.from(NODE_LOCATION_KEY, node.location().lineRange()); + return Optional.of(CodeActionInfo.from(title, List.of(locationArg))); + } + + /** + * Get line range from location key. + * + * @param context Code action execution context + * @return Line range + */ + public static Optional getLineRangeFromLocationKey(CodeActionExecutionContext context) { + for (CodeActionArgument argument : context.arguments()) { + if (NODE_LOCATION_KEY.equals(argument.key())) { + return Optional.of(argument.valueAs(LineRange.class)); + } + } + return Optional.empty(); + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/Constants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/Constants.java index d1cd8715bf..9bbcdc9eea 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/Constants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/Constants.java @@ -26,6 +26,7 @@ private Constants () {} public static final String NODE_LOCATION_KEY = "node.location"; public static final String IS_ERROR_INTERCEPTOR_TYPE = "node.errorInterceptor"; + public static final String EXPECTED_BASE_PATH = "expectedBasePath"; public static final String REMOTE = "remote"; public static final String RESOURCE = "resource"; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContract.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContract.java new file mode 100644 index 0000000000..5f03c7ec96 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codeaction/ImplementServiceContract.java @@ -0,0 +1,182 @@ +/* + * 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.stdlib.http.compiler.codeaction; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.MethodSymbol; +import io.ballerina.compiler.api.symbols.ModuleSymbol; +import io.ballerina.compiler.api.symbols.ObjectTypeSymbol; +import io.ballerina.compiler.api.symbols.ResourceMethodSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.compiler.api.symbols.resourcepath.ResourcePath; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; +import io.ballerina.projects.plugins.codeaction.CodeAction; +import io.ballerina.projects.plugins.codeaction.CodeActionContext; +import io.ballerina.projects.plugins.codeaction.CodeActionExecutionContext; +import io.ballerina.projects.plugins.codeaction.CodeActionInfo; +import io.ballerina.projects.plugins.codeaction.DocumentEdit; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; +import io.ballerina.tools.text.LineRange; +import io.ballerina.tools.text.TextDocument; +import io.ballerina.tools.text.TextDocumentChange; +import io.ballerina.tools.text.TextEdit; +import io.ballerina.tools.text.TextRange; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.ballerina.stdlib.http.compiler.HttpServiceContractResourceValidator.constructResourcePathName; +import static io.ballerina.stdlib.http.compiler.HttpServiceValidator.getServiceContractTypeDesc; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getCodeActionInfoWithLocation; +import static io.ballerina.stdlib.http.compiler.codeaction.CodeActionUtil.getLineRangeFromLocationKey; +import static io.ballerina.stdlib.http.compiler.codeaction.Constants.LS; + +/** + * Represents a code action to implement all the resource methods from the service contract type. + * + * @since 2.12.0 + */ +public class ImplementServiceContract implements CodeAction { + @Override + public List supportedDiagnosticCodes() { + return List.of(HttpDiagnostic.HTTP_HINT_105.getCode()); + } + + @Override + public Optional codeActionInfo(CodeActionContext context) { + NonTerminalNode node = CodeActionUtil.findNode(context.currentDocument().syntaxTree(), + context.diagnostic().location().lineRange()); + if (!node.kind().equals(SyntaxKind.SERVICE_DECLARATION)) { + return Optional.empty(); + } + + return getCodeActionInfoWithLocation(node, "Implement service contract resources"); + } + + @Override + public List execute(CodeActionExecutionContext context) { + Optional lineRange = getLineRangeFromLocationKey(context); + + if (lineRange.isEmpty()) { + return Collections.emptyList(); + } + + SyntaxTree syntaxTree = context.currentDocument().syntaxTree(); + SemanticModel semanticModel = context.currentSemanticModel(); + NonTerminalNode node = CodeActionUtil.findNode(syntaxTree, lineRange.get()); + if (!node.kind().equals(SyntaxKind.SERVICE_DECLARATION)) { + return Collections.emptyList(); + } + + Optional serviceTypeDesc = getServiceContractTypeDesc(semanticModel, + (ServiceDeclarationNode) node); + if (serviceTypeDesc.isEmpty()) { + return Collections.emptyList(); + } + + Optional serviceTypeSymbol = semanticModel.symbol(serviceTypeDesc.get()); + if (serviceTypeSymbol.isEmpty() || + !(serviceTypeSymbol.get() instanceof TypeReferenceTypeSymbol serviceTypeRef)) { + return Collections.emptyList(); + } + + TypeSymbol serviceTypeRefSymbol = serviceTypeRef.typeDescriptor(); + if (!(serviceTypeRefSymbol instanceof ObjectTypeSymbol serviceObjTypeSymbol)) { + return Collections.emptyList(); + } + + NodeList members = ((ServiceDeclarationNode) node).members(); + List existingMethods = new ArrayList<>(); + for (Node member : members) { + if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { + Optional functionDefinitionSymbol = semanticModel.symbol(member); + if (functionDefinitionSymbol.isEmpty() || + !(functionDefinitionSymbol.get() instanceof ResourceMethodSymbol resourceMethodSymbol)) { + continue; + } + ResourcePath resourcePath = resourceMethodSymbol.resourcePath(); + existingMethods.add(resourceMethodSymbol.getName().orElse("") + " " + + constructResourcePathName(resourcePath)); + } + } + + Map methodSymbolMap = serviceObjTypeSymbol.methods(); + String parentModuleName = getParentModuleName(semanticModel, (ServiceDeclarationNode) node); + StringBuilder methods = new StringBuilder(); + for (Map.Entry entry : methodSymbolMap.entrySet()) { + if (existingMethods.contains(entry.getKey())) { + continue; + } + MethodSymbol methodSymbol = entry.getValue(); + if (methodSymbol instanceof ResourceMethodSymbol resourceMethodSymbol) { + methods.append(getMethodSignature(resourceMethodSymbol, parentModuleName)); + } + } + + TextRange textRange = TextRange.from(((ServiceDeclarationNode) node).closeBraceToken(). + textRange().startOffset(), 0); + List textEdits = new ArrayList<>(); + textEdits.add(TextEdit.from(textRange, methods.toString())); + TextDocumentChange change = TextDocumentChange.from(textEdits.toArray(new TextEdit[0])); + TextDocument modifiedTextDocument = syntaxTree.textDocument().apply(change); + return Collections.singletonList(new DocumentEdit(context.fileUri(), SyntaxTree.from(modifiedTextDocument))); + } + + private String getMethodSignature(ResourceMethodSymbol resourceMethodSymbol, String parentModuleName) { + String resourceSignature = resourceMethodSymbol.signature(); + if (Objects.nonNull(parentModuleName)) { + resourceSignature = resourceSignature.replace(parentModuleName + ":", ""); + } + return LS + "\t" + sanitizePackageNames(resourceSignature) + " {" + LS + LS + "\t}" + LS; + } + + private String sanitizePackageNames(String input) { + Pattern pattern = Pattern.compile("(\\w+)/(\\w+:)(\\d+\\.\\d+\\.\\d+):"); + Matcher matcher = pattern.matcher(input); + return matcher.replaceAll("$2"); + } + + private String getParentModuleName(SemanticModel semanticModel, ServiceDeclarationNode serviceDeclarationNode) { + Optional serviceDeclarationSymbol = semanticModel.symbol(serviceDeclarationNode); + if (serviceDeclarationSymbol.isEmpty()) { + return null; + } + + Optional module = serviceDeclarationSymbol.get().getModule(); + return module.map(moduleSymbol -> moduleSymbol.id().toString()).orElse(null); + } + + @Override + public String name() { + return "IMPLEMENT_SERVICE_CONTRACT"; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpServiceModifier.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpServiceModifier.java index 4aa6227431..0aac4114c2 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpServiceModifier.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpServiceModifier.java @@ -22,7 +22,10 @@ import io.ballerina.projects.DocumentId; import io.ballerina.projects.plugins.CodeModifier; import io.ballerina.projects.plugins.CodeModifierContext; -import io.ballerina.stdlib.http.compiler.codemodifier.context.DocumentContext; +import io.ballerina.stdlib.http.compiler.codemodifier.contract.ContractInfoModifierTask; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.HttpPayloadParamIdentifier; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.PayloadAnnotationModifierTask; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.PayloadParamContext; import java.util.HashMap; import java.util.List; @@ -34,17 +37,22 @@ * @since 2201.5.0 */ public class HttpServiceModifier extends CodeModifier { - private final Map payloadParamContextMap; + private final Map payloadParamContextMap; + private final Map ctxData; - public HttpServiceModifier() { + public HttpServiceModifier(Map ctxData) { this.payloadParamContextMap = new HashMap<>(); + this.ctxData = ctxData; } @Override public void init(CodeModifierContext codeModifierContext) { + ctxData.put("HTTP_CODE_MODIFIER_EXECUTED", true); codeModifierContext.addSyntaxNodeAnalysisTask( new HttpPayloadParamIdentifier(this.payloadParamContextMap), - List.of(SyntaxKind.SERVICE_DECLARATION, SyntaxKind.CLASS_DEFINITION)); + List.of(SyntaxKind.SERVICE_DECLARATION, SyntaxKind.CLASS_DEFINITION, SyntaxKind.OBJECT_TYPE_DESC)); codeModifierContext.addSourceModifierTask(new PayloadAnnotationModifierTask(this.payloadParamContextMap)); + + codeModifierContext.addSourceModifierTask(new ContractInfoModifierTask()); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/contract/ContractInfoModifierTask.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/contract/ContractInfoModifierTask.java new file mode 100644 index 0000000000..70a8d1106a --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/contract/ContractInfoModifierTask.java @@ -0,0 +1,231 @@ +/* + * 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.stdlib.http.compiler.codemodifier.contract; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.syntax.tree.AbstractNodeFactory; +import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.MappingFieldNode; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.ModuleMemberDeclarationNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.SeparatedNodeList; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.compiler.syntax.tree.SpecificFieldNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; +import io.ballerina.projects.Document; +import io.ballerina.projects.DocumentId; +import io.ballerina.projects.Module; +import io.ballerina.projects.ModuleId; +import io.ballerina.projects.Package; +import io.ballerina.projects.plugins.ModifierTask; +import io.ballerina.projects.plugins.SourceModifierContext; +import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import io.ballerina.tools.text.TextDocument; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static io.ballerina.compiler.syntax.tree.AbstractNodeFactory.createIdentifierToken; +import static io.ballerina.compiler.syntax.tree.AbstractNodeFactory.createNodeList; +import static io.ballerina.compiler.syntax.tree.AbstractNodeFactory.createSeparatedNodeList; +import static io.ballerina.compiler.syntax.tree.AbstractNodeFactory.createToken; +import static io.ballerina.compiler.syntax.tree.NodeFactory.createAnnotationNode; +import static io.ballerina.compiler.syntax.tree.NodeFactory.createMappingConstructorExpressionNode; +import static io.ballerina.compiler.syntax.tree.NodeFactory.createMetadataNode; +import static io.ballerina.compiler.syntax.tree.NodeFactory.createQualifiedNameReferenceNode; +import static io.ballerina.compiler.syntax.tree.NodeFactory.createSpecificFieldNode; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.AT_TOKEN; +import static io.ballerina.compiler.syntax.tree.SyntaxKind.COLON_TOKEN; +import static io.ballerina.stdlib.http.compiler.Constants.COLON; +import static io.ballerina.stdlib.http.compiler.Constants.HTTP; +import static io.ballerina.stdlib.http.compiler.Constants.SERVICE_CONFIG_ANNOTATION; +import static io.ballerina.stdlib.http.compiler.HttpServiceValidator.getServiceContractTypeDesc; + +/** + * {@code ServiceTypeModifierTask} injects the `serviceType` field in the `http:ServiceConfig` annotation. + * + * @since 2.12.0 + */ +public class ContractInfoModifierTask implements ModifierTask { + + @Override + public void modify(SourceModifierContext modifierContext) { + boolean erroneousCompilation = modifierContext.compilation().diagnosticResult() + .diagnostics().stream() + .anyMatch(d -> DiagnosticSeverity.ERROR.equals(d.diagnosticInfo().severity())); + if (erroneousCompilation) { + return; + } + + modifyServiceDeclarationNodes(modifierContext); + } + + private void modifyServiceDeclarationNodes(SourceModifierContext modifierContext) { + Package currentPackage = modifierContext.currentPackage(); + for (ModuleId moduleId : currentPackage.moduleIds()) { + modifyServiceDeclarationsPerModule(modifierContext, moduleId, currentPackage); + } + } + + private void modifyServiceDeclarationsPerModule(SourceModifierContext modifierContext, ModuleId moduleId, + Package currentPackage) { + Module currentModule = currentPackage.module(moduleId); + for (DocumentId documentId : currentModule.documentIds()) { + modifyServiceDeclarationsPerDocument(modifierContext, documentId, currentModule); + } + for (DocumentId documentId : currentModule.testDocumentIds()) { + modifyServiceDeclarationsPerDocument(modifierContext, documentId, currentModule); + } + } + + private void modifyServiceDeclarationsPerDocument(SourceModifierContext modifierContext, DocumentId documentId, + Module currentModule) { + Document currentDoc = currentModule.document(documentId); + ModulePartNode rootNode = currentDoc.syntaxTree().rootNode(); + SemanticModel semanticModel = modifierContext.compilation().getSemanticModel(currentModule.moduleId()); + NodeList newMembers = updateMemberNodes(rootNode.members(), semanticModel); + ModulePartNode newModulePart = rootNode.modify(rootNode.imports(), newMembers, rootNode.eofToken()); + SyntaxTree updatedSyntaxTree = currentDoc.syntaxTree().modifyWith(newModulePart); + TextDocument textDocument = updatedSyntaxTree.textDocument(); + if (currentModule.documentIds().contains(documentId)) { + modifierContext.modifySourceFile(textDocument, documentId); + } else { + modifierContext.modifyTestSourceFile(textDocument, documentId); + } + } + + private NodeList updateMemberNodes(NodeList oldMembers, + SemanticModel semanticModel) { + List updatedMembers = new ArrayList<>(); + for (ModuleMemberDeclarationNode memberNode : oldMembers) { + if (memberNode.kind().equals(SyntaxKind.SERVICE_DECLARATION)) { + updatedMembers.add(updateServiceDeclarationNode((ServiceDeclarationNode) memberNode, semanticModel)); + } else { + updatedMembers.add(memberNode); + } + } + return AbstractNodeFactory.createNodeList(updatedMembers); + } + + private ServiceDeclarationNode updateServiceDeclarationNode(ServiceDeclarationNode serviceDeclarationNode, + SemanticModel semanticModel) { + Optional serviceTypeDesc = getServiceContractTypeDesc(semanticModel, + serviceDeclarationNode); + if (serviceTypeDesc.isEmpty()) { + return serviceDeclarationNode; + } + + Optional metadataNodeOptional = serviceDeclarationNode.metadata(); + if (metadataNodeOptional.isEmpty()) { + return addServiceConfigAnnotation(serviceTypeDesc.get(), serviceDeclarationNode); + } + + NodeList annotations = metadataNodeOptional.get().annotations(); + for (AnnotationNode annotation : annotations) { + Node annotReference = annotation.annotReference(); + String annotName = annotReference.toString(); + if (annotReference.kind() != SyntaxKind.QUALIFIED_NAME_REFERENCE) { + continue; + } + String[] annotStrings = annotName.split(COLON); + if (SERVICE_CONFIG_ANNOTATION.equals(annotStrings[annotStrings.length - 1].trim()) + && HTTP.equals(annotStrings[0].trim())) { + return updateServiceConfigAnnotation(serviceTypeDesc.get(), serviceDeclarationNode, annotation); + } + } + + return addServiceConfigAnnotation(serviceTypeDesc.get(), serviceDeclarationNode); + } + + private ServiceDeclarationNode updateServiceConfigAnnotation(TypeDescriptorNode serviceTypeDesc, + ServiceDeclarationNode serviceDeclarationNode, + AnnotationNode serviceConfigAnnotation) { + SpecificFieldNode serviceTypeField = createSpecificFieldNode(null, createIdentifierToken("serviceType"), + createToken(COLON_TOKEN), serviceTypeDesc); + Optional serviceConfigConstruct = serviceConfigAnnotation.annotValue(); + MappingConstructorExpressionNode newServiceConfigConstruct; + if (serviceConfigConstruct.isEmpty() || serviceConfigConstruct.get().fields().isEmpty()) { + newServiceConfigConstruct = createMappingConstructorExpressionNode( + createToken(SyntaxKind.OPEN_BRACE_TOKEN), createSeparatedNodeList(serviceTypeField), + createToken(SyntaxKind.CLOSE_BRACE_TOKEN)); + } else { + MappingConstructorExpressionNode existingServiceConfigConstruct = serviceConfigConstruct.get(); + SeparatedNodeList fields = existingServiceConfigConstruct.fields(); + boolean hasServiceType = fields.stream().anyMatch(field -> { + if (field.kind().equals(SyntaxKind.SPECIFIC_FIELD)) { + SpecificFieldNode specificField = (SpecificFieldNode) field; + return specificField.fieldName().toString().equals("serviceType"); + } + return false; + }); + if (hasServiceType) { + return serviceDeclarationNode; + } + List fieldList = fields.stream().collect(Collectors.toList()); + fieldList.add(createToken(SyntaxKind.COMMA_TOKEN)); + fieldList.add(serviceTypeField); + SeparatedNodeList updatedFields = createSeparatedNodeList(fieldList); + newServiceConfigConstruct = createMappingConstructorExpressionNode( + createToken(SyntaxKind.OPEN_BRACE_TOKEN), updatedFields, + createToken(SyntaxKind.CLOSE_BRACE_TOKEN)); + } + AnnotationNode newServiceConfigAnnotation = serviceConfigAnnotation.modify() + .withAnnotValue(newServiceConfigConstruct).apply(); + Optional metadata = serviceDeclarationNode.metadata(); + if (metadata.isEmpty()) { + MetadataNode metadataNode = createMetadataNode(null, + createNodeList(newServiceConfigAnnotation)); + return serviceDeclarationNode.modify().withMetadata(metadataNode).apply(); + } + + NodeList updatedAnnotations = metadata.get().annotations() + .remove(serviceConfigAnnotation) + .add(newServiceConfigAnnotation); + MetadataNode metadataNode = metadata.get().modify().withAnnotations(updatedAnnotations).apply(); + return serviceDeclarationNode.modify().withMetadata(metadataNode).apply(); + } + + private ServiceDeclarationNode addServiceConfigAnnotation(TypeDescriptorNode serviceTypeDesc, + ServiceDeclarationNode serviceDeclarationNode) { + SpecificFieldNode serviceTypeField = createSpecificFieldNode(null, createIdentifierToken("serviceType"), + createToken(COLON_TOKEN), serviceTypeDesc); + MappingConstructorExpressionNode serviceConfigConstruct = createMappingConstructorExpressionNode( + createToken(SyntaxKind.OPEN_BRACE_TOKEN), createSeparatedNodeList(serviceTypeField), + createToken(SyntaxKind.CLOSE_BRACE_TOKEN)); + AnnotationNode serviceConfigAnnotation = createAnnotationNode(createToken(AT_TOKEN), + createQualifiedNameReferenceNode(createIdentifierToken(HTTP), createToken(COLON_TOKEN), + createIdentifierToken(SERVICE_CONFIG_ANNOTATION)), serviceConfigConstruct); + Optional metadata = serviceDeclarationNode.metadata(); + MetadataNode metadataNode; + if (metadata.isEmpty()) { + metadataNode = createMetadataNode(null, createNodeList(serviceConfigAnnotation)); + } else { + NodeList annotations = metadata.get().annotations().add(serviceConfigAnnotation); + metadataNode = metadata.get().modify().withAnnotations(annotations).apply(); + } + return serviceDeclarationNode.modify().withMetadata(metadataNode).apply(); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpPayloadParamIdentifier.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/HttpPayloadParamIdentifier.java similarity index 74% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpPayloadParamIdentifier.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/HttpPayloadParamIdentifier.java index 902560d9ab..8b88b4840c 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/HttpPayloadParamIdentifier.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/HttpPayloadParamIdentifier.java @@ -16,7 +16,7 @@ * under the License. */ -package io.ballerina.stdlib.http.compiler.codemodifier; +package io.ballerina.stdlib.http.compiler.codemodifier.payload; import io.ballerina.compiler.api.ModuleID; import io.ballerina.compiler.api.symbols.AnnotationSymbol; @@ -29,8 +29,10 @@ import io.ballerina.compiler.api.symbols.UnionTypeSymbol; import io.ballerina.compiler.syntax.tree.ClassDefinitionNode; import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.Token; @@ -38,14 +40,17 @@ import io.ballerina.projects.DocumentId; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.stdlib.http.compiler.Constants; -import io.ballerina.stdlib.http.compiler.HttpDiagnosticCodes; +import io.ballerina.stdlib.http.compiler.HttpDiagnostic; import io.ballerina.stdlib.http.compiler.HttpResourceValidator; import io.ballerina.stdlib.http.compiler.HttpServiceValidator; -import io.ballerina.stdlib.http.compiler.codemodifier.context.DocumentContext; -import io.ballerina.stdlib.http.compiler.codemodifier.context.ParamAvailability; -import io.ballerina.stdlib.http.compiler.codemodifier.context.ParamData; -import io.ballerina.stdlib.http.compiler.codemodifier.context.ResourceContext; -import io.ballerina.stdlib.http.compiler.codemodifier.context.ServiceContext; +import io.ballerina.stdlib.http.compiler.ResourceFunction; +import io.ballerina.stdlib.http.compiler.ResourceFunctionDeclaration; +import io.ballerina.stdlib.http.compiler.ResourceFunctionDefinition; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.PayloadParamAvailability; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.PayloadParamContext; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.PayloadParamData; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.ResourcePayloadParamContext; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.ServicePayloadParamContext; import java.util.ArrayList; import java.util.List; @@ -67,7 +72,10 @@ import static io.ballerina.stdlib.http.compiler.Constants.TABLE_OF_ANYDATA_MAP; import static io.ballerina.stdlib.http.compiler.Constants.TUPLE_OF_ANYDATA; import static io.ballerina.stdlib.http.compiler.Constants.XML; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.diagnosticContainsErrors; import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.getCtxTypes; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.getServiceDeclarationNode; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.isHttpServiceType; import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.subtypeOf; import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.updateDiagnostic; import static io.ballerina.stdlib.http.compiler.HttpResourceValidator.getEffectiveType; @@ -81,9 +89,9 @@ * @since 2201.5.0 */ public class HttpPayloadParamIdentifier extends HttpServiceValidator { - private final Map documentContextMap; + private final Map documentContextMap; - public HttpPayloadParamIdentifier(Map documentContextMap) { + public HttpPayloadParamIdentifier(Map documentContextMap) { this.documentContextMap = documentContextMap; } @@ -98,6 +106,9 @@ public void perform(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext) { validateServiceDeclaration(syntaxNodeAnalysisContext, typeSymbols); } else if (kind == SyntaxKind.CLASS_DEFINITION) { validateClassDefinition(syntaxNodeAnalysisContext, typeSymbols); + } else if (kind == SyntaxKind.OBJECT_TYPE_DESC && isHttpServiceType(syntaxNodeAnalysisContext.semanticModel(), + syntaxNodeAnalysisContext.node())) { + validateServiceObjDefinition(syntaxNodeAnalysisContext, typeSymbols); } } @@ -108,10 +119,28 @@ private void validateServiceDeclaration(SyntaxNodeAnalysisContext syntaxNodeAnal return; } NodeList members = serviceDeclarationNode.members(); - ServiceContext serviceContext = new ServiceContext(serviceDeclarationNode.hashCode()); + ServicePayloadParamContext serviceContext = new ServicePayloadParamContext(serviceDeclarationNode.hashCode()); + validateResources(syntaxNodeAnalysisContext, typeSymbols, members, serviceContext); + } + + private void validateServiceObjDefinition(SyntaxNodeAnalysisContext context, Map typeSymbols) { + ObjectTypeDescriptorNode serviceObjType = (ObjectTypeDescriptorNode) context.node(); + NodeList members = serviceObjType.members(); + ServicePayloadParamContext serviceContext = new ServicePayloadParamContext(serviceObjType.hashCode()); + validateResources(context, typeSymbols, members, serviceContext); + } + + private void validateResources(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, + Map typeSymbols, NodeList members, + ServicePayloadParamContext serviceContext) { for (Node member : members) { if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { - validateResource(syntaxNodeAnalysisContext, (FunctionDefinitionNode) member, serviceContext, + validateResource(syntaxNodeAnalysisContext, + new ResourceFunctionDefinition((FunctionDefinitionNode) member), serviceContext, + typeSymbols); + } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DECLARATION) { + validateResource(syntaxNodeAnalysisContext, + new ResourceFunctionDeclaration((MethodDeclarationNode) member), serviceContext, typeSymbols); } } @@ -128,7 +157,7 @@ private void validateClassDefinition(SyntaxNodeAnalysisContext syntaxNodeAnalysi return; } NodeList members = classDefinitionNode.members(); - ServiceContext serviceContext = new ServiceContext(classDefinitionNode.hashCode()); + ServicePayloadParamContext serviceContext = new ServicePayloadParamContext(classDefinitionNode.hashCode()); boolean proceed = false; for (Node member : members) { if (member.kind() == SyntaxKind.TYPE_REFERENCE) { @@ -145,28 +174,25 @@ private void validateClassDefinition(SyntaxNodeAnalysisContext syntaxNodeAnalysi } } if (proceed) { - for (Node member : members) { - if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { - validateResource(syntaxNodeAnalysisContext, (FunctionDefinitionNode) member, serviceContext, - typeSymbols); - } - } + validateResources(syntaxNodeAnalysisContext, typeSymbols, members, serviceContext); } } - void validateResource(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, ServiceContext serviceContext, + void validateResource(SyntaxNodeAnalysisContext ctx, ResourceFunction member, + ServicePayloadParamContext serviceContext, Map typeSymbols) { extractInputParamTypeAndValidate(ctx, member, serviceContext, typeSymbols); } - void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, FunctionDefinitionNode member, - ServiceContext serviceContext, Map typeSymbols) { + void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, ResourceFunction member, + ServicePayloadParamContext serviceContext, + Map typeSymbols) { - Optional resourceMethodSymbolOptional = ctx.semanticModel().symbol(member); - int resourceId = member.hashCode(); + Optional resourceMethodSymbolOptional = member.getSymbol(ctx.semanticModel()); if (resourceMethodSymbolOptional.isEmpty()) { return; } + int resourceId = member.getResourceIdentifierCode(); Optional resourceMethodOptional = resourceMethodSymbolOptional.get().getName(); if (resourceMethodOptional.isPresent()) { @@ -184,9 +210,9 @@ void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, FunctionDef return; // No modification is done for non param resources functions } - List nonAnnotatedParams = new ArrayList<>(); - List annotatedParams = new ArrayList<>(); - ParamAvailability paramAvailability = new ParamAvailability(); + List nonAnnotatedParams = new ArrayList<>(); + List annotatedParams = new ArrayList<>(); + PayloadParamAvailability paramAvailability = new PayloadParamAvailability(); // Disable error diagnostic in the code modifier since this validation is also done in the code analyzer paramAvailability.setErrorDiagnostic(false); int index = 0; @@ -196,29 +222,29 @@ void extractInputParamTypeAndValidate(SyntaxNodeAnalysisContext ctx, FunctionDef isHttpPackageAnnotationTypeDesc(annotationSymbol.typeDescriptor().get())) .collect(Collectors.toList()); if (annotations.isEmpty()) { - nonAnnotatedParams.add(new ParamData(param, index++)); + nonAnnotatedParams.add(new PayloadParamData(param, index++)); } else { - annotatedParams.add(new ParamData(param, index++)); + annotatedParams.add(new PayloadParamData(param, index++)); } } - for (ParamData annotatedParam : annotatedParams) { + for (PayloadParamData annotatedParam : annotatedParams) { validateAnnotatedParams(annotatedParam.getParameterSymbol(), paramAvailability); if (paramAvailability.isAnnotatedPayloadParam()) { return; } } - for (ParamData nonAnnotatedParam : nonAnnotatedParams) { + for (PayloadParamData nonAnnotatedParam : nonAnnotatedParams) { ParameterSymbol parameterSymbol = nonAnnotatedParam.getParameterSymbol(); if (validateNonAnnotatedParams(ctx, parameterSymbol.typeDescriptor(), paramAvailability, parameterSymbol, typeSymbols)) { - ResourceContext resourceContext = - new ResourceContext(parameterSymbol, nonAnnotatedParam.getIndex()); - DocumentContext documentContext = documentContextMap.get(ctx.documentId()); + ResourcePayloadParamContext resourceContext = + new ResourcePayloadParamContext(parameterSymbol, nonAnnotatedParam.getIndex()); + PayloadParamContext documentContext = documentContextMap.get(ctx.documentId()); if (documentContext == null) { - documentContext = new DocumentContext(ctx); + documentContext = new PayloadParamContext(ctx); documentContextMap.put(ctx.documentId(), documentContext); } serviceContext.setResourceContext(resourceId, resourceContext); @@ -240,7 +266,8 @@ private boolean isHttpPackageAnnotationTypeDesc(TypeSymbol typeSymbol) { return id.orgName().equals(BALLERINA) && id.moduleName().startsWith(HTTP); } - public static void validateAnnotatedParams(ParameterSymbol parameterSymbol, ParamAvailability paramAvailability) { + public static void validateAnnotatedParams(ParameterSymbol parameterSymbol, + PayloadParamAvailability paramAvailability) { List annotations = parameterSymbol.annotations().stream() .filter(annotationSymbol -> annotationSymbol.typeDescriptor().isPresent()) .collect(Collectors.toList()); @@ -261,7 +288,7 @@ public static void validateAnnotatedParams(ParameterSymbol parameterSymbol, Para } public static boolean validateNonAnnotatedParams(SyntaxNodeAnalysisContext analysisContext, - TypeSymbol typeSymbol, ParamAvailability paramAvailability, + TypeSymbol typeSymbol, PayloadParamAvailability paramAvailability, ParameterSymbol parameterSymbol, Map typeSymbols) { typeSymbol = getEffectiveType(typeSymbol); @@ -290,7 +317,8 @@ public static boolean validateNonAnnotatedParams(SyntaxNodeAnalysisContext analy } private static boolean isUnionStructuredType(SyntaxNodeAnalysisContext ctx, UnionTypeSymbol unionTypeSymbol, - ParameterSymbol parameterSymbol, ParamAvailability paramAvailability, + ParameterSymbol parameterSymbol, + PayloadParamAvailability paramAvailability, Map typeSymbols) { List typeDescriptors = unionTypeSymbol.memberTypeDescriptors(); boolean foundNonStructuredType = false; @@ -339,7 +367,7 @@ private static boolean isStructuredType(Map typeSymbols, Typ } private static boolean checkErrorsAndReturn(SyntaxNodeAnalysisContext analysisContext, - ParamAvailability availability, ParameterSymbol pSymbol) { + PayloadParamAvailability availability, ParameterSymbol pSymbol) { if (availability.isDefaultPayloadParam() && isDistinctVariable(availability, pSymbol)) { reportAmbiguousPayloadParam(analysisContext, pSymbol, availability); availability.setErrorOccurred(true); @@ -349,28 +377,28 @@ private static boolean checkErrorsAndReturn(SyntaxNodeAnalysisContext analysisCo return true; } - private static boolean isDistinctVariable(ParamAvailability availability, ParameterSymbol pSymbol) { + private static boolean isDistinctVariable(PayloadParamAvailability availability, ParameterSymbol pSymbol) { return !pSymbol.getName().get().equals(availability.getPayloadParamSymbol().getName().get()); } private static void reportAmbiguousPayloadParam(SyntaxNodeAnalysisContext analysisContext, ParameterSymbol parameterSymbol, - ParamAvailability paramAvailability) { + PayloadParamAvailability paramAvailability) { if (paramAvailability.isEnableErrorDiagnostic()) { - updateDiagnostic(analysisContext, parameterSymbol.getLocation().get(), HttpDiagnosticCodes.HTTP_151, + updateDiagnostic(analysisContext, parameterSymbol.getLocation().get(), HttpDiagnostic.HTTP_151, paramAvailability.getPayloadParamSymbol().getName().get(), parameterSymbol.getName().get()); } } private static void reportInvalidUnionPayloadParam(SyntaxNodeAnalysisContext analysisContext, ParameterSymbol parameterSymbol, - ParamAvailability paramAvailability) { + PayloadParamAvailability paramAvailability) { if (paramAvailability.isErrorOccurred()) { return; } if (!paramAvailability.isDefaultPayloadParam()) { if (paramAvailability.isEnableErrorDiagnostic()) { - updateDiagnostic(analysisContext, parameterSymbol.getLocation().get(), HttpDiagnosticCodes.HTTP_152, + updateDiagnostic(analysisContext, parameterSymbol.getLocation().get(), HttpDiagnostic.HTTP_152, parameterSymbol.getName().get()); } paramAvailability.setErrorOccurred(true); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/PayloadAnnotationModifierTask.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/PayloadAnnotationModifierTask.java similarity index 70% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/PayloadAnnotationModifierTask.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/PayloadAnnotationModifierTask.java index e43542777c..51d5d81ff8 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/PayloadAnnotationModifierTask.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/PayloadAnnotationModifierTask.java @@ -16,7 +16,7 @@ * under the License. */ -package io.ballerina.stdlib.http.compiler.codemodifier; +package io.ballerina.stdlib.http.compiler.codemodifier.payload; import io.ballerina.compiler.syntax.tree.AbstractNodeFactory; import io.ballerina.compiler.syntax.tree.AnnotationNode; @@ -24,11 +24,13 @@ import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; import io.ballerina.compiler.syntax.tree.FunctionSignatureNode; import io.ballerina.compiler.syntax.tree.IdentifierToken; +import io.ballerina.compiler.syntax.tree.MethodDeclarationNode; import io.ballerina.compiler.syntax.tree.ModuleMemberDeclarationNode; import io.ballerina.compiler.syntax.tree.ModulePartNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeFactory; import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.ParameterNode; import io.ballerina.compiler.syntax.tree.RequiredParameterNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; @@ -37,6 +39,7 @@ import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.SyntaxTree; import io.ballerina.compiler.syntax.tree.Token; +import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; import io.ballerina.projects.Document; import io.ballerina.projects.DocumentId; import io.ballerina.projects.Module; @@ -44,9 +47,12 @@ import io.ballerina.projects.plugins.ModifierTask; import io.ballerina.projects.plugins.SourceModifierContext; import io.ballerina.stdlib.http.compiler.Constants; -import io.ballerina.stdlib.http.compiler.codemodifier.context.DocumentContext; -import io.ballerina.stdlib.http.compiler.codemodifier.context.ResourceContext; -import io.ballerina.stdlib.http.compiler.codemodifier.context.ServiceContext; +import io.ballerina.stdlib.http.compiler.ResourceFunction; +import io.ballerina.stdlib.http.compiler.ResourceFunctionDeclaration; +import io.ballerina.stdlib.http.compiler.ResourceFunctionDefinition; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.PayloadParamContext; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.ResourcePayloadParamContext; +import io.ballerina.stdlib.http.compiler.codemodifier.payload.context.ServicePayloadParamContext; import io.ballerina.tools.diagnostics.DiagnosticSeverity; import io.ballerina.tools.text.TextDocument; @@ -55,6 +61,9 @@ import java.util.Map; import java.util.Objects; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.isHttpServiceType; +import static io.ballerina.stdlib.http.compiler.HttpServiceValidator.isServiceContractImplementation; + /** * {@code HttpPayloadParamIdentifier} injects the @http:Payload annotation to the Payload param which found during the * initial analysis. @@ -63,9 +72,9 @@ */ public class PayloadAnnotationModifierTask implements ModifierTask { - private final Map documentContextMap; + private final Map documentContextMap; - public PayloadAnnotationModifierTask(Map documentContextMap) { + public PayloadAnnotationModifierTask(Map documentContextMap) { this.documentContextMap = documentContextMap; } @@ -78,15 +87,15 @@ public void modify(SourceModifierContext modifierContext) { return; } - for (Map.Entry entry : documentContextMap.entrySet()) { + for (Map.Entry entry : documentContextMap.entrySet()) { DocumentId documentId = entry.getKey(); - DocumentContext documentContext = entry.getValue(); + PayloadParamContext documentContext = entry.getValue(); modifyPayloadParam(modifierContext, documentId, documentContext); } } private void modifyPayloadParam(SourceModifierContext modifierContext, DocumentId documentId, - DocumentContext documentContext) { + PayloadParamContext documentContext) { ModuleId moduleId = documentId.moduleId(); Module currentModule = modifierContext.currentPackage().module(moduleId); Document currentDoc = currentModule.document(documentId); @@ -103,13 +112,15 @@ private void modifyPayloadParam(SourceModifierContext modifierContext, DocumentI } private NodeList updateMemberNodes(NodeList oldMembers, - DocumentContext documentContext) { + PayloadParamContext documentContext) { List updatedMembers = new ArrayList<>(); for (ModuleMemberDeclarationNode memberNode : oldMembers) { int serviceId; NodeList members; - if (memberNode.kind() == SyntaxKind.SERVICE_DECLARATION) { + if (memberNode.kind() == SyntaxKind.SERVICE_DECLARATION && + !isServiceContractImplementation(documentContext.getContext().semanticModel(), + (ServiceDeclarationNode) memberNode)) { ServiceDeclarationNode serviceNode = (ServiceDeclarationNode) memberNode; serviceId = serviceNode.hashCode(); members = serviceNode.members(); @@ -117,6 +128,12 @@ private NodeList updateMemberNodes(NodeList updateMemberNodes(NodeList resourceMembers = new ArrayList<>(); for (Node member : members) { - if (member.kind() != SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { + ResourceFunction resourceFunctionNode; + if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { + resourceFunctionNode = new ResourceFunctionDefinition((FunctionDefinitionNode) member); + } else if (member.kind() == SyntaxKind.RESOURCE_ACCESSOR_DECLARATION) { + resourceFunctionNode = new ResourceFunctionDeclaration((MethodDeclarationNode) member); + } else { resourceMembers.add(member); continue; } - FunctionDefinitionNode resourceNode = (FunctionDefinitionNode) member; - int resourceId = resourceNode.hashCode(); + + int resourceId = member.hashCode(); if (!serviceContext.containsResource(resourceId)) { resourceMembers.add(member); continue; } - ResourceContext resourceContext = serviceContext.getResourceContext(resourceId); - FunctionSignatureNode functionSignatureNode = resourceNode.functionSignature(); + ResourcePayloadParamContext resourceContext = serviceContext.getResourceContext(resourceId); + FunctionSignatureNode functionSignatureNode = resourceFunctionNode.functionSignature(); SeparatedNodeList parameterNodes = functionSignatureNode.parameters(); List newParameterNodes = new ArrayList<>(); int index = 0; @@ -174,27 +196,34 @@ private NodeList updateMemberNodes(NodeList(newParameterNodes)); signatureModifier.withParameters(separatedNodeList); FunctionSignatureNode updatedFunctionNode = signatureModifier.apply(); - - FunctionDefinitionNode.FunctionDefinitionNodeModifier resourceModifier = resourceNode.modify(); - resourceModifier.withFunctionSignature(updatedFunctionNode); - FunctionDefinitionNode updatedResourceNode = resourceModifier.apply(); + Node updatedResourceNode = resourceFunctionNode.modifyWithSignature(updatedFunctionNode); resourceMembers.add(updatedResourceNode); } NodeList resourceNodeList = AbstractNodeFactory.createNodeList(resourceMembers); - if (memberNode instanceof ServiceDeclarationNode) { - ServiceDeclarationNode serviceNode = (ServiceDeclarationNode) memberNode; + if (memberNode instanceof ServiceDeclarationNode serviceNode) { ServiceDeclarationNode.ServiceDeclarationNodeModifier serviceDeclarationNodeModifier = serviceNode.modify(); ServiceDeclarationNode updatedServiceDeclarationNode = serviceDeclarationNodeModifier.withMembers(resourceNodeList).apply(); updatedMembers.add(updatedServiceDeclarationNode); - } else { - ClassDefinitionNode classDefinitionNode = (ClassDefinitionNode) memberNode; + } else if (memberNode instanceof ClassDefinitionNode classDefinitionNode) { ClassDefinitionNode.ClassDefinitionNodeModifier classDefinitionNodeModifier = classDefinitionNode.modify(); ClassDefinitionNode updatedClassDefinitionNode = classDefinitionNodeModifier.withMembers(resourceNodeList).apply(); updatedMembers.add(updatedClassDefinitionNode); + } else { + TypeDefinitionNode typeDefinitionNode = (TypeDefinitionNode) memberNode; + ObjectTypeDescriptorNode objectTypeDescriptorNode = (ObjectTypeDescriptorNode) typeDefinitionNode + .typeDescriptor(); + ObjectTypeDescriptorNode.ObjectTypeDescriptorNodeModifier objectTypeDescriptorNodeModifier = + objectTypeDescriptorNode.modify(); + ObjectTypeDescriptorNode updatedObjectTypeDescriptorNode = + objectTypeDescriptorNodeModifier.withMembers(resourceNodeList).apply(); + TypeDefinitionNode.TypeDefinitionNodeModifier typeDefinitionNodeModifier = typeDefinitionNode.modify(); + TypeDefinitionNode updatedTypeDefinitionNode = typeDefinitionNodeModifier.withTypeDescriptor( + updatedObjectTypeDescriptorNode).apply(); + updatedMembers.add(updatedTypeDefinitionNode); } } return AbstractNodeFactory.createNodeList(updatedMembers); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ParamAvailability.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamAvailability.java similarity index 94% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ParamAvailability.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamAvailability.java index 88746fef2c..6e9678c539 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ParamAvailability.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamAvailability.java @@ -16,7 +16,7 @@ * under the License. */ -package io.ballerina.stdlib.http.compiler.codemodifier.context; +package io.ballerina.stdlib.http.compiler.codemodifier.payload.context; import io.ballerina.compiler.api.symbols.ParameterSymbol; @@ -25,7 +25,7 @@ * * @since 2201.5.0 */ -public class ParamAvailability { +public class PayloadParamAvailability { private boolean annotatedPayloadParam = false; private boolean errorOccurred = false; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/DocumentContext.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamContext.java similarity index 77% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/DocumentContext.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamContext.java index fa6cfc1838..2a2445c4f6 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/DocumentContext.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamContext.java @@ -16,7 +16,7 @@ * under the License. */ -package io.ballerina.stdlib.http.compiler.codemodifier.context; +package io.ballerina.stdlib.http.compiler.codemodifier.payload.context; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; @@ -28,11 +28,11 @@ * * @since 2201.5.0 */ -public class DocumentContext { +public class PayloadParamContext { private final SyntaxNodeAnalysisContext context; - private final Map serviceContextMap; + private final Map serviceContextMap; - public DocumentContext(SyntaxNodeAnalysisContext context) { + public PayloadParamContext(SyntaxNodeAnalysisContext context) { this.context = context; this.serviceContextMap = new HashMap<>(); } @@ -41,7 +41,7 @@ public SyntaxNodeAnalysisContext getContext() { return context; } - public void setServiceContext(ServiceContext serviceContext) { + public void setServiceContext(ServicePayloadParamContext serviceContext) { serviceContextMap.put(serviceContext.getServiceId(), serviceContext); } @@ -49,7 +49,7 @@ public boolean containsService(int serviceId) { return serviceContextMap.containsKey(serviceId); } - public ServiceContext getServiceContext(int serviceId) { + public ServicePayloadParamContext getServiceContext(int serviceId) { return serviceContextMap.get(serviceId); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ParamData.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamData.java similarity index 88% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ParamData.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamData.java index 4e92ea9b41..46b0f13206 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ParamData.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/PayloadParamData.java @@ -16,7 +16,7 @@ * under the License. */ -package io.ballerina.stdlib.http.compiler.codemodifier.context; +package io.ballerina.stdlib.http.compiler.codemodifier.payload.context; import io.ballerina.compiler.api.symbols.ParameterSymbol; @@ -25,12 +25,12 @@ * * @since 2201.5.0 */ -public class ParamData { +public class PayloadParamData { private ParameterSymbol parameterSymbol; private int index; - public ParamData(ParameterSymbol parameterSymbol, int index) { + public PayloadParamData(ParameterSymbol parameterSymbol, int index) { this.setParameterSymbol(parameterSymbol); this.setIndex(index); } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ResourceContext.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/ResourcePayloadParamContext.java similarity index 85% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ResourceContext.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/ResourcePayloadParamContext.java index f488430303..5eb74cac45 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ResourceContext.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/ResourcePayloadParamContext.java @@ -16,7 +16,7 @@ * under the License. */ -package io.ballerina.stdlib.http.compiler.codemodifier.context; +package io.ballerina.stdlib.http.compiler.codemodifier.payload.context; import io.ballerina.compiler.api.symbols.ParameterSymbol; @@ -25,11 +25,11 @@ * * @since 2201.5.0 */ -public class ResourceContext { +public class ResourcePayloadParamContext { private final int index; private final ParameterSymbol parameterSymbol; - public ResourceContext(ParameterSymbol parameterSymbol, int index) { + public ResourcePayloadParamContext(ParameterSymbol parameterSymbol, int index) { this.parameterSymbol = parameterSymbol; this.index = index; } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ServiceContext.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/ServicePayloadParamContext.java similarity index 77% rename from compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ServiceContext.java rename to compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/ServicePayloadParamContext.java index 2ac018d3da..eacce6003f 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/context/ServiceContext.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/codemodifier/payload/context/ServicePayloadParamContext.java @@ -16,7 +16,7 @@ * under the License. */ -package io.ballerina.stdlib.http.compiler.codemodifier.context; +package io.ballerina.stdlib.http.compiler.codemodifier.payload.context; import java.util.HashMap; import java.util.Map; @@ -26,16 +26,16 @@ * * @since 2201.5.0 */ -public class ServiceContext { +public class ServicePayloadParamContext { private final int serviceId; - private final Map resourceContextMap; + private final Map resourceContextMap; - public ServiceContext(int serviceId) { + public ServicePayloadParamContext(int serviceId) { this.serviceId = serviceId; this.resourceContextMap = new HashMap<>(); } - public void setResourceContext(int resourceId, ResourceContext payloadParamInfo) { + public void setResourceContext(int resourceId, ResourcePayloadParamContext payloadParamInfo) { this.resourceContextMap.put(resourceId, payloadParamInfo); } @@ -51,7 +51,7 @@ public boolean containsResource(int resourceId) { return resourceContextMap.containsKey(resourceId); } - public ResourceContext getResourceContext(int resourceId) { + public ResourcePayloadParamContext getResourceContext(int resourceId) { return resourceContextMap.get(resourceId); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/oas/ServiceContractOasGenerator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/oas/ServiceContractOasGenerator.java new file mode 100644 index 0000000000..d5ce82ecf9 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/oas/ServiceContractOasGenerator.java @@ -0,0 +1,71 @@ +/* + * 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.stdlib.http.compiler.oas; + +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.ObjectTypeDescriptorNode; +import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; +import io.ballerina.openapi.service.mapper.model.ServiceContractType; +import io.ballerina.openapi.service.mapper.model.ServiceNode; +import io.ballerina.projects.ProjectKind; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; + +import java.util.Optional; + +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.diagnosticContainsErrors; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.isHttpServiceType; + +/** + * This class generates the OpenAPI definition resource for the service contract type node. + * + * @since 2.12.0 + */ +public class ServiceContractOasGenerator extends ServiceOasGenerator { + + @Override + public void perform(SyntaxNodeAnalysisContext context) { + if (diagnosticContainsErrors(context) || + !context.currentPackage().project().kind().equals(ProjectKind.BUILD_PROJECT)) { + return; + } + + Node typeNode = context.node(); + if (!isHttpServiceType(context.semanticModel(), typeNode)) { + return; + } + + ObjectTypeDescriptorNode serviceObjectType = (ObjectTypeDescriptorNode) typeNode; + TypeDefinitionNode serviceTypeNode = (TypeDefinitionNode) serviceObjectType.parent(); + + if (!serviceTypeNode.visibilityQualifier() + .map(qualifier -> qualifier.text().equals("public")) + .orElse(false)) { + return; + } + + String fileName = String.format("%s.json", serviceTypeNode.typeName().text()); + ServiceNode serviceNode = new ServiceContractType(serviceTypeNode); + Optional openApi = generateOpenApi(fileName, context.currentPackage().project(), + context.semanticModel(), serviceNode); + if (openApi.isEmpty()) { + return; + } + + writeOpenApiAsTargetResource(context.currentPackage().project(), fileName, openApi.get()); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/oas/ServiceOasGenerator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/oas/ServiceOasGenerator.java new file mode 100644 index 0000000000..d74f0e0230 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/http/compiler/oas/ServiceOasGenerator.java @@ -0,0 +1,172 @@ +/* + * 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.stdlib.http.compiler.oas; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.compiler.syntax.tree.SpecificFieldNode; +import io.ballerina.openapi.service.mapper.ServiceToOpenAPIMapper; +import io.ballerina.openapi.service.mapper.model.OASGenerationMetaInfo; +import io.ballerina.openapi.service.mapper.model.OASResult; +import io.ballerina.openapi.service.mapper.model.ServiceDeclaration; +import io.ballerina.openapi.service.mapper.model.ServiceNode; +import io.ballerina.projects.Project; +import io.ballerina.projects.plugins.AnalysisTask; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.OpenAPI; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Optional; + +import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.normalizeTitle; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.diagnosticContainsErrors; +import static io.ballerina.stdlib.http.compiler.HttpCompilerPluginUtil.getServiceDeclarationNode; + +/** + * This class generates the OpenAPI definition resource for the service declaration node. + * + * @since 2.12.0 + */ +public class ServiceOasGenerator implements AnalysisTask { + + @Override + public void perform(SyntaxNodeAnalysisContext context) { + if (diagnosticContainsErrors(context)) { + return; + } + + ServiceDeclarationNode serviceDeclarationNode = getServiceDeclarationNode(context); + if (serviceDeclarationNode == null) { + return; + } + + Optional serviceInfoAnnotation = getServiceInfoAnnotation(serviceDeclarationNode); + if (serviceInfoAnnotation.isEmpty()) { + return; + } + + boolean embedOpenAPI = retrieveValueFromAnnotation(serviceInfoAnnotation.get(), "embed") + .map(Boolean::parseBoolean) + .orElse(false); + if (!embedOpenAPI) { + return; + } + + Optional contractPath = retrieveValueFromAnnotation(serviceInfoAnnotation.get(), "contract"); + if (contractPath.isPresent()) { + return; + } + + Optional symbol = context.semanticModel().symbol(serviceDeclarationNode); + if (symbol.isEmpty()) { + return; + } + String fileName = getFileName(symbol.get().hashCode()); + + + ServiceNode serviceNode = new ServiceDeclaration(serviceDeclarationNode, context.semanticModel()); + Optional openApi = generateOpenApi(fileName, context.currentPackage().project(), + context.semanticModel(), serviceNode); + if (openApi.isEmpty()) { + return; + } + + writeOpenApiAsTargetResource(context.currentPackage().project(), fileName, openApi.get()); + } + + protected static String getFileName(int hashCode) { + String hashString = Integer.toString(hashCode); + return String.format("openapi_%s.json", + hashString.startsWith("-") ? "0" + hashString.substring(1) : hashString); + } + + protected static void writeOpenApiAsTargetResource(Project project, String fileName, String openApi) { + Path resourcesPath = project.generatedResourcesDir(); + writeFile(fileName, openApi, resourcesPath); + } + + protected static void writeFile(String fileName, String content, Path dirPath) { + Path openApiPath = dirPath.resolve(fileName); + try (FileWriter writer = new FileWriter(openApiPath.toString(), StandardCharsets.UTF_8)) { + writer.write(content); + } catch (IOException e) { + // Add warning diagnostic + } + } + + private Optional getServiceInfoAnnotation(ServiceDeclarationNode serviceDeclarationNode) { + Optional metadata = serviceDeclarationNode.metadata(); + if (metadata.isEmpty()) { + return Optional.empty(); + } + MetadataNode metaData = metadata.get(); + NodeList annotations = metaData.annotations(); + String serviceInfoAnnotation = String.format("%s:%s", "openapi", "ServiceInfo"); + return annotations.stream() + .filter(ann -> serviceInfoAnnotation.equals(ann.annotReference().toString().trim())) + .findFirst(); + } + + private Optional retrieveValueFromAnnotation(AnnotationNode annotation, String fieldName) { + return annotation + .annotValue() + .map(MappingConstructorExpressionNode::fields) + .flatMap(fields -> + fields.stream() + .filter(fld -> fld instanceof SpecificFieldNode) + .map(fld -> (SpecificFieldNode) fld) + .filter(fld -> fieldName.equals(fld.fieldName().toString().trim())) + .findFirst() + ).flatMap(SpecificFieldNode::valueExpr) + .map(en -> en.toString().trim()); + } + + protected Optional generateOpenApi(String fileName, Project project, SemanticModel semanticModel, + ServiceNode serviceNode) { + OASGenerationMetaInfo.OASGenerationMetaInfoBuilder builder = new + OASGenerationMetaInfo.OASGenerationMetaInfoBuilder(); + builder.setServiceNode(serviceNode) + .setSemanticModel(semanticModel) + .setOpenApiFileName(fileName) + .setBallerinaFilePath(null) + .setProject(project); + OASResult oasResult = ServiceToOpenAPIMapper.generateOAS(builder.build()); + Optional openApiOpt = oasResult.getOpenAPI(); + if (oasResult.getDiagnostics().stream().anyMatch( + diagnostic -> diagnostic.getDiagnosticSeverity().equals(DiagnosticSeverity.ERROR)) + || openApiOpt.isEmpty()) { + // Add warning diagnostic + return Optional.empty(); + } + OpenAPI openApi = openApiOpt.get(); + if (openApi.getInfo().getTitle() == null || openApi.getInfo().getTitle().equals("/")) { + openApi.getInfo().setTitle(normalizeTitle(fileName)); + } + return Optional.of(Json.pretty(openApi)); + } +} diff --git a/compiler-plugin/src/main/java/module-info.java b/compiler-plugin/src/main/java/module-info.java index 8372628273..66d065826f 100644 --- a/compiler-plugin/src/main/java/module-info.java +++ b/compiler-plugin/src/main/java/module-info.java @@ -20,4 +20,7 @@ requires io.ballerina.lang; requires io.ballerina.tools.api; requires io.ballerina.parser; + requires io.swagger.v3.core; + requires io.swagger.v3.oas.models; + requires io.ballerina.openapi.service; } diff --git a/gradle.properties b/gradle.properties index 13e4e2d79c..d4ab0649b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.caching=true group=io.ballerina.stdlib version=2.12.0-SNAPSHOT -ballerinaLangVersion=2201.9.0 +ballerinaLangVersion=2201.10.0-20240801-104200-87df251c ballerinaTomlParserVersion=1.2.2 commonsLang3Version=3.12.0 nettyVersion=4.1.108.Final @@ -23,6 +23,8 @@ lz4Version=1.3.0 marshallingVersion=2.0.5.Final protobufVersion=3.20.3 jacocoVersion=0.8.10 +ballerinaToOpenApiVersion=2.1.0-20240801-125916-f3852a9 +swaggerVersion=2.2.9 stdlibIoVersion=1.6.0 stdlibTimeVersion=2.4.0 diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HTTPServicesRegistry.java b/native/src/main/java/io/ballerina/stdlib/http/api/HTTPServicesRegistry.java index 9bca326543..22e36aaeb0 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HTTPServicesRegistry.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HTTPServicesRegistry.java @@ -21,19 +21,29 @@ import io.ballerina.runtime.api.Runtime; import io.ballerina.runtime.api.creators.ErrorCreator; +import io.ballerina.runtime.api.types.ReferenceType; +import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BMap; import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BTypedesc; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import static io.ballerina.stdlib.http.api.HttpConstants.DEFAULT_BASE_PATH; import static io.ballerina.stdlib.http.api.HttpConstants.DEFAULT_HOST; +import static io.ballerina.stdlib.http.api.HttpUtil.checkConfigAnnotationAvailability; /** * This services registry holds all the services of HTTP + WebSocket. This is a singleton class where all HTTP + @@ -44,12 +54,14 @@ public class HTTPServicesRegistry { private static final Logger logger = LoggerFactory.getLogger(HTTPServicesRegistry.class); + private static final BString SERVICE_TYPE = StringUtils.fromString("serviceType"); protected Map servicesMapByHost = new ConcurrentHashMap<>(); protected Map servicesByBasePath; protected List sortedServiceURIs; private Runtime runtime; private boolean possibleLastService = true; + private List serviceContractImpls = new ArrayList<>(); /** * Get ServiceInfo instance for given interface and base path. @@ -95,10 +107,33 @@ public List getSortedServiceURIsByHost(String hostName) { * Register a service into the map. * * @param service requested serviceInfo to be registered. - * @param basePath absolute resource path of the service + * @param basePathFromDeclaration absolute resource path of the service */ - public void registerService(BObject service, String basePath) { - HttpService httpService = HttpService.buildHttpService(service, basePath); + public void registerService(BObject service, String basePathFromDeclaration) { + Optional serviceContractType = getServiceContractType(service); + if (serviceContractType.isPresent()) { + serviceContractImpls.add(service); + return; + } + HttpService httpService = serviceContractType.map(referenceType -> + HttpServiceFromContract.buildHttpService(service, basePathFromDeclaration, + referenceType)).orElseGet( + () -> HttpService.buildHttpService(service, basePathFromDeclaration)); + registerBallerinaService(service, httpService); + } + + public void registerServiceImplementedByContract(BObject service) { + Optional serviceContractType = getServiceContractType(service); + if (serviceContractType.isEmpty()) { + return; + } + HttpService httpService = HttpServiceFromContract.buildHttpService(service, DEFAULT_BASE_PATH, + serviceContractType.get()); + registerBallerinaService(service, httpService); + } + + private void registerBallerinaService(BObject service, HttpService httpService) { + String basePath = httpService.getBasePath(); service.addNativeData(HttpConstants.ABSOLUTE_RESOURCE_PATH, basePath); String hostName = httpService.getHostName(); if (servicesMapByHost.get(hostName) == null) { @@ -127,6 +162,30 @@ public void registerService(BObject service, String basePath) { sortedServiceURIs.sort((basePath1, basePath2) -> basePath2.length() - basePath1.length()); } + public List getServiceContractImpls() { + return serviceContractImpls; + } + + + private static Optional getServiceContractType(BObject service) { + BMap serviceConfig = HttpService.getHttpServiceConfigAnnotation(service); + if (!checkConfigAnnotationAvailability(serviceConfig)) { + return Optional.empty(); + } + + Object serviceType = ((BMap) serviceConfig).get(SERVICE_TYPE); + if (Objects.isNull(serviceType) || !(serviceType instanceof BTypedesc serviceTypeDesc)) { + return Optional.empty(); + } + + Type serviceContractType = serviceTypeDesc.getDescribingType(); + if (Objects.isNull(serviceContractType) || + !(serviceContractType instanceof ReferenceType serviceContractRefType)) { + return Optional.empty(); + } + return Optional.of(serviceContractRefType); + } + public String findTheMostSpecificBasePath(String requestURIPath, Map services, List sortedServiceURIs) { for (Object key : sortedServiceURIs) { diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java index 1819ad152d..0f90f4c859 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java @@ -125,6 +125,7 @@ public final class HttpConstants { public static final BString ANN_CONFIG_ATTR_COMPRESSION = StringUtils.fromString("compression"); public static final BString ANN_CONFIG_ATTR_COMPRESSION_ENABLE = StringUtils.fromString("enable"); public static final BString ANN_CONFIG_ATTR_COMPRESSION_CONTENT_TYPES = StringUtils.fromString("contentTypes"); + public static final BString ANN_CONFIG_BASE_PATH = StringUtils.fromString("basePath"); public static final String ANN_CONFIG_ATTR_CACHE_SIZE = "cacheSize"; public static final String ANN_CONFIG_ATTR_CACHE_VALIDITY_PERIOD = "cacheValidityPeriod"; public static final String ANN_CONFIG_ATTR_WEBSOCKET = "webSocket"; 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 0d3ba28c9d..20d29c39e6 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 @@ -26,6 +26,7 @@ import io.ballerina.runtime.api.types.ArrayType; import io.ballerina.runtime.api.types.MethodType; import io.ballerina.runtime.api.types.ObjectType; +import io.ballerina.runtime.api.types.ResourceMethodType; import io.ballerina.runtime.api.types.ServiceType; import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.utils.TypeUtils; @@ -43,11 +44,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; @@ -70,9 +74,7 @@ public class HttpService implements Service { private static final Logger log = LoggerFactory.getLogger(HttpService.class); - protected static final BString BASE_PATH_FIELD = fromString("basePath"); private static final BString CORS_FIELD = fromString("cors"); - private static final BString VERSIONING_FIELD = fromString("versioning"); private static final BString HOST_FIELD = fromString("host"); private static final BString OPENAPI_DEF_FIELD = fromString("openApiDefinition"); private static final BString MEDIA_TYPE_SUBTYPE_PREFIX = fromString("mediaTypeSubtypePrefix"); @@ -115,7 +117,7 @@ public void setKeepAlive(boolean keepAlive) { this.keepAlive = keepAlive; } - private void setCompressionConfig(BMap compression) { + protected void setCompressionConfig(BMap compression) { this.compression = compression; } @@ -220,7 +222,9 @@ public void setCorsHeaders(CorsHeaders corsHeaders) { } public void setIntrospectionPayload(byte[] introspectionPayload) { - this.introspectionPayload = introspectionPayload.clone(); + if (this.introspectionPayload.length == 0) { + this.introspectionPayload = introspectionPayload.clone(); + } } public byte[] getIntrospectionPayload() { @@ -238,31 +242,36 @@ public URITemplate getUriTemplate() throws URITempl public static HttpService buildHttpService(BObject service, String basePath) { HttpService httpService = new HttpService(service, basePath); BMap serviceConfig = getHttpServiceConfigAnnotation(service); + httpService.populateServiceConfig(serviceConfig); + httpService.populateIntrospectionPayload(); + return httpService; + } + + protected void populateServiceConfig(BMap serviceConfig) { if (checkConfigAnnotationAvailability(serviceConfig)) { - httpService.setCompressionConfig( + this.setCompressionConfig( (BMap) serviceConfig.get(HttpConstants.ANN_CONFIG_ATTR_COMPRESSION)); - httpService.setChunkingConfig(serviceConfig.get(HttpConstants.ANN_CONFIG_ATTR_CHUNKING).toString()); - httpService.setCorsHeaders(CorsHeaders.buildCorsHeaders(serviceConfig.getMapValue(CORS_FIELD))); - httpService.setHostName(serviceConfig.getStringValue(HOST_FIELD).getValue().trim()); - httpService.setIntrospectionPayload(serviceConfig.getArrayValue(OPENAPI_DEF_FIELD).getByteArray()); + this.setChunkingConfig(serviceConfig.get(HttpConstants.ANN_CONFIG_ATTR_CHUNKING).toString()); + this.setCorsHeaders(CorsHeaders.buildCorsHeaders(serviceConfig.getMapValue(CORS_FIELD))); + this.setHostName(serviceConfig.getStringValue(HOST_FIELD).getValue().trim()); + // TODO: Remove once the field is removed from the annotation + this.setIntrospectionPayload(serviceConfig.getArrayValue(OPENAPI_DEF_FIELD).getByteArray()); if (serviceConfig.containsKey(MEDIA_TYPE_SUBTYPE_PREFIX)) { - httpService.setMediaTypeSubtypePrefix(serviceConfig.getStringValue(MEDIA_TYPE_SUBTYPE_PREFIX) + this.setMediaTypeSubtypePrefix(serviceConfig.getStringValue(MEDIA_TYPE_SUBTYPE_PREFIX) .getValue().trim()); } - httpService.setTreatNilableAsOptional(serviceConfig.getBooleanValue(TREAT_NILABLE_AS_OPTIONAL)); - httpService.setConstraintValidation(serviceConfig.getBooleanValue(DATA_VALIDATION)); + this.setTreatNilableAsOptional(serviceConfig.getBooleanValue(TREAT_NILABLE_AS_OPTIONAL)); + this.setConstraintValidation(serviceConfig.getBooleanValue(DATA_VALIDATION)); } else { - httpService.setHostName(HttpConstants.DEFAULT_HOST); + this.setHostName(HttpConstants.DEFAULT_HOST); } - processResources(httpService); - httpService.setAllAllowedMethods(DispatcherUtil.getAllResourceMethods(httpService)); - return httpService; + processResources(this); + this.setAllAllowedMethods(DispatcherUtil.getAllResourceMethods(this)); } - private static void processResources(HttpService httpService) { + protected static void processResources(HttpService httpService) { List httpResources = new ArrayList<>(); - for (MethodType resource : ((ServiceType) TypeUtils.getType( - httpService.getBalService())).getResourceMethods()) { + for (MethodType resource : httpService.getResourceMethods()) { if (!SymbolFlags.isFlagOn(resource.getFlags(), SymbolFlags.RESOURCE)) { continue; } @@ -282,6 +291,10 @@ private static void processResources(HttpService httpService) { httpService.setResources(httpResources); } + protected ResourceMethodType[] getResourceMethods() { + return ((ServiceType) TypeUtils.getType(balService)).getResourceMethods(); + } + private static void processLinks(HttpService httpService, List httpResources) { for (HttpResource targetResource : httpResources) { for (HttpResource.LinkedResourceInfo link : targetResource.getLinkedResources()) { @@ -395,7 +408,7 @@ private static void updateResourceTree(HttpService httpService, List getOpenApiDocFileName(BObject service) { + BMap openApiDocMap = (BMap) ((ObjectType) TypeUtils.getReferredType(TypeUtils.getType(service))).getAnnotation( + fromString("ballerina/lang.annotations:0:IntrospectionDocConfig")); + if (Objects.isNull(openApiDocMap)) { + return Optional.empty(); + } + BString name = openApiDocMap.getStringValue(fromString("name")); + return Objects.isNull(name) ? Optional.empty() : Optional.of(name.getValue()); + } + + protected void populateIntrospectionPayload() { + Optional openApiFileNameOpt = getOpenApiDocFileName(balService); + if (openApiFileNameOpt.isEmpty()) { + return; + } + // Load from resources + String openApiFileName = openApiFileNameOpt.get(); + String openApiDocPath = String.format("resources/openapi_%s.json", + openApiFileName.startsWith("-") ? "0" + openApiFileName.substring(1) : openApiFileName); + try (InputStream is = HttpService.class.getClassLoader().getResourceAsStream(openApiDocPath)) { + if (Objects.isNull(is)) { + log.debug("OpenAPI definition is not available in the resources"); + return; + } + this.setIntrospectionPayload(is.readAllBytes()); + } catch (IOException e) { + log.debug("Error while loading OpenAPI definition from resources", e); + } + } + @Override public String getOasResourceLink() { if (this.oasResourceLinks.isEmpty()) { @@ -544,7 +587,7 @@ public boolean getConstraintValidation() { return constraintValidation; } - private void setConstraintValidation(boolean constraintValidation) { + protected void setConstraintValidation(boolean constraintValidation) { this.constraintValidation = constraintValidation; } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpServiceFromContract.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpServiceFromContract.java new file mode 100644 index 0000000000..531521ac73 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpServiceFromContract.java @@ -0,0 +1,82 @@ +/* + * 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.stdlib.http.api; + +import io.ballerina.runtime.api.types.AnnotatableType; +import io.ballerina.runtime.api.types.ReferenceType; +import io.ballerina.runtime.api.types.ResourceMethodType; +import io.ballerina.runtime.api.types.ServiceType; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.stdlib.http.api.nativeimpl.ModuleUtils; + +import java.util.Objects; + +import static io.ballerina.runtime.api.utils.StringUtils.fromString; +import static io.ballerina.stdlib.http.api.HttpUtil.checkConfigAnnotationAvailability; + +/** + * Represents an HTTP service built from a service contract type. + * + * @since 2.0.0 + */ +public class HttpServiceFromContract extends HttpService { + + private final ReferenceType serviceContractType; + + protected HttpServiceFromContract(BObject service, String basePath, ReferenceType httpServiceContractType) { + super(service, basePath); + this.serviceContractType = httpServiceContractType; + } + + public static HttpService buildHttpService(BObject service, String basePath, + ReferenceType serviceContractType) { + HttpService httpService = new HttpServiceFromContract(service, basePath, serviceContractType); + httpService.populateIntrospectionPayload(); + BMap serviceConfig = getHttpServiceConfigAnnotation(serviceContractType); + httpService.populateServiceConfig(serviceConfig); + return httpService; + } + + @Override + protected void populateServiceConfig(BMap serviceConfig) { + if (checkConfigAnnotationAvailability(serviceConfig)) { + Object basePathFromAnnotation = serviceConfig.get(HttpConstants.ANN_CONFIG_BASE_PATH); + if (Objects.nonNull(basePathFromAnnotation)) { + this.setBasePath(basePathFromAnnotation.toString()); + } + } + super.populateServiceConfig(serviceConfig); + } + + public static BMap getHttpServiceConfigAnnotation(ReferenceType serviceContractType) { + String packagePath = ModuleUtils.getHttpPackageIdentifier(); + String annotationName = HttpConstants.ANN_NAME_HTTP_SERVICE_CONFIG; + String key = packagePath.replaceAll(HttpConstants.REGEX, HttpConstants.SINGLE_SLASH); + if (!(serviceContractType instanceof AnnotatableType annotatableServiceContractType)) { + return null; + } + return (BMap) annotatableServiceContractType.getAnnotation(fromString(key + ":" + annotationName)); + } + + @Override + protected ResourceMethodType[] getResourceMethods() { + return ((ServiceType) TypeUtils.getReferredType(serviceContractType)).getResourceMethods(); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/CreateSimpleHttpClient.java b/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/CreateSimpleHttpClient.java index af38f44b8e..1d622df8ec 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/CreateSimpleHttpClient.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/client/endpoint/CreateSimpleHttpClient.java @@ -19,6 +19,7 @@ package io.ballerina.stdlib.http.api.client.endpoint; import io.ballerina.runtime.api.values.BDecimal; +import io.ballerina.runtime.api.values.BError; import io.ballerina.runtime.api.values.BMap; import io.ballerina.runtime.api.values.BObject; import io.ballerina.runtime.api.values.BString; @@ -156,7 +157,8 @@ public static Object createSimpleHttpClient(BObject httpClient, BMap globalPoolC httpClient.addNativeData(HttpConstants.CLIENT_ENDPOINT_CONFIG, clientEndpointConfig); return null; } catch (Exception ex) { - return HttpUtil.createHttpError(ex.getMessage(), HttpErrorType.GENERIC_CLIENT_ERROR); + return ex instanceof BError ? ex : + HttpUtil.createHttpError(ex.getMessage(), HttpErrorType.GENERIC_CLIENT_ERROR); } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/service/endpoint/Start.java b/native/src/main/java/io/ballerina/stdlib/http/api/service/endpoint/Start.java index 7a93a52a46..d4766ebc34 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/service/endpoint/Start.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/service/endpoint/Start.java @@ -48,10 +48,14 @@ public static Object start(Environment env, BObject listener) { } private static Object startServerConnector(Environment env, BObject serviceEndpoint) { - // TODO : Move this to `register` after this issue is fixed + // TODO : Move these to `register` after this issue is fixed // https://github.com/ballerina-platform/ballerina-lang/issues/33594 - // Get and populate interceptor services HTTPServicesRegistry httpServicesRegistry = getHttpServicesRegistry(serviceEndpoint); + // Register services implemented via service contract type + for (BObject serviceContractImpl : httpServicesRegistry.getServiceContractImpls()) { + httpServicesRegistry.registerServiceImplementedByContract(serviceContractImpl); + } + // Get and populate interceptor services Runtime runtime = getRuntime(env, httpServicesRegistry); try { HttpUtil.populateInterceptorServicesFromListener(serviceEndpoint, runtime);