Skip to content

Commit

Permalink
Merge pull request #577 from dulajdilshan/feature/create-graphql-clas…
Browse files Browse the repository at this point in the history
…s-type

Add `createGraphqlClassType` API to support creating class related to GraphQL designer
  • Loading branch information
dulajdilshan authored Feb 6, 2025
2 parents cc64ee1 + 861d04f commit be47bf0
Show file tree
Hide file tree
Showing 10 changed files with 609 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,22 @@ public JsonElement updateType(Path filePath, TypeData typeData) {
return gson.toJsonTree(textEditsMap);
}

public JsonElement createGraphqlClassType(Path filePath, TypeData typeData) {
List<TextEdit> textEdits = new ArrayList<>();
Map<Path, List<TextEdit>> textEditsMap = new HashMap<>();
textEditsMap.put(filePath, textEdits);

// Generate code snippet for the type
SourceCodeGenerator sourceCodeGenerator = new SourceCodeGenerator();
String codeSnippet = sourceCodeGenerator.generateGraphqlClassType(typeData);

SyntaxTree syntaxTree = this.typeDocument.syntaxTree();
ModulePartNode modulePartNode = syntaxTree.rootNode();
LinePosition startPos = LinePosition.from(modulePartNode.lineRange().endLine().line() + 1, 0);
textEdits.add(new TextEdit(CommonUtils.toRange(startPos), codeSnippet));
return gson.toJsonTree(textEditsMap);
}

private void addMemberTypes(TypeSymbol typeSymbol, Map<String, Symbol> symbolMap) {
// Record
switch (typeSymbol.typeKind()) {
Expand Down Expand Up @@ -383,6 +399,9 @@ private void addDependencyTypes(TypeSymbol typeSymbol, Map<String, Object> refer
Symbol definition = ((TypeReferenceTypeSymbol) typeSymbol).definition();
ModuleInfo moduleInfo = ModuleInfo.from(this.module.descriptor());
String typeName = TypeUtils.generateReferencedTypeId(typeSymbol, moduleInfo);
if (references.containsKey(typeName)) {
return;
}
references.putIfAbsent(typeName, getTypeData(definition));
if (CommonUtils.isWithinPackage(definition, moduleInfo)) {
addDependencyTypes(((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(), references);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package io.ballerina.flowmodelgenerator.core.utils;

import com.google.gson.Gson;
import io.ballerina.flowmodelgenerator.core.model.Function;
import io.ballerina.flowmodelgenerator.core.model.Member;
import io.ballerina.flowmodelgenerator.core.model.NodeKind;
import io.ballerina.flowmodelgenerator.core.model.TypeData;
Expand All @@ -43,6 +44,87 @@ public String generateCodeSnippetForType(TypeData typeData) {
};
}

public String generateGraphqlClassType(TypeData typeData) {
NodeKind nodeKind = typeData.codedata().node();
if (nodeKind != NodeKind.CLASS) {
return "";
}

StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("\nservice class ")
.append(typeData.name())
.append(" {");

// Add inferred fields from functions
for (Function function : typeData.functions()) {
generateInferredGraphqlClassField(function, stringBuilder);
}

// init functions
stringBuilder.append("\n\t").append("function init(");
if (!typeData.functions().isEmpty()) {
for (int i = 0; i < typeData.functions().size(); i++) {
Function function = typeData.functions().get(i);
generateTypeDescriptor(function.returnType(), stringBuilder);
stringBuilder.append(" ").append(function.name());
if (i < typeData.functions().size() - 1) {
stringBuilder.append(", ");
}
}
}

stringBuilder.append(") {");
if (!typeData.functions().isEmpty()) {
for (Function function : typeData.functions()) {
stringBuilder.append("\n\t\tself.")
.append(function.name())
.append(" = ")
.append(function.name()).append(";");
}
}
stringBuilder.append("\n\t}");

// Add resource functions
for (Function function : typeData.functions()) {
if (function.description() != null && !function.description().isEmpty()) {
stringBuilder
.append("\n\t")
.append(CommonUtils.convertToBalDocs(function.description()));
} else {
stringBuilder.append("\n");
}
stringBuilder.append("\t")
.append("resource function ")
.append(function.accessor())
.append(" ")
.append(function.name())
.append("(");

// Function params
for (Member param: function.parameters()) {
generateTypeDescriptor(param.type(), stringBuilder);
stringBuilder.append(" ").append(param.name());
if (param.defaultValue() != null && !param.defaultValue().isEmpty()) {
stringBuilder.append(" = ").append(param.defaultValue());
}
if (function.parameters().indexOf(param) < function.parameters().size() - 1) {
stringBuilder.append(", ");
}
}

stringBuilder.append(") returns ");
generateTypeDescriptor(function.returnType(), stringBuilder);
stringBuilder.append(" {");

// Function body: "return self.<function-name>;"
stringBuilder.append("\n\t\treturn self.").append(function.name()).append(";");

stringBuilder.append("\n\t}");
}

return stringBuilder.append("\n}\n").toString();
}

private String generateEnumCodeSnippet(TypeData typeData) {
StringBuilder stringBuilder = new StringBuilder();

Expand Down Expand Up @@ -296,4 +378,11 @@ private void generateArrayTypeDescriptor(TypeData typeData, StringBuilder string
}
stringBuilder.append("[]");
}

private void generateInferredGraphqlClassField(Function function, StringBuilder stringBuilder) {
stringBuilder.append("\n\t").append("private final ");
generateTypeDescriptor(function.returnType(), stringBuilder);
stringBuilder.append(" ").append(function.name());
stringBuilder.append(";");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ public CompletableFuture<TypeResponse> getGraphqlType(GetTypeRequest request) {
return getType(request);
}

@JsonRequest
public CompletableFuture<TypeUpdateResponse> createGraphqlClassType(TypeUpdateRequest request) {
return CompletableFuture.supplyAsync(() -> {
TypeUpdateResponse response = new TypeUpdateResponse();
try {
Path filePath = Path.of(request.filePath());
this.workspaceManager.loadProject(filePath);
TypeData typeData = (new Gson()).fromJson(request.type(), TypeData.class);
Optional<Document> document = this.workspaceManager.document(filePath);
if (document.isEmpty()) {
return response;
}
TypesManager typesManager = new TypesManager(document.get());
response.setName(typeData.name());
response.setTextEdits(typesManager.createGraphqlClassType(filePath, typeData));
} catch (Throwable e) {
throw new RuntimeException(e);
}
return response;
});
}

@JsonRequest
public CompletableFuture<TypeUpdateResponse> updateType(TypeUpdateRequest request) {
return CompletableFuture.supplyAsync(() -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.ballerina.flowmodelgenerator.extension.typesmanager;

import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import io.ballerina.flowmodelgenerator.extension.AbstractLSTest;
import io.ballerina.flowmodelgenerator.extension.request.TypeUpdateRequest;
import org.eclipse.lsp4j.TextEdit;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Test cases for create graphql related class types.
*
* @since 2.0.0
*/
public class CreateGraphqlClassTypeTest extends AbstractLSTest {
private static final Type textEditListType = new TypeToken<Map<String, List<TextEdit>>>() {
}.getType();

@Override
@Test(dataProvider = "data-provider")
public void test(Path config) throws IOException {
Path configJsonPath = configDir.resolve(config);
TestConfig testConfig = gson.fromJson(Files.newBufferedReader(configJsonPath), TestConfig.class);
TypeUpdateRequest request = new TypeUpdateRequest(
sourceDir.resolve(testConfig.filePath()).toAbsolutePath().toString(), testConfig.type());
JsonElement response = getResponse(request).getAsJsonObject("textEdits");

Map<String, List<TextEdit>> actualTextEdits = gson.fromJson(response, textEditListType);
boolean assertFailure = false;
Map<String, List<TextEdit>> newMap = new HashMap<>();
for (Map.Entry<String, List<TextEdit>> entry : actualTextEdits.entrySet()) {
Path fullPath = Paths.get(entry.getKey());
String relativePath = sourceDir.relativize(fullPath).toString();

List<TextEdit> textEdits = testConfig.output().get(relativePath.replace("\\", "/"));
if (textEdits == null) {
log.info("No text edits found for the file: " + relativePath);
assertFailure = true;
} else if (!assertArray("text edits", entry.getValue(), textEdits)) {
assertFailure = true;
}

newMap.put(relativePath, entry.getValue());
}

if (assertFailure) {
TestConfig updateConfig = new TestConfig(testConfig.filePath(), testConfig.description(),
testConfig.type(), newMap);
// updateConfig(configJsonPath, updateConfig);
Assert.fail(String.format("Failed test: '%s' (%s)", testConfig.description(), configJsonPath));
}
}

@DataProvider(name = "data-provider")
@Override
protected Object[] getConfigsList() {
return new Object[][]{
{Path.of("create_graphql_class_type1.json")},
{Path.of("create_graphql_class_type2.json")}
};
}

@Override
protected String getResourceDir() {
return "types_manager";
}

@Override
protected Class<? extends AbstractLSTest> clazz() {
return CreateGraphqlClassTypeTest.class;
}

@Override
protected String getApiName() {
return "createGraphqlClassType";
}

@Override
protected String getServiceName() {
return "typesManager";
}

private record TestConfig(String filePath, String description, JsonElement type,
Map<String, List<TextEdit>> output) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ under the License.
<class name="io.ballerina.flowmodelgenerator.extension.typesmanager.GetTypeTest"/>
<class name="io.ballerina.flowmodelgenerator.extension.typesmanager.CreateAndUpdateTypeTest"/>
<class name="io.ballerina.flowmodelgenerator.extension.typesmanager.GetGraphqlTypeTest"/>
<class name="io.ballerina.flowmodelgenerator.extension.typesmanager.CreateGraphqlClassTypeTest"/>
</classes>
</test>
</suite>
Loading

0 comments on commit be47bf0

Please sign in to comment.