diff --git a/ballerina-tests/Dependencies.toml b/ballerina-tests/Dependencies.toml index 3848d2fc8..93db6f16b 100644 --- a/ballerina-tests/Dependencies.toml +++ b/ballerina-tests/Dependencies.toml @@ -22,7 +22,7 @@ dependencies = [ [[package]] org = "ballerina" name = "cache" -version = "3.7.0" +version = "3.7.1" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "jballerina.java"}, @@ -119,7 +119,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.2" +version = "2.10.5" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, @@ -311,7 +311,7 @@ dependencies = [ [[package]] org = "ballerina" name = "observe" -version = "1.2.0" +version = "1.2.2" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -383,7 +383,7 @@ modules = [ [[package]] org = "ballerina" name = "websocket" -version = "2.10.0" +version = "2.10.1" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "constraint"}, diff --git a/ballerina-tests/tests/01_listener_test.bal b/ballerina-tests/tests/01_listener_test.bal index 2f2a5ccb7..cafa718ce 100644 --- a/ballerina-tests/tests/01_listener_test.bal +++ b/ballerina-tests/tests/01_listener_test.bal @@ -144,3 +144,45 @@ function testAttachServiceWithSubscriptionToHttp1BasedListener() returns error? check validateNextMessage(wsClient2, expectedMsgPayload, id = "2"); } } + +@test:Config { + groups: ["listener", "service_object"] +} +function testServiceDeclarationUsingLocalServiceObject() returns error? { + graphql:Service localService = service object { + resource function get greeting() returns string { + return "Hello world"; + } + }; + check basicListener.attach(localService, "/local_service_object"); + + string url = "http://localhost:9091/local_service_object"; + string document = string `query { greeting }`; + json actualPayload = check getJsonPayloadFromService(url, document); + json expectedPayload = { + data: { + greeting: "Hello world" + } + }; + check basicListener.detach(localService); + test:assertEquals(actualPayload, expectedPayload); +} + +@test:Config { + groups: ["listener", "service_object"] +} +function testServiceDeclarationUsingObjectField() returns error? { + graphql:Service localService = (new ServiceDeclarationOnObjectField()).getService(); + check basicListener.attach(localService, "/object_field_service_object"); + + string url = "http://localhost:9091/object_field_service_object"; + string document = string `query { greeting }`; + json actualPayload = check getJsonPayloadFromService(url, document); + json expectedPayload = { + data: { + greeting: "Hello world" + } + }; + check basicListener.detach(localService); + test:assertEquals(actualPayload, expectedPayload); +} diff --git a/ballerina-tests/tests/test_services.bal b/ballerina-tests/tests/test_services.bal index 3cdd84bd4..593309d4e 100644 --- a/ballerina-tests/tests/test_services.bal +++ b/ballerina-tests/tests/test_services.bal @@ -2475,3 +2475,19 @@ service /defaultParam on wrappedListener { resource function get nestedField() returns NestedField => new; } + +class ServiceDeclarationOnObjectField { + + private graphql:Service objectFieldService = service object { + resource function get greeting() returns string { + return "Hello world"; + } + }; + + public function init() {} + + public function getService() returns graphql:Service { + return self.objectFieldService; + } + +} diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 150b38832..e23833369 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -25,7 +25,7 @@ modules = [ [[package]] org = "ballerina" name = "cache" -version = "3.7.0" +version = "3.7.1" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "jballerina.java"}, @@ -97,7 +97,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.2" +version = "2.10.5" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, @@ -301,7 +301,7 @@ modules = [ [[package]] org = "ballerina" name = "observe" -version = "1.2.0" +version = "1.2.2" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -376,7 +376,7 @@ modules = [ [[package]] org = "ballerina" name = "websocket" -version = "2.10.0" +version = "2.10.1" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "constraint"}, diff --git a/changelog.md b/changelog.md index ee845543a..97f322670 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -[[#1586] Add Compile Time Schema Generation for Default Parameters](https://github.com/ballerina-platform/module-ballerina-graphql/pull/1586) +-[[#3317] Add Support to Generate GraphQL Schema for Local Service Variable Declaration](https://github.com/ballerina-platform/ballerina-library/issues/3317) ### Changed - [[#4634] Use Aliases in GraphQL Error Path](https://github.com/ballerina-platform/ballerina-standard-library/issues/4634) diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/SchemaGenerationTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/SchemaGenerationTest.java index 67904c145..29d059eb5 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/SchemaGenerationTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/SchemaGenerationTest.java @@ -207,6 +207,27 @@ public void testGraphqlDataLoader() { Assert.assertEquals(diagnosticResult.errorCount(), 0); } + @Test + public void testGraphqlLocalServiceObjects() { + String packagePath = "25_local_service_object_declarations"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 0); + } + + @Test + public void testGraphqlObjectFieldServiceObjects() { + String packagePath = "26_object_field_service_object_declarations"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 0); + } + + @Test + public void testMultipleInlineServiceDeclarations() { + String packagePath = "27_inline_service_declarations"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 0); + } + private DiagnosticResult getDiagnosticResult(String path) { Path projectDirPath = RESOURCE_DIRECTORY.resolve(path); BuildProject project = BuildProject.load(getEnvironmentBuilder(), projectDirPath); diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/22_graphql_service_with_http_service/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/22_graphql_service_with_http_service/service.bal index 1682b5f07..4f4851531 100644 --- a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/22_graphql_service_with_http_service/service.bal +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/22_graphql_service_with_http_service/service.bal @@ -16,6 +16,7 @@ import ballerina/http; import ballerina/graphql; +import ballerina/lang.runtime; service /greeting on new http:Listener(9090) { resource function get greeting() returns string { @@ -23,8 +24,63 @@ service /greeting on new http:Listener(9090) { } } -service graphql:Service /query on new graphql:Listener(8080) { - resource function get name() returns string { - return "Jack"; - } +service / on new graphql:Listener(9091) { + resource function get greeting() returns string { + return "Hello from global listener binding service"; + } +} + +service /greeting on new http:Listener(9092) { + resource function get greeting() returns string { + return "Hello, World!"; + } +} + +graphql:Service globalService = service object { + resource function get greeting() returns string { + return "Hello from global service"; + } +}; + +service /too on new graphql:Listener(9093) { + resource function get greeting() returns string { + return "Hello from global listener binding service too"; + } +} + +class TestService { + private graphql:Service fieldService = service object { + resource function get greeting() returns string { + return "Hello from object field service object"; + } + }; + + public function init() {} + + public function startService() returns error? { + graphql:Listener localListener = check new(9094); + check localListener.attach(self.fieldService); + check localListener.'start(); + runtime:registerListener(localListener); + } +} + +public function main() returns error? { + graphql:Service localService = service object { + resource function get greeting() returns string { + return "Hello from local service 2"; + } + }; + + TestService serviceClass = new (); + check serviceClass.startService(); + + graphql:Listener localListener = check new(9095); + graphql:Listener globalListener = check new(9096); + check localListener.attach(localService); + check globalListener.attach(globalService); + check localListener.'start(); + check globalListener.'start(); + runtime:registerListener(localListener); + runtime:registerListener(globalListener); } diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/25_local_service_object_declarations/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/25_local_service_object_declarations/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/25_local_service_object_declarations/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/25_local_service_object_declarations/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/25_local_service_object_declarations/service.bal new file mode 100644 index 000000000..dfe6b547f --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/25_local_service_object_declarations/service.bal @@ -0,0 +1,39 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// 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/lang.runtime; +import ballerina/graphql; + +public function main() returns error? { + graphql:Service localService1 = service object { + resource function get greeting() returns string { + return "Hello from local service 1"; + } + }; + graphql:Service localService2 = service object { + resource function get greeting() returns string { + return "Hello from local service 1"; + } + }; + graphql:Listener localListener1 = check new(9090); + graphql:Listener localListener2 = check new(9091); + check localListener1.attach(localService1); + check localListener2.attach(localService2); + check localListener1.'start(); + check localListener2.'start(); + runtime:registerListener(localListener1); + runtime:registerListener(localListener2); +} diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/26_object_field_service_object_declarations/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/26_object_field_service_object_declarations/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/26_object_field_service_object_declarations/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/26_object_field_service_object_declarations/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/26_object_field_service_object_declarations/service.bal new file mode 100644 index 000000000..0914ed7f9 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/26_object_field_service_object_declarations/service.bal @@ -0,0 +1,49 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// 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/lang.runtime; +import ballerina/graphql; + +class TestService { + private graphql:Service fieldService1 = service object { + resource function get greeting() returns string { + return "Hello from object field service object 1"; + } + }; + private graphql:Service fieldService2 = service object { + resource function get greeting() returns string { + return "Hello from object field service object 2"; + } + }; + + public function init() {} + + public function startService() returns error? { + graphql:Listener localListener1 = check new(9090); + graphql:Listener localListener2 = check new(9091); + check localListener1.attach(self.fieldService1); + check localListener2.attach(self.fieldService2); + check localListener1.'start(); + check localListener2.'start(); + runtime:registerListener(localListener1); + runtime:registerListener(localListener2); + } +} + +public function main() returns error? { + TestService serviceClass = new (); + check serviceClass.startService(); +} diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/27_inline_service_declarations/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/27_inline_service_declarations/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/27_inline_service_declarations/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/27_inline_service_declarations/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/27_inline_service_declarations/service.bal new file mode 100644 index 000000000..31d71e458 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/27_inline_service_declarations/service.bal @@ -0,0 +1,17 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// 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/lang.runtime; import ballerina/graphql; import ballerina/http; service / on new http:Listener(9090) { resource function get greet() returns string { return "Hello from global http service"; } } public function main() returns error? { graphql:Service gqlService = service object { resource function get greet() returns string { return "Hello from gql service"; } }; http:Service httpService = service object { resource function get greet() returns string { return "Hello from local http service"; } }; graphql:Listener gqlListener = check new(9091); http:Listener httpListener = check new(9092); check gqlListener.attach(gqlService); check httpListener.attach(httpService); check gqlListener.'start(); check httpListener.'start(); runtime:registerListener(gqlListener); runtime:registerListener(httpListener); } diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/28_with_distinct_service_class_type/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/28_with_distinct_service_class_type/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/28_with_distinct_service_class_type/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/28_with_distinct_service_class_type/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/28_with_distinct_service_class_type/service.bal new file mode 100644 index 000000000..476cf33cc --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/28_with_distinct_service_class_type/service.bal @@ -0,0 +1,53 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// 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/lang.runtime; +import ballerina/graphql; + +public distinct service isolated class Person { + private final string name; + + isolated function init(string name) { + self.name = name; + } + + isolated resource function get name() returns string { + return self.name; + } + +} + +class TestService { + private graphql:Service fieldService = service object { + resource function get greeting() returns Person { + return new Person("Rick"); + } + }; + + public function init() {} + + public function startService() returns error? { + graphql:Listener localListener = check new(9090); + check localListener.attach(self.fieldService); + check localListener.'start(); + runtime:registerListener(localListener); + } +} + +public function main() returns error? { + TestService serviceClass = new (); + check serviceClass.startService(); +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/GraphqlCodeModifier.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/GraphqlCodeModifier.java index 829d87897..4b46039f7 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/GraphqlCodeModifier.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/GraphqlCodeModifier.java @@ -45,7 +45,7 @@ public void init(CodeModifierContext modifierContext) { modifierContext.addSyntaxNodeAnalysisTask(new ServiceDeclarationAnalysisTask(this.modifierContextMap), SyntaxKind.SERVICE_DECLARATION); modifierContext.addSyntaxNodeAnalysisTask( - new ModuleLevelVariableDeclarationAnalysisTask(this.modifierContextMap), SyntaxKind.MODULE_VAR_DECL); + new ObjectConstructorAnalysisTask(this.modifierContextMap), SyntaxKind.OBJECT_CONSTRUCTOR); modifierContext.addSyntaxNodeAnalysisTask(new InterceptorAnalysisTask(), SyntaxKind.CLASS_DEFINITION); modifierContext.addSyntaxNodeAnalysisTask(new ListenerValidator(), Arrays.asList(SyntaxKind.IMPLICIT_NEW_EXPRESSION, diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ObjectConstructorAnalysisTask.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ObjectConstructorAnalysisTask.java new file mode 100644 index 000000000..a95bbd7e7 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ObjectConstructorAnalysisTask.java @@ -0,0 +1,85 @@ +// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// 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.graphql.compiler; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.ModuleVariableDeclarationNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.compiler.syntax.tree.ObjectConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.ObjectFieldNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.VariableDeclarationNode; +import io.ballerina.projects.DocumentId; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.stdlib.graphql.commons.types.Schema; +import io.ballerina.stdlib.graphql.compiler.schema.generator.GraphqlModifierContext; +import io.ballerina.stdlib.graphql.compiler.service.InterfaceEntityFinder; +import io.ballerina.stdlib.graphql.compiler.service.validator.ServiceValidator; + +import java.util.Map; +import java.util.Optional; + +import static io.ballerina.stdlib.graphql.commons.utils.Utils.isGraphqlModuleSymbol; +import static io.ballerina.stdlib.graphql.compiler.Utils.hasCompilationErrors; + +public class ObjectConstructorAnalysisTask extends ServiceAnalysisTask { + + public ObjectConstructorAnalysisTask(Map nodeMap) { + super(nodeMap); + } + + @Override + public void perform(SyntaxNodeAnalysisContext context) { + if (hasCompilationErrors(context)) { + return; + } + ObjectConstructorExpressionNode node = (ObjectConstructorExpressionNode) context.node(); + if (!isGraphQLServiceObjectDeclaration(context.semanticModel(), (NonTerminalNode) context.node())) { + return; + } + + InterfaceEntityFinder interfaceEntityFinder = getInterfaceEntityFinder(context.semanticModel()); + ServiceValidator serviceValidator = getServiceValidator(context, node, interfaceEntityFinder); + if (serviceValidator.isErrorOccurred()) { + return; + } + Schema schema = generateSchema(context, interfaceEntityFinder, node, null); + DocumentId documentId = context.documentId(); + addToModifierContextMap(documentId, node.parent(), schema); + } + + public boolean isGraphQLServiceObjectDeclaration(SemanticModel semanticModel, + NonTerminalNode node) { + Node typeReferenceNode; + if (node.parent().kind() == SyntaxKind.LOCAL_VAR_DECL) { + typeReferenceNode = ((VariableDeclarationNode) node.parent()).typedBindingPattern() + .typeDescriptor(); + } else if (node.parent().kind() == SyntaxKind.MODULE_VAR_DECL) { + typeReferenceNode = ((ModuleVariableDeclarationNode) node.parent()).typedBindingPattern() + .typeDescriptor(); + } else if (node.parent().kind() == SyntaxKind.OBJECT_FIELD) { + typeReferenceNode = ((ObjectFieldNode) node.parent()).typeName(); + } else { + return false; + } + Optional typeReferenceSymbol = semanticModel.symbol(typeReferenceNode); + return typeReferenceSymbol.isPresent() && isGraphqlModuleSymbol(typeReferenceSymbol.get()); + } + +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlSourceModifier.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlSourceModifier.java index 4e206f05b..2277c3f06 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlSourceModifier.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlSourceModifier.java @@ -34,7 +34,9 @@ import io.ballerina.compiler.syntax.tree.NodeFactory; import io.ballerina.compiler.syntax.tree.NodeList; import io.ballerina.compiler.syntax.tree.NodeParser; +import io.ballerina.compiler.syntax.tree.NonTerminalNode; import io.ballerina.compiler.syntax.tree.ObjectConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.ObjectFieldNode; import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; @@ -43,6 +45,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.VariableDeclarationNode; import io.ballerina.projects.DocumentId; import io.ballerina.projects.Module; import io.ballerina.projects.plugins.ModifierTask; @@ -55,6 +58,7 @@ import io.ballerina.tools.diagnostics.DiagnosticInfo; import io.ballerina.tools.diagnostics.Location; import io.ballerina.tools.text.TextDocument; +import io.ballerina.tools.text.TextRange; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -65,6 +69,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -121,7 +126,7 @@ public void modify(SourceModifierContext sourceModifierContext) { private ModulePartNode modifyDocument(SourceModifierContext context, ModulePartNode rootNode, GraphqlModifierContext modifierContext) { this.subgraphModulePrefix = getSubgraphModulePrefix(rootNode); - Map nodeMap = new HashMap<>(); + Map modifiedNodes = new HashMap<>(); Map nodeSchemaMap = modifierContext.getNodeSchemaMap(); for (Map.Entry entry : nodeSchemaMap.entrySet()) { Schema schema = entry.getValue(); @@ -135,15 +140,29 @@ private ModulePartNode modifyDocument(SourceModifierContext context, ModulePartN if (targetNode.kind() == SyntaxKind.SERVICE_DECLARATION) { ServiceDeclarationNode updatedNode = modifyServiceDeclarationNode( (ServiceDeclarationNode) targetNode, schemaString, prefix); - nodeMap.put(targetNode, updatedNode); + modifiedNodes.put((NonTerminalNode) targetNode, updatedNode); this.entityTypeNamesMap.put(targetNode, this.entityUnionTypeName); this.entityUnionSuffix++; } else if (targetNode.kind() == SyntaxKind.MODULE_VAR_DECL) { ModuleVariableDeclarationNode graphqlServiceVariableDeclaration = (ModuleVariableDeclarationNode) targetNode; - ModuleVariableDeclarationNode updatedNode = modifyServiceVariableDeclarationNode( + ModuleVariableDeclarationNode updatedNode = modifyModuleLevelServiceDeclarationNode( schemaString, graphqlServiceVariableDeclaration, prefix); - nodeMap.put(targetNode, updatedNode); + modifiedNodes.put((NonTerminalNode) targetNode, updatedNode); + this.entityTypeNamesMap.put(targetNode, this.entityUnionTypeName); + this.entityUnionSuffix++; + } else if (targetNode.kind() == SyntaxKind.LOCAL_VAR_DECL) { + VariableDeclarationNode graphqlServiceVariableDeclaration = (VariableDeclarationNode) targetNode; + VariableDeclarationNode updatedNode = modifyVariableServiceDeclarationNode( + schemaString, graphqlServiceVariableDeclaration, prefix); + modifiedNodes.put((NonTerminalNode) targetNode, updatedNode); + this.entityTypeNamesMap.put(targetNode, this.entityUnionTypeName); + this.entityUnionSuffix++; + } else if (targetNode.kind() == SyntaxKind.OBJECT_FIELD) { + ObjectFieldNode graphqlServiceFieldDeclaration = (ObjectFieldNode) targetNode; + ObjectFieldNode updatedNode = modifyObjectFieldServiceDeclarationNode( + schemaString, graphqlServiceFieldDeclaration, prefix); + modifiedNodes.put((NonTerminalNode) targetNode, updatedNode); this.entityTypeNamesMap.put(targetNode, this.entityUnionTypeName); this.entityUnionSuffix++; } @@ -152,24 +171,13 @@ private ModulePartNode modifyDocument(SourceModifierContext context, ModulePartN e.getMessage()); } } - NodeList members = NodeFactory.createNodeList(); - for (ModuleMemberDeclarationNode member : rootNode.members()) { - if (member.kind() == SyntaxKind.SERVICE_DECLARATION || member.kind() == SyntaxKind.MODULE_VAR_DECL) { - if (nodeMap.containsKey(member)) { - this.entityUnionTypeName = this.entityTypeNamesMap.get(member); - Schema schema = nodeSchemaMap.get(member); - isSubgraph = schema.isSubgraph(); - if (schema.getEntities().size() > 0) { - this.entities = schema.getEntities().stream().map(Type::getName).collect(Collectors.toList()); - members = addEntityTypeDefinition(members); - } - members = members.add((ModuleMemberDeclarationNode) nodeMap.get(member)); - continue; - } - } - members = members.add(member); - } - return rootNode.modify(rootNode.imports(), members, rootNode.eofToken()); + + ArrayList nodesToBeModified = new ArrayList<>(modifiedNodes.keySet()); + nodesToBeModified.sort(Comparator.comparingInt(n -> n.textRange().startOffset())); + List entities = getEntityTypeDefinitions(nodeSchemaMap, nodesToBeModified); + ModulePartNode modifiedRootNode = addServiceDeclarationAnnotations(rootNode, nodesToBeModified, modifiedNodes); + NodeList modifiedMembers = modifiedRootNode.members().addAll(entities); + return modifiedRootNode.modify(modifiedRootNode.imports(), modifiedMembers, modifiedRootNode.eofToken()); } private String getSubgraphModulePrefix(ModulePartNode rootNode) { @@ -197,15 +205,49 @@ private NodeList addEntityTypeDefinition( return moduleMembers.add(typeDefinition); } + private ModulePartNode addServiceDeclarationAnnotations(ModulePartNode rootNode, + ArrayList nodesToBeModified, + Map modifiedNodes) { + int prevModifiedNodeLength = 0; + int prevOriginalNodeLength = 0; + for (NonTerminalNode originalNode : nodesToBeModified) { + int originalNodeStartOffset = originalNode.textRangeWithMinutiae().startOffset() + + prevModifiedNodeLength - prevOriginalNodeLength; + int originalNodeLength = originalNode.textRangeWithMinutiae().length(); + NonTerminalNode replacingNode = rootNode.findNode( + TextRange.from(originalNodeStartOffset, originalNodeLength), + true); + rootNode = rootNode.replace(replacingNode, modifiedNodes.get(originalNode)); + prevModifiedNodeLength += modifiedNodes.get(originalNode).textRangeWithMinutiae().length(); + prevOriginalNodeLength += originalNode.textRangeWithMinutiae().length(); + } + return rootNode; + } + + private List getEntityTypeDefinitions(Map nodeSchemaMap, + ArrayList serviceNodes) { + NodeList entities = NodeFactory.createNodeList(); + for (NonTerminalNode serviceNode : serviceNodes) { + this.entityUnionTypeName = this.entityTypeNamesMap.get(serviceNode); + Schema schema = nodeSchemaMap.get(serviceNode); + isSubgraph = schema.isSubgraph(); + if (schema.getEntities().size() > 0) { + this.entities = schema.getEntities().stream().map(Type::getName).collect(Collectors.toList()); + entities = addEntityTypeDefinition(entities); + } + } + return entities.stream().toList(); + } + private ModuleMemberDeclarationNode getEntityTypeDefinition() { String unionOfEntities = String.join("|", this.entities); return NodeParser.parseModuleMemberDeclaration( "type " + this.entityUnionTypeName + " " + unionOfEntities + ";"); } - private ModuleVariableDeclarationNode modifyServiceVariableDeclarationNode(String schemaString, - ModuleVariableDeclarationNode node, - String prefix) { + private ModuleVariableDeclarationNode modifyModuleLevelServiceDeclarationNode(String schemaString, + ModuleVariableDeclarationNode node, + String prefix) { // noinspection OptionalGetWithoutIsPresent ObjectConstructorExpressionNode graphqlServiceObject = (ObjectConstructorExpressionNode) node.initializer().get(); @@ -214,6 +256,26 @@ private ModuleVariableDeclarationNode modifyServiceVariableDeclarationNode(Strin return node.modify().withInitializer(updatedGraphqlServiceObject).apply(); } + private VariableDeclarationNode modifyVariableServiceDeclarationNode(String schemaString, + VariableDeclarationNode node, + String prefix) { + ObjectConstructorExpressionNode graphqlServiceObject + = (ObjectConstructorExpressionNode) node.initializer().get(); + ObjectConstructorExpressionNode updatedGraphqlServiceObject = modifyServiceObjectNode( + graphqlServiceObject, schemaString, prefix); + return node.modify().withInitializer(updatedGraphqlServiceObject).apply(); + } + + private ObjectFieldNode modifyObjectFieldServiceDeclarationNode(String schemaString, + ObjectFieldNode node, + String prefix) { + ObjectConstructorExpressionNode graphqlServiceObject + = (ObjectConstructorExpressionNode) node.expression().get(); + ObjectConstructorExpressionNode updatedGraphqlServiceObject = modifyServiceObjectNode( + graphqlServiceObject, schemaString, prefix); + return node.modify().withExpression(updatedGraphqlServiceObject).apply(); + } + private ObjectConstructorExpressionNode modifyServiceObjectNode(ObjectConstructorExpressionNode node, String schemaString, String prefix) { NodeList annotations = NodeFactory.createNodeList();