Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@
import io.ballerina.compiler.syntax.tree.NonTerminalNode;
import io.ballerina.compiler.syntax.tree.SyntaxKind;
import io.ballerina.compiler.syntax.tree.SyntaxTree;
import io.ballerina.flowmodelgenerator.core.model.AnnotationAttachment;
import io.ballerina.flowmodelgenerator.core.model.Codedata;
import io.ballerina.flowmodelgenerator.core.model.Member;
import io.ballerina.flowmodelgenerator.core.model.TypeData;
import io.ballerina.flowmodelgenerator.core.utils.SourceCodeGenerator;
import io.ballerina.flowmodelgenerator.core.utils.TypeTransformer;
Expand Down Expand Up @@ -194,13 +196,18 @@ public JsonElement updateType(Path filePath, TypeData typeData) {
Map<Path, List<TextEdit>> textEditsMap = new HashMap<>();
textEditsMap.put(filePath, textEdits);

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

SyntaxTree syntaxTree = this.typeDocument.syntaxTree();
ModulePartNode rootNode = syntaxTree.rootNode();
LineRange lineRange = typeData.codedata().lineRange();

// If editing existing type, remove jsondata:Name annotations from members
if (lineRange != null) {
typeData = removeJsonDataAnnotations(typeData);
}

// Regenerate code snippet for the type
SourceCodeGenerator sourceCodeGenerator = new SourceCodeGenerator();
String codeSnippet = sourceCodeGenerator.generateCodeSnippetForType(typeData);
if (lineRange == null) {
textEdits.add(new TextEdit(CommonUtils.toRange(rootNode.lineRange().endLine()), codeSnippet));
} else {
Expand Down Expand Up @@ -578,6 +585,89 @@ private static String getImportStmt(String org, String module) {
return String.format("%nimport %s/%s;%n", org, module);
}

/**
* Removes jsondata:Name annotations from all members of a TypeData when editing existing types.
* This ensures that edit operations don't carry forward JSON-specific annotations.
*
* @param typeData The TypeData to clean up
* @return A new TypeData with jsondata:Name annotations removed from members
*/
private TypeData removeJsonDataAnnotations(TypeData typeData) {
if (typeData.members() == null || typeData.members().isEmpty()) {
return typeData;
}

// Clean up members by removing jsondata:Name annotations
List<Member> cleanedMembers = new ArrayList<>();
for (Member member : typeData.members()) {
Member cleanedMember = removeJsonDataAnnotationFromMember(member);
cleanedMembers.add(cleanedMember);
}

// Return a new TypeData with the cleaned members
return new TypeData(
typeData.name(),
typeData.editable(),
typeData.metadata(),
typeData.codedata(),
typeData.properties(),
cleanedMembers,
typeData.restMember(),
typeData.includes(),
typeData.functions(),
typeData.annotationAttachments(),
typeData.allowAdditionalFields()
);
}

/**
* Removes jsondata:Name annotation from a single member.
*
* @param member The member to clean up
* @return A new Member without jsondata:Name annotations
*/
private Member removeJsonDataAnnotationFromMember(Member member) {
if (member.annotationAttachments() == null || member.annotationAttachments().isEmpty()) {
return member;
}

// Filter out jsondata:Name annotations
List<AnnotationAttachment> filteredAnnotations = member.annotationAttachments().stream()
.filter(annotation -> !isJsonDataNameAnnotation(annotation))
.toList();

// Return the same member if no annotations were removed
if (filteredAnnotations.size() == member.annotationAttachments().size()) {
return member;
}

// Return a new Member with filtered annotations
return new Member(
member.kind(),
member.refs(),
member.type(),
member.name(),
member.defaultValue(),
member.optional(),
member.readonly(),
member.isGraphqlId(),
member.docs(),
filteredAnnotations.isEmpty() ? null : filteredAnnotations,
member.imports()
);
}

/**
* Checks if an annotation is a jsondata:Name annotation.
*
* @param annotation The annotation to check
* @return true if it's a jsondata:Name annotation, false otherwise
*/
private boolean isJsonDataNameAnnotation(AnnotationAttachment annotation) {
return "jsondata".equals(annotation.modulePrefix()) &&
"Name".equals(annotation.name());
}

public record TypeDataWithRefs(Object type, List<Object> refs) {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
import com.google.gson.JsonPrimitive;
import io.ballerina.flowmodelgenerator.core.TypesManager;
import io.ballerina.flowmodelgenerator.core.converters.exception.JsonToRecordConverterException;
import io.ballerina.flowmodelgenerator.core.model.AnnotationAttachment;
import io.ballerina.flowmodelgenerator.core.model.Member;
import io.ballerina.flowmodelgenerator.core.model.NodeKind;
import io.ballerina.flowmodelgenerator.core.model.Property;
import io.ballerina.flowmodelgenerator.core.model.TypeData;
import org.apache.commons.lang3.StringUtils;
import org.ballerinalang.langserver.commons.workspace.WorkspaceManager;
Expand Down Expand Up @@ -67,6 +69,10 @@ public class JsonToTypeMapper {
private static final String DECIMAL_TYPE = "decimal";
private static final String INT_TYPE = "int";
private static final String ANYDATA_TYPE = "anydata";
public static final String BALLERINA_JSONDATA = "ballerina/data.jsondata";
public static final String JSONDATA = "jsondata";
public static final String NAME = "Name";
public static final String VALUE_PROPERTY = "value";

private final Set<String> existingFieldNamesSet;
private final Map<String, String> updatedFieldNames;
Expand Down Expand Up @@ -241,28 +247,34 @@ private RecordTypeDesc createRecordTypeDesc(JsonObject jsonObject, String record
}

private RecordField createRecordField(Map.Entry<String, JsonElement> entry, boolean isOptional) {
String fieldName = entry.getKey();
String originalFieldName = entry.getKey();
String fieldName = escapeIdentifier(originalFieldName);
JsonElement element = entry.getValue();

String annotation = null;
if (needsJsonAnnotation(originalFieldName, fieldName)) {
annotation = "\"" + originalFieldName.replace("\"", "\\\"") + "\"";
}

if (element.isJsonPrimitive()) {
String typeName = getPrimitiveType(element.getAsJsonPrimitive());
return new RecordField(fieldName, new PrimitiveTypeDesc(typeName), isOptional);
return new RecordField(fieldName, new PrimitiveTypeDesc(typeName), isOptional, annotation);
} else if (element.isJsonObject()) {
String typeName = escapeIdentifier(getRecordName(fieldName.trim()));
String updatedTypeName = getAndUpdateFieldNames(
typeName, false, new ArrayList<>(existingFieldNamesSet), updatedFieldNames
);
return new RecordField(fieldName, new ReferenceTypeDesc(updatedTypeName), isOptional);
return new RecordField(fieldName, new ReferenceTypeDesc(updatedTypeName), isOptional, annotation);
} else if (element.isJsonNull()) {
return new RecordField(fieldName, new NilTypeDesc(), isOptional);
return new RecordField(fieldName, new NilTypeDesc(), isOptional, annotation);
} else if (element.isJsonArray()) {
String typeName = escapeIdentifier(getRecordName(fieldName.trim()));
String updatedTypeName = getAndUpdateFieldNames(
typeName, false, new ArrayList<>(existingFieldNamesSet), updatedFieldNames
);
return new RecordField(fieldName, new ReferenceTypeDesc(updatedTypeName), isOptional);
return new RecordField(fieldName, new ReferenceTypeDesc(updatedTypeName), isOptional, annotation);
}
return new RecordField(fieldName, null);
return new RecordField(fieldName, null, isOptional, annotation);
}

private ArrayTypeDesc createArrayTypeDesc(String typeName, JsonArray jsonArray) {
Expand Down Expand Up @@ -318,6 +330,10 @@ private String getRecordName(String name) {
return (typePrefix == null || typePrefix.isEmpty()) ? cap : StringUtils.capitalize(typePrefix) + cap;
}

private boolean needsJsonAnnotation(String originalName, String escapedName) {
return !originalName.equals(escapedName);
}

private String getNextAvailableTypeName(String baseName) {
int maxSuffix = 0;
String pattern = baseName + "_";
Expand Down Expand Up @@ -391,7 +407,7 @@ private void updateRecordFields(List<RecordField> updatedRecordFields,
boolean isOptional = f1.isOptional || f2.isOptional;

if (f1.type.toString().equals(f2.type.toString()) && isOptional) {
updatedRecordFields.add(new RecordField(fieldName, f1.type, true));
updatedRecordFields.add(new RecordField(fieldName, f1.type, true, f1.annotation));
continue;
}

Expand All @@ -401,42 +417,42 @@ private void updateRecordFields(List<RecordField> updatedRecordFields,

if (f1Type.toString().equals(f2Type.toString())) {
OptionalTypeDesc optionalTypeDesc = new OptionalTypeDesc(f1Type);
updatedRecordFields.add(new RecordField(fieldName, optionalTypeDesc, isOptional));
updatedRecordFields.add(new RecordField(fieldName, optionalTypeDesc, isOptional, f1.annotation));
continue;
}

if (f1Type instanceof NilTypeDesc || f2Type instanceof NilTypeDesc) {
TypeDesc nonNilType = f1Type instanceof NilTypeDesc ? f2Type : f1Type;
if (isNullAsOptional) {
// If null is treated as optional, we add the field as an optional field with the non-nil type.
updatedRecordFields.add(new RecordField(fieldName, nonNilType, true));
updatedRecordFields.add(new RecordField(fieldName, nonNilType, true, f1.annotation));
continue;
}
OptionalTypeDesc optionalNonNilType = new OptionalTypeDesc(nonNilType);
updatedRecordFields.add(new RecordField(fieldName, optionalNonNilType, isOptional));
updatedRecordFields.add(new RecordField(fieldName, optionalNonNilType, isOptional, f1.annotation));
continue;
}

UnionTypeDesc unionTypeDesc = new UnionTypeDesc(List.of(f1Type, f2Type));
OptionalTypeDesc optionalTypeDesc = new OptionalTypeDesc(unionTypeDesc);
updatedRecordFields.add(new RecordField(fieldName, optionalTypeDesc, isOptional));
updatedRecordFields.add(new RecordField(fieldName, optionalTypeDesc, isOptional, f1.annotation));
continue;
}

if (f1.type instanceof NilTypeDesc || f2.type instanceof NilTypeDesc) {
TypeDesc nonNilType = f1.type instanceof NilTypeDesc ? f2.type : f1.type;
if (isNullAsOptional) {
// If null is treated as optional, we add the field as an optional field with the non-nil type.
updatedRecordFields.add(new RecordField(fieldName, nonNilType, true));
updatedRecordFields.add(new RecordField(fieldName, nonNilType, true, f1.annotation));
continue;
}
OptionalTypeDesc optionalNonNilType = new OptionalTypeDesc(nonNilType);
updatedRecordFields.add(new RecordField(fieldName, optionalNonNilType, isOptional));
updatedRecordFields.add(new RecordField(fieldName, optionalNonNilType, isOptional, f1.annotation));
continue;
}

UnionTypeDesc unionTypeDesc = new UnionTypeDesc(List.of(f1.type, f2.type));
updatedRecordFields.add(new RecordField(fieldName, unionTypeDesc, isOptional));
updatedRecordFields.add(new RecordField(fieldName, unionTypeDesc, isOptional, f1.annotation));
}

for (RecordField recordField : differentRecordFields.values()) {
Expand All @@ -453,7 +469,8 @@ private TypeDesc convertToInlineTypeDesc(TypeDesc mainTypeDesc) {
recordFields.add(new RecordField(
field.fieldName,
convertToInlineTypeDesc(field.type),
field.isOptional
field.isOptional,
field.annotation
));
}
yield new RecordTypeDesc(recordFields, !this.allowAdditionalFields);
Expand Down Expand Up @@ -503,17 +520,24 @@ private Object toTypeData(String name, TypeDesc typeDesc) {

return switch (typeDesc) {
case RecordTypeDesc recordTypeDesc -> {
Member.MemberBuilder memberBuilder = new Member.MemberBuilder();
List<Member> members = new ArrayList<>(recordTypeDesc.fields.size());

for (RecordField field : recordTypeDesc.fields) {
members.add(memberBuilder
Member.MemberBuilder memberBuilder = new Member.MemberBuilder();
memberBuilder
.kind(Member.MemberKind.FIELD)
.type(toTypeData(null, field.type))
.name(field.fieldName)
.refs(List.of())
.optional((field.type instanceof NilTypeDesc && isNullAsOptional) || field.isOptional)
.build());
.optional((field.type instanceof NilTypeDesc && isNullAsOptional) || field.isOptional);
if (field.annotation != null) {
Property.Builder<Object> propertyBuilder = new Property.Builder<>(null);
Property property = propertyBuilder.value(field.annotation).build();
AnnotationAttachment annotationAttachment =
new AnnotationAttachment(JSONDATA, NAME, Map.of(VALUE_PROPERTY, property));
memberBuilder.annotationAttachments(List.of(annotationAttachment));
memberBuilder.imports(Map.of(JSONDATA, BALLERINA_JSONDATA));
}
members.add(memberBuilder.build());
}

/*
Expand All @@ -523,6 +547,7 @@ private Object toTypeData(String name, TypeDesc typeDesc) {
* E.g. record {| int id; string name; json...; |} instead of record { int id; string name; }
*/
if (this.allowAdditionalFields) {
Member.MemberBuilder memberBuilder = new Member.MemberBuilder();
typeDataBuilder.restMember(memberBuilder
.kind(Member.MemberKind.FIELD)
.type(JSON_KEYWORD)
Expand Down Expand Up @@ -626,21 +651,40 @@ private static final class RecordField {
final String fieldName;
final TypeDesc type;
boolean isOptional = false;
final String annotation;

RecordField(String fieldName, TypeDesc type) {
this.fieldName = fieldName;
this.type = type;
this.annotation = null;
}

RecordField(String fieldName, TypeDesc type, boolean isOptional) {
this.fieldName = fieldName;
this.type = type;
this.isOptional = isOptional;
this.annotation = null;
}

RecordField(String fieldName, TypeDesc type, boolean isOptional, String annotation) {
this.fieldName = fieldName;
this.type = type;
this.isOptional = isOptional;
this.annotation = annotation;
}

@Override
public String toString() {
return type.toString() + " " + fieldName + (isOptional ? "?" : "") + ";";
StringBuilder sb = new StringBuilder();
if (annotation != null) {
sb.append(annotation).append("\n ");
}
sb.append(type.toString()).append(" ").append(fieldName);
if (isOptional) {
sb.append("?");
}
sb.append(";");
return sb.toString();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public class SourceCodeGenerator {
private static final String LS = System.lineSeparator();

public String generateCodeSnippetForType(TypeData typeData) {
return generateCodeSnippetForType(typeData, false);
}

public String generateCodeSnippetForType(TypeData typeData, boolean isNewType) {
NodeKind nodeKind = typeData.codedata().node();
return switch (nodeKind) {
case SERVICE_DECLARATION, CLASS -> ""; // TODO: Implement this
Expand Down Expand Up @@ -451,7 +455,7 @@ private String generateFunctionParameter(Member member, boolean withDefaultValue
return template.formatted(annotatedTypeDesc, paramName, defaultValue);
}

private static List<AnnotationAttachment> getAnnotationAttachments(Member member) {
private List<AnnotationAttachment> getAnnotationAttachments(Member member) {
List<AnnotationAttachment> copyOfAnnotAttachments;
if (Objects.nonNull(member.annotationAttachments())) {
copyOfAnnotAttachments = new ArrayList<>(member.annotationAttachments());
Expand All @@ -466,6 +470,8 @@ private static List<AnnotationAttachment> getAnnotationAttachments(Member member
null
));
}


return copyOfAnnotAttachments;
}

Expand Down Expand Up @@ -546,6 +552,7 @@ private boolean isReadonlyFlagOn(Map<String, Property> properties) {
return readonlyProperty != null && readonlyProperty.value().equals("true");
}


/**
* Helper method to add imports from a member to the imports map.
*
Expand All @@ -561,6 +568,7 @@ private void addMemberImports(Member member) {
this.imports.putIfAbsent(TypeUtils.GRAPHQL_DEFAULT_MODULE_PREFIX,
TypeUtils.BALLERINA_ORG + "/" + TypeUtils.GRAPHQL_DEFAULT_MODULE_PREFIX);
}

}

/**
Expand All @@ -579,4 +587,5 @@ private void addFunctionImports(Function function) {
TypeUtils.BALLERINA_ORG + "/" + TypeUtils.GRAPHQL_DEFAULT_MODULE_PREFIX);
}
}

}
Loading
Loading