diff --git a/architecture-model-generator/modules/architecture-model-generator-core/build.gradle b/architecture-model-generator/modules/architecture-model-generator-core/build.gradle index 7a74526286..07fc34954f 100644 --- a/architecture-model-generator/modules/architecture-model-generator-core/build.gradle +++ b/architecture-model-generator/modules/architecture-model-generator-core/build.gradle @@ -37,8 +37,10 @@ dependencies { implementation "org.ballerinalang:ballerina-tools-api:${ballerinaLangVersion}" implementation "org.ballerinalang:ballerina-runtime:${ballerinaLangVersion}" implementation "com.google.code.gson:gson:${gsonVersion}" + compileOnly "org.eclipse.lsp4j:org.eclipse.lsp4j:${eclipseLsp4jVersion}" testImplementation "org.testng:testng:${testngVersion}" + testImplementation "org.eclipse.lsp4j:org.eclipse.lsp4j:${eclipseLsp4jVersion}" balTools("org.ballerinalang:jballerina-tools:${ballerinaLangVersion}") { transitive = false diff --git a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapArtifact.java b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapArtifact.java new file mode 100644 index 0000000000..537eb5448b --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapArtifact.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2026, 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.artifactsgenerator.codemap; + +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.tools.text.LinePosition; +import io.ballerina.tools.text.LineRange; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a code artifact extracted from Ballerina source code for the code map. + * + * @param name the name of the artifact + * @param type the type of the artifact (e.g., function, service, class) + * @param range the range in source code where this artifact is located + * @param properties additional properties of the artifact + * @param children nested artifacts contained within this artifact + * @since 1.6.0 + */ +public record CodeMapArtifact(String name, String type, Range range, + Map properties, List children) { + + // Property key constants + private static final String MODIFIERS = "modifiers"; + private static final String DOCUMENTATION = "documentation"; + private static final String COMMENT = "comment"; + + + /** + * Converts a Ballerina LineRange to an LSP4J Range. + * + * @param lineRange the Ballerina line range + * @return the corresponding LSP4J Range + */ + public static Range toRange(LineRange lineRange) { + return new Range(toPosition(lineRange.startLine()), toPosition(lineRange.endLine())); + } + + /** + * Converts a Ballerina LinePosition to an LSP4J Position. + * + * @param linePosition the Ballerina line position + * @return the corresponding LSP4J Position + */ + public static Position toPosition(LinePosition linePosition) { + return new Position(linePosition.line(), linePosition.offset()); + } + + public CodeMapArtifact { + properties = Collections.unmodifiableMap(properties); + children = Collections.unmodifiableList(children); + } + + /** + * Builder class for constructing {@link CodeMapArtifact} instances. + */ + public static class Builder { + private String name; + private String type; + private Range range; + private final Map properties = new HashMap<>(); + private final List children = new ArrayList<>(); + + /** + * Creates a new Builder initialized with the line range from the given syntax node. + * + * @param node the syntax node to extract line range from + */ + public Builder(Node node) { + this.range = toRange(node.lineRange()); + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder modifiers(List modifiers) { + if (!modifiers.isEmpty()) { + this.properties.put(MODIFIERS, new ArrayList<>(modifiers)); + } + return this; + } + + public Builder addProperty(String key, Object value) { + this.properties.put(key, value); + return this; + } + + public Builder addChild(CodeMapArtifact child) { + this.children.add(child); + return this; + } + + public Builder documentation(String documentation) { + return addProperty(DOCUMENTATION, documentation); + } + + public Builder comment(String comment) { + return addProperty(COMMENT, comment); + } + + + /** + * Builds and returns the {@link CodeMapArtifact} instance. + * + * @return the constructed CodeMapArtifact + */ + public CodeMapArtifact build() { + return new CodeMapArtifact(name, type, range, + new HashMap<>(properties), new ArrayList<>(children)); + } + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapFile.java b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapFile.java new file mode 100644 index 0000000000..ad60270a70 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapFile.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, 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.artifactsgenerator.codemap; + +import java.util.Collections; +import java.util.List; + +/** + * Represents a Ballerina source file with its extracted code map artifacts. + * + * @param artifacts the list of code map artifacts extracted from this file + * @since 1.6.0 + */ +public record CodeMapFile(List artifacts) { + + public CodeMapFile { + artifacts = artifacts == null ? Collections.emptyList() : Collections.unmodifiableList(artifacts); + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapFilesTracker.java b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapFilesTracker.java new file mode 100644 index 0000000000..a242267336 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapFilesTracker.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026, 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.artifactsgenerator.codemap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Tracks modified (changed or added) files per project for incremental code map generation. + * This singleton maintains a thread-safe record of file modifications between API calls. + * + * @since 1.6.0 + */ +public class CodeMapFilesTracker { + + // Map: projectKey (URI) -> Set of modified (changed or added) file relative paths + private final Map> modifiedFilesMap; + + private CodeMapFilesTracker() { + this.modifiedFilesMap = new ConcurrentHashMap<>(); + } + + private static class Holder { + private static final CodeMapFilesTracker INSTANCE = new CodeMapFilesTracker(); + } + + /** + * Returns the singleton instance of ChangedFilesTracker. + * + * @return the ChangedFilesTracker instance + */ + public static CodeMapFilesTracker getInstance() { + return Holder.INSTANCE; + } + + /** + * Track a changed file for a given project. + * + * @param projectKey the project identifier + * @param relativePath the relative path of the changed file from project root + */ + public void trackFile(String projectKey, String relativePath) { + modifiedFilesMap + .computeIfAbsent(projectKey, k -> ConcurrentHashMap.newKeySet()) + .add(relativePath); + } + + /** + * Retrieves all tracked modified files for the given project. + * + * @param projectKey the project URI key + * @return list of modified file relative paths, or empty list if none tracked + */ + public List getModifiedFiles(String projectKey) { + Set files = modifiedFilesMap.get(projectKey); + if (files == null || files.isEmpty()) { + return Collections.emptyList(); + } + return new ArrayList<>(files); + } + + /** + * Clears all tracked modified files for the given project. + * + * @param projectKey the project URI key + */ + public void clearModifiedFiles(String projectKey) { + modifiedFilesMap.remove(projectKey); + } + +} diff --git a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapGenerator.java b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapGenerator.java new file mode 100644 index 0000000000..3d024dcc65 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapGenerator.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026, 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.artifactsgenerator.codemap; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.modelgenerator.commons.ModuleInfo; +import io.ballerina.projects.Document; +import io.ballerina.projects.Module; +import io.ballerina.projects.Package; +import io.ballerina.projects.Project; +import io.ballerina.projects.ProjectKind; +import org.ballerinalang.langserver.commons.workspace.WorkspaceManager; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Generates code map from Ballerina projects by extracting artifacts from source files. + * + * @since 1.6.0 + */ +public class CodeMapGenerator { + + /** + * Generates a code map for all files in the given project. + * + * @param project the Ballerina project + * @param workspaceManager the workspace manager to obtain semantic models + * @return a map of relative file paths to their code map files + */ + public static Map generateCodeMap(Project project, WorkspaceManager workspaceManager) { + return generateCodeMap(project, workspaceManager, null); + } + + /** + * Generates a code map for specific files in the given project. If {@code fileNames} is {@code null}, + * all files in the project are processed. + * + * @param project the Ballerina project + * @param workspaceManager the workspace manager to obtain semantic models + * @param fileNames the list of file names to process, or {@code null} to process all files + * @return a map of relative file paths to their code map files + */ + public static Map generateCodeMap(Project project, WorkspaceManager workspaceManager, + List fileNames) { + Package currentPackage = project.currentPackage(); + Map codeMapFiles = new LinkedHashMap<>(); + String projectPath = project.sourceRoot().toAbsolutePath().toString(); + Set targetFiles = fileNames != null ? new HashSet<>(fileNames) : null; + + for (var moduleId : currentPackage.moduleIds()) { + Module module = currentPackage.module(moduleId); + ModuleInfo moduleInfo = ModuleInfo.from(module.descriptor()); + + for (var documentId : module.documentIds()) { + Document document = module.document(documentId); + String fileName = document.name(); + String relativeFilePath = getRelativeFilePath(module, fileName); + + // Ignore the file if it is not in the targeted list. + if (targetFiles != null && !targetFiles.contains(relativeFilePath)) { + continue; + } + + Path filePath = getDocumentPath(project, module, fileName); + Optional semanticModelOpt = workspaceManager.semanticModel(filePath); + if (semanticModelOpt.isEmpty()) { + continue; + } + + SyntaxTree syntaxTree = document.syntaxTree(); + List artifacts = collectArtifactsFromSyntaxTree(projectPath, syntaxTree, + semanticModelOpt.get(), moduleInfo); + + CodeMapFile codeMapFile = new CodeMapFile(artifacts); + codeMapFiles.put(relativeFilePath, codeMapFile); + } + } + + return codeMapFiles; + } + + private static List collectArtifactsFromSyntaxTree(String projectPath, SyntaxTree syntaxTree, + SemanticModel semanticModel, + ModuleInfo moduleInfo) { + List artifacts = new ArrayList<>(); + if (!syntaxTree.containsModulePart()) { + return artifacts; + } + + ModulePartNode rootNode = syntaxTree.rootNode(); + CodeMapNodeTransformer codeMapNodeTransformer = new CodeMapNodeTransformer(projectPath, semanticModel, + moduleInfo); + + // Process imports + rootNode.imports().stream() + .map(importNode -> importNode.apply(codeMapNodeTransformer)) + .flatMap(Optional::stream) + .forEach(artifacts::add); + + // Process other members (functions, services, types, etc.) + rootNode.members().stream() + .map(member -> member.apply(codeMapNodeTransformer)) + .flatMap(Optional::stream) + .forEach(artifacts::add); + + return artifacts; + } + + private static String getRelativeFilePath(Module module, String fileName) { + if (module.isDefaultModule()) { + return fileName; + } + String moduleName = module.moduleName().moduleNamePart(); + return "modules" + File.separator + moduleName + File.separator + fileName; + } + + private static Path getDocumentPath(Project project, Module module, String fileName) { + Path sourceRoot = project.sourceRoot(); + if (project.kind() == ProjectKind.SINGLE_FILE_PROJECT) { + return sourceRoot; + } + if (module.isDefaultModule()) { + return sourceRoot.resolve(fileName); + } + return sourceRoot.resolve("modules").resolve(module.moduleName().moduleNamePart()).resolve(fileName); + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapNodeTransformer.java b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapNodeTransformer.java new file mode 100644 index 0000000000..abaee4b57b --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/artifactsgenerator/codemap/CodeMapNodeTransformer.java @@ -0,0 +1,676 @@ +/* + * Copyright (c) 2026, 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.artifactsgenerator.codemap; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.ClassSymbol; +import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; +import io.ballerina.compiler.api.symbols.Qualifier; +import io.ballerina.compiler.api.symbols.RecordFieldSymbol; +import io.ballerina.compiler.api.symbols.RecordTypeSymbol; +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.VariableSymbol; +import io.ballerina.compiler.syntax.tree.ClassDefinitionNode; +import io.ballerina.compiler.syntax.tree.ConstantDeclarationNode; +import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; +import io.ballerina.compiler.syntax.tree.EnumDeclarationNode; +import io.ballerina.compiler.syntax.tree.EnumMemberNode; +import io.ballerina.compiler.syntax.tree.ExplicitNewExpressionNode; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.FunctionArgumentNode; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.FunctionSignatureNode; +import io.ballerina.compiler.syntax.tree.ImplicitNewExpressionNode; +import io.ballerina.compiler.syntax.tree.ImportDeclarationNode; +import io.ballerina.compiler.syntax.tree.ListenerDeclarationNode; +import io.ballerina.compiler.syntax.tree.MarkdownDocumentationLineNode; +import io.ballerina.compiler.syntax.tree.MarkdownDocumentationNode; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.ModuleVariableDeclarationNode; +import io.ballerina.compiler.syntax.tree.NamedArgumentNode; +import io.ballerina.compiler.syntax.tree.NewExpressionNode; +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.NodeTransformer; +import io.ballerina.compiler.syntax.tree.ObjectFieldNode; +import io.ballerina.compiler.syntax.tree.ParameterNode; +import io.ballerina.compiler.syntax.tree.ParenthesizedArgList; +import io.ballerina.compiler.syntax.tree.PositionalArgumentNode; +import io.ballerina.compiler.syntax.tree.RequiredParameterNode; +import io.ballerina.compiler.syntax.tree.RestParameterNode; +import io.ballerina.compiler.syntax.tree.SeparatedNodeList; +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.TypeDefinitionNode; +import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; +import io.ballerina.modelgenerator.commons.CommonUtils; +import io.ballerina.modelgenerator.commons.ModuleInfo; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static io.ballerina.modelgenerator.commons.CommonUtils.CONNECTOR_TYPE; +import static io.ballerina.modelgenerator.commons.CommonUtils.PERSIST; +import static io.ballerina.modelgenerator.commons.CommonUtils.PERSIST_MODEL_FILE; +import static io.ballerina.modelgenerator.commons.CommonUtils.getPersistModelFilePath; +import static io.ballerina.modelgenerator.commons.CommonUtils.isAiMemoryStore; +import static io.ballerina.modelgenerator.commons.CommonUtils.isAiKnowledgeBase; +import static io.ballerina.modelgenerator.commons.CommonUtils.isAiVectorStore; +import static io.ballerina.modelgenerator.commons.CommonUtils.isPersistClient; + +/** + * Transforms Ballerina syntax tree nodes into {@link CodeMapArtifact} instances. + * + * @since 1.6.0 + */ +class CodeMapNodeTransformer extends NodeTransformer> { + + private final SemanticModel semanticModel; + private final String projectPath; + private final ModuleInfo moduleInfo; + private final boolean extractComments; + + private static final String MAIN_FUNCTION_NAME = "main"; + + // Artifact type constants + private static final String TYPE_FUNCTION = "FUNCTION"; + private static final String TYPE_SERVICE = "SERVICE"; + private static final String TYPE_IMPORT = "IMPORT"; + private static final String TYPE_LISTENER = "LISTENER"; + private static final String TYPE_VARIABLE = "VARIABLE"; + private static final String TYPE_TYPE = "TYPE"; + private static final String TYPE_CLASS = "CLASS"; + private static final String TYPE_FIELD = "FIELD"; + + + // Property key constants + private static final String PROP_PARAMETERS = "parameters"; + private static final String PROP_RETURNS = "returns"; + private static final String PROP_BASE_PATH = "basePath"; + private static final String PROP_PORT = "port"; + private static final String PROP_LISTENER_TYPE = "listenerType"; + private static final String PROP_ORG_NAME = "orgName"; + private static final String PROP_MODULE_NAME = "moduleName"; + private static final String PROP_ALIAS = "alias"; + private static final String PROP_TYPE = "type"; + private static final String PROP_ARGUMENTS = "arguments"; + private static final String PROP_TYPE_DESCRIPTOR = "typeDescriptor"; + private static final String PROP_VALUE = "value"; + private static final String PROP_FIELDS = "fields"; + private static final String PROP_ACCESSOR = "accessor"; + + // Other constants + private static final String RECORD_TYPE_NAME = "record"; + private static final String ENUM_TYPE_NAME = "enum"; + private static final String ALIAS_SEPARATOR = " as "; + + /** + * Creates a new CodeMapNodeTransformer with comment extraction enabled. + * + * @param projectPath the project root path + * @param semanticModel the semantic model for symbol resolution + * @param moduleInfo the module information + */ + CodeMapNodeTransformer(String projectPath, SemanticModel semanticModel, ModuleInfo moduleInfo) { + this(projectPath, semanticModel, moduleInfo, true); + } + + /** + * Creates a new CodeMapNodeTransformer. + * + * @param projectPath the project root path + * @param semanticModel the semantic model for symbol resolution + * @param moduleInfo the module information + * @param extractComments whether to extract comments from nodes + */ + CodeMapNodeTransformer(String projectPath, SemanticModel semanticModel, ModuleInfo moduleInfo, + boolean extractComments) { + this.semanticModel = semanticModel; + this.projectPath = projectPath; + this.moduleInfo = moduleInfo; + this.extractComments = extractComments; + } + + @Override + public Optional transform(FunctionDefinitionNode functionDefinitionNode) { + CodeMapArtifact.Builder functionBuilder = new CodeMapArtifact.Builder(functionDefinitionNode); + String functionName = functionDefinitionNode.functionName().text(); + + List modifiers = extractModifiers(functionDefinitionNode.qualifierList()); + functionBuilder.modifiers(modifiers); + + List parameters = extractParameters(functionDefinitionNode.functionSignature()); + functionBuilder.addProperty(PROP_PARAMETERS, parameters); + + String returnType = extractReturnType(functionDefinitionNode.functionSignature()); + functionBuilder.addProperty(PROP_RETURNS, returnType); + + extractDocumentation(functionDefinitionNode.metadata()).ifPresent(functionBuilder::documentation); + extractInlineComments(functionDefinitionNode).ifPresent(functionBuilder::comment); + + functionBuilder.type(TYPE_FUNCTION); + + if (functionName.equals(MAIN_FUNCTION_NAME)) { + functionBuilder.name(MAIN_FUNCTION_NAME); + } else if (functionDefinitionNode.kind() == SyntaxKind.RESOURCE_ACCESSOR_DEFINITION) { + String pathString = getPathString(functionDefinitionNode.relativeResourcePath()); + functionBuilder + .name(pathString) + .addProperty(PROP_ACCESSOR, functionName); + } else { + functionBuilder.name(functionName); + } + return Optional.of(functionBuilder.build()); + } + + @Override + public Optional transform(ServiceDeclarationNode serviceDeclarationNode) { + CodeMapArtifact.Builder serviceBuilder = new CodeMapArtifact.Builder(serviceDeclarationNode); + + SeparatedNodeList expressions = serviceDeclarationNode.expressions(); + ExpressionNode firstExpression = expressions.isEmpty() ? null : expressions.get(0); + + Optional typeDescriptorNode = serviceDeclarationNode.typeDescriptor(); + NodeList resourcePaths = serviceDeclarationNode.absoluteResourcePath(); + + determineServiceName(serviceDeclarationNode, typeDescriptorNode, resourcePaths, firstExpression) + .ifPresent(serviceBuilder::name); + + String basePath = getPathString(resourcePaths); + serviceBuilder.addProperty(PROP_BASE_PATH, basePath); + + if (firstExpression != null) { + extractPortFromExpression(firstExpression).ifPresent(port -> serviceBuilder.addProperty(PROP_PORT, port)); + extractListenerType(firstExpression).ifPresent(listenerType -> + serviceBuilder.addProperty(PROP_LISTENER_TYPE, listenerType)); + } + + serviceBuilder.type(TYPE_SERVICE); + + extractDocumentation(serviceDeclarationNode.metadata()).ifPresent(serviceBuilder::documentation); + extractInlineComments(serviceDeclarationNode).ifPresent(serviceBuilder::comment); + + serviceDeclarationNode.members().forEach(member -> { + member.apply(this).ifPresent(serviceBuilder::addChild); + }); + + return Optional.of(serviceBuilder.build()); + } + + @Override + public Optional transform(ImportDeclarationNode importDeclarationNode) { + // Extract org name + String orgName = importDeclarationNode.orgName() + .map(org -> org.orgName().text()) + .orElse(""); + + // Extract module name + String moduleName = importDeclarationNode.moduleName().stream() + .map(Token::text) + .collect(Collectors.joining(".")); + + // Extract alias/prefix if present + Optional alias = importDeclarationNode.prefix() + .map(prefix -> prefix.prefix().text()); + + // Build full import name + String fullImportName = orgName.isEmpty() ? moduleName : orgName + "/" + moduleName; + if (alias.isPresent()) { + fullImportName += ALIAS_SEPARATOR + alias.get(); + } + + CodeMapArtifact.Builder importBuilder = new CodeMapArtifact.Builder(importDeclarationNode) + .name(fullImportName) + .type(TYPE_IMPORT); + + // Add individual components as properties + if (!orgName.isEmpty()) { + importBuilder.addProperty(PROP_ORG_NAME, orgName); + } + importBuilder.addProperty(PROP_MODULE_NAME, moduleName); + alias.ifPresent(a -> importBuilder.addProperty(PROP_ALIAS, a)); + + extractInlineComments(importDeclarationNode).ifPresent(importBuilder::comment); + + return Optional.of(importBuilder.build()); + } + + @Override + public Optional transform(ListenerDeclarationNode listenerDeclarationNode) { + CodeMapArtifact.Builder listenerBuilder = new CodeMapArtifact.Builder(listenerDeclarationNode) + .name(listenerDeclarationNode.variableName().text()) + .type(TYPE_LISTENER); + + listenerDeclarationNode.typeDescriptor().flatMap(semanticModel::symbol).ifPresent(symbol -> { + if (symbol instanceof TypeSymbol typeSymbol) { + listenerBuilder.addProperty(PROP_TYPE, + io.ballerina.designmodelgenerator.core.CommonUtils.getTypeSignature(typeSymbol, moduleInfo)); + } + }); + + // Extract initialization arguments + Node initializer = listenerDeclarationNode.initializer(); + if (initializer instanceof NewExpressionNode newExpressionNode) { + List args = extractListenerArguments(newExpressionNode); + if (!args.isEmpty()) { + listenerBuilder.addProperty(PROP_ARGUMENTS, args); + } + } + + extractDocumentation(listenerDeclarationNode.metadata()).ifPresent(listenerBuilder::documentation); + extractInlineComments(listenerDeclarationNode).ifPresent(listenerBuilder::comment); + + return Optional.of(listenerBuilder.build()); + } + + private List extractListenerArguments(NewExpressionNode newExpressionNode) { + List arguments = new ArrayList<>(); + SeparatedNodeList argList = getArgList(newExpressionNode); + + for (FunctionArgumentNode argNode : argList) { + if (argNode instanceof NamedArgumentNode namedArg) { + String argName = namedArg.argumentName().name().text(); + String argValue = normalizeWhitespace(namedArg.expression().toSourceCode()); + arguments.add(argName + " = " + argValue); + } else if (argNode instanceof PositionalArgumentNode positionalArg) { + arguments.add(normalizeWhitespace(positionalArg.expression().toSourceCode())); + } + } + return arguments; + } + + private String normalizeWhitespace(String source) { + // Replace newlines and multiple spaces with single space + return source.replaceAll("\\s+", " ").strip(); + } + + private SeparatedNodeList getArgList(NewExpressionNode newExpressionNode) { + if (newExpressionNode instanceof ExplicitNewExpressionNode explicitNew) { + return explicitNew.parenthesizedArgList().arguments(); + } else if (newExpressionNode instanceof ImplicitNewExpressionNode implicitNew) { + Optional argList = implicitNew.parenthesizedArgList(); + if (argList.isPresent()) { + return argList.get().arguments(); + } + } + return NodeFactory.createSeparatedNodeList(); + } + + @Override + public Optional transform(ConstantDeclarationNode constantDeclarationNode) { + CodeMapArtifact.Builder constantBuilder = new CodeMapArtifact.Builder(constantDeclarationNode) + .name(constantDeclarationNode.variableName().text()) + .type(TYPE_VARIABLE); + + // Extract the type descriptor + constantDeclarationNode.typeDescriptor().ifPresent(typeDesc -> { + String typeString = typeDesc.toSourceCode().strip(); + constantBuilder.addProperty(PROP_TYPE_DESCRIPTOR, typeString); + }); + + // Extract the constant value/initializer + String value = constantDeclarationNode.initializer().toSourceCode().strip(); + constantBuilder.addProperty(PROP_VALUE, value); + + // Extract visibility qualifier (public, etc.) + constantDeclarationNode.visibilityQualifier().ifPresent(visibility -> { + constantBuilder.modifiers(List.of(visibility.text())); + }); + + extractDocumentation(constantDeclarationNode.metadata()).ifPresent(constantBuilder::documentation); + extractInlineComments(constantDeclarationNode).ifPresent(constantBuilder::comment); + + return Optional.of(constantBuilder.build()); + } + + @Override + public Optional transform(ModuleVariableDeclarationNode moduleVariableDeclarationNode) { + CodeMapArtifact.Builder variableBuilder = new CodeMapArtifact.Builder(moduleVariableDeclarationNode) + .name(CommonUtils.getVariableName( + moduleVariableDeclarationNode.typedBindingPattern().bindingPattern())); + + List modifiers = extractModifiers(moduleVariableDeclarationNode.qualifiers()); + variableBuilder.modifiers(modifiers); + + variableBuilder.type(TYPE_VARIABLE); + + if (hasQualifier(moduleVariableDeclarationNode.qualifiers(), SyntaxKind.CONFIGURABLE_KEYWORD)) { + } else { + Optional connection = getConnection(moduleVariableDeclarationNode); + if (connection.isPresent()) { + variableBuilder + .addProperty(PROP_TYPE, connection.get().signature()); + if (isPersistClient(connection.get(), semanticModel)) { + variableBuilder.addProperty(CONNECTOR_TYPE, PERSIST); + getPersistModelFilePath(projectPath) + .ifPresent(modelFile -> variableBuilder.addProperty(PERSIST_MODEL_FILE, modelFile)); + } + } + } + + semanticModel.symbol(moduleVariableDeclarationNode).ifPresent(symbol -> { + if (symbol instanceof VariableSymbol variableSymbol) { + variableBuilder.addProperty(PROP_TYPE, + io.ballerina.designmodelgenerator.core.CommonUtils.getTypeSignature( + variableSymbol.typeDescriptor(), moduleInfo)); + } + }); + + extractDocumentation(moduleVariableDeclarationNode.metadata()).ifPresent(variableBuilder::documentation); + extractInlineComments(moduleVariableDeclarationNode).ifPresent(variableBuilder::comment); + + return Optional.of(variableBuilder.build()); + } + + @Override + public Optional transform(TypeDefinitionNode typeDefinitionNode) { + CodeMapArtifact.Builder typeBuilder = new CodeMapArtifact.Builder(typeDefinitionNode) + .name(typeDefinitionNode.typeName().text()) + .type(TYPE_TYPE); + + semanticModel.symbol(typeDefinitionNode).ifPresent(symbol -> { + if (symbol instanceof TypeDefinitionSymbol typeDefSymbol) { + TypeSymbol typeSymbol = typeDefSymbol.typeDescriptor(); + // For records (including intersection types like "readonly & record"), + // just use "record" since fields are extracted separately + String typeDescriptor = isRecordType(typeSymbol) + ? RECORD_TYPE_NAME + : io.ballerina.designmodelgenerator.core.CommonUtils.getTypeSignature(typeSymbol, moduleInfo); + typeBuilder.addProperty(PROP_TYPE_DESCRIPTOR, typeDescriptor); + } + }); + + List fields = extractFieldsFromTypeDefinition(typeDefinitionNode); + typeBuilder.addProperty(PROP_FIELDS, fields); + + extractDocumentation(typeDefinitionNode.metadata()).ifPresent(typeBuilder::documentation); + extractInlineComments(typeDefinitionNode).ifPresent(typeBuilder::comment); + + return Optional.of(typeBuilder.build()); + } + + @Override + public Optional transform(EnumDeclarationNode enumDeclarationNode) { + CodeMapArtifact.Builder typeBuilder = new CodeMapArtifact.Builder(enumDeclarationNode) + .name(enumDeclarationNode.identifier().text()) + .type(TYPE_TYPE); + + typeBuilder.addProperty(PROP_TYPE_DESCRIPTOR, ENUM_TYPE_NAME); + + List members = new ArrayList<>(); + for (Node memberNode : enumDeclarationNode.enumMemberList()) { + if (memberNode instanceof EnumMemberNode enumMember) { + members.add(enumMember.identifier().text()); + } + } + typeBuilder.addProperty(PROP_FIELDS, members); + + extractDocumentation(enumDeclarationNode.metadata()).ifPresent(typeBuilder::documentation); + extractInlineComments(enumDeclarationNode).ifPresent(typeBuilder::comment); + return Optional.of(typeBuilder.build()); + } + + @Override + public Optional transform(ClassDefinitionNode classDefinitionNode) { + NodeList classTypeQualifiers = classDefinitionNode.classTypeQualifiers(); + + CodeMapArtifact.Builder classBuilder = new CodeMapArtifact.Builder(classDefinitionNode) + .name(classDefinitionNode.className().text()) + .type(TYPE_CLASS) + .modifiers(extractModifiers(classDefinitionNode.visibilityQualifier(), classTypeQualifiers)); + + extractDocumentation(classDefinitionNode.metadata()).ifPresent(classBuilder::documentation); + extractInlineComments(classDefinitionNode).ifPresent(classBuilder::comment); + + classDefinitionNode.members().forEach(member -> { + member.apply(this).ifPresent(classBuilder::addChild); + }); + + return Optional.of(classBuilder.build()); + } + + @Override + public Optional transform(ObjectFieldNode objectFieldNode) { + String fieldName = objectFieldNode.fieldName().text(); + String fieldType = objectFieldNode.typeName().toSourceCode().strip(); + + List modifiers = new ArrayList<>(); + objectFieldNode.visibilityQualifier().ifPresent(token -> modifiers.add(token.text())); + objectFieldNode.qualifierList().forEach(token -> modifiers.add(token.text())); + + CodeMapArtifact.Builder fieldBuilder = new CodeMapArtifact.Builder(objectFieldNode) + .name(fieldName) + .type(TYPE_FIELD) + .modifiers(modifiers); + + fieldBuilder.addProperty(PROP_TYPE, fieldType); + extractInlineComments(objectFieldNode).ifPresent(fieldBuilder::comment); + + return Optional.of(fieldBuilder.build()); + } + + private List extractModifiers(Optional visibilityQualifier, NodeList classTypeQualifiers) { + List modifiers = new ArrayList<>(); + visibilityQualifier.ifPresent(token -> modifiers.add(token.text())); + classTypeQualifiers.forEach(token -> modifiers.add(token.text())); + return modifiers; + } + + @Override + protected Optional transformSyntaxNode(Node node) { + return Optional.empty(); + } + + private List extractModifiers(NodeList qualifierList) { + return qualifierList.stream() + .map(Token::text) + .collect(Collectors.toList()); + } + + private List extractParameters(FunctionSignatureNode functionSignature) { + List parameters = new ArrayList<>(); + SeparatedNodeList parameterNodes = functionSignature.parameters(); + + for (ParameterNode paramNode : parameterNodes) { + if (paramNode instanceof RequiredParameterNode requiredParam) { + String paramType = requiredParam.typeName().toSourceCode().strip(); + String paramName = requiredParam.paramName().map(name -> name.text()).orElse(""); + parameters.add(paramName + ": " + paramType); + } else if (paramNode instanceof DefaultableParameterNode defaultableParam) { + String paramType = defaultableParam.typeName().toSourceCode().strip(); + String paramName = defaultableParam.paramName().map(name -> name.text()).orElse(""); + String defaultValue = defaultableParam.expression().toSourceCode().strip(); + parameters.add(paramName + ": " + paramType + " = " + defaultValue); + } else if (paramNode instanceof RestParameterNode restParam) { + String paramType = restParam.typeName().toSourceCode().strip(); + String paramName = restParam.paramName().map(name -> name.text()).orElse(""); + parameters.add(paramName + ": " + paramType + "..."); + } else { + parameters.add(paramNode.toSourceCode().strip()); + } + } + return parameters; + } + + private String extractReturnType(FunctionSignatureNode functionSignature) { + return functionSignature.returnTypeDesc() + .map(returnTypeDesc -> returnTypeDesc.type().toSourceCode().strip()) + .orElse("()"); + } + + private List extractFieldsFromTypeDefinition(TypeDefinitionNode typeDefinitionNode) { + List fields = new ArrayList<>(); + semanticModel.symbol(typeDefinitionNode).ifPresent(symbol -> { + if (symbol instanceof TypeDefinitionSymbol typeDefSymbol) { + TypeSymbol typeSymbol = typeDefSymbol.typeDescriptor(); + RecordTypeSymbol recordType = getRecordTypeSymbol(typeSymbol); + if (recordType != null) { + for (RecordFieldSymbol field : recordType.fieldDescriptors().values()) { + fields.add(field.getName().orElse("") + ": " + + io.ballerina.designmodelgenerator.core.CommonUtils.getTypeSignature( + field.typeDescriptor(), moduleInfo)); + } + } + } + }); + return fields; + } + + private boolean isRecordType(TypeSymbol typeSymbol) { + return getRecordTypeSymbol(typeSymbol) != null; + } + + private RecordTypeSymbol getRecordTypeSymbol(TypeSymbol typeSymbol) { + if (typeSymbol.typeKind() == TypeDescKind.RECORD) { + return (RecordTypeSymbol) typeSymbol; + } + // Handle intersection types like "readonly & record" + if (typeSymbol.typeKind() == TypeDescKind.INTERSECTION) { + IntersectionTypeSymbol intersectionType = (IntersectionTypeSymbol) typeSymbol; + TypeSymbol effectiveType = intersectionType.effectiveTypeDescriptor(); + if (effectiveType.typeKind() == TypeDescKind.RECORD) { + return (RecordTypeSymbol) effectiveType; + } + } + return null; + } + + private Optional determineServiceName(ServiceDeclarationNode serviceDeclarationNode, + Optional typeDescriptorNode, + NodeList resourcePaths, + ExpressionNode firstExpression) { + if (typeDescriptorNode.isPresent()) { + return Optional.of(typeDescriptorNode.get().toSourceCode().strip()); + } else if (!resourcePaths.isEmpty()) { + return Optional.of(getPathString(resourcePaths)); + } else if (firstExpression != null) { + return Optional.of(firstExpression.toSourceCode().strip()); + } else { + return Optional.empty(); + } + } + + private Optional extractPortFromExpression(ExpressionNode expression) { + String expressionText = expression.toSourceCode().strip(); + if (expressionText.matches(".*\\d+.*")) { + return Optional.of(expressionText.replaceAll("\\D", "")); + } + return Optional.empty(); + } + + private Optional extractListenerType(ExpressionNode expression) { + if (expression instanceof ExplicitNewExpressionNode explicitNewExpr) { + return semanticModel.symbol(explicitNewExpr.typeDescriptor()) + .filter(symbol -> symbol instanceof TypeSymbol) + .map(symbol -> io.ballerina.designmodelgenerator.core.CommonUtils + .getTypeSignature((TypeSymbol) symbol, moduleInfo)); + } + + if (expression instanceof ImplicitNewExpressionNode) { + return semanticModel.typeOf(expression) + .map(typeSymbol -> io.ballerina.designmodelgenerator.core.CommonUtils + .getTypeSignature(typeSymbol, moduleInfo)); + } + return semanticModel.symbol(expression) + .filter(symbol -> symbol instanceof VariableSymbol) + .map(symbol -> ((VariableSymbol) symbol).typeDescriptor()) + .map(typeSymbol -> io.ballerina.designmodelgenerator.core.CommonUtils + .getTypeSignature(typeSymbol, moduleInfo)); + } + + private Optional getConnection(Node node) { + try { + Symbol symbol = semanticModel.symbol(node).orElseThrow(); + TypeReferenceTypeSymbol typeDescriptorSymbol = + (TypeReferenceTypeSymbol) ((VariableSymbol) symbol).typeDescriptor(); + ClassSymbol classSymbol = (ClassSymbol) typeDescriptorSymbol.typeDescriptor(); + if (classSymbol.qualifiers().contains(Qualifier.CLIENT) || isAiKnowledgeBase(classSymbol) + || isAiVectorStore(symbol) || isAiMemoryStore(symbol)) { + return Optional.of(classSymbol); + } + } catch (Throwable e) { + // Ignore + } + return Optional.empty(); + } + + private static String getPathString(NodeList nodes) { + return nodes.stream() + .map(node -> node.toString().trim()) + .collect(Collectors.joining()); + } + + private static boolean hasQualifier(NodeList qualifierList, SyntaxKind kind) { + return qualifierList.stream().anyMatch(qualifier -> qualifier.kind() == kind); + } + + private Optional extractDocumentation(Optional metadata) { + if (metadata.isEmpty()) { + return Optional.empty(); + } + return metadata.get().documentationString() + .filter(node -> node instanceof MarkdownDocumentationNode) + .map(node -> { + MarkdownDocumentationNode docNode = (MarkdownDocumentationNode) node; + StringBuilder description = new StringBuilder(); + for (Node documentationLine : docNode.documentationLines()) { + SyntaxKind lineKind = documentationLine.kind(); + if (lineKind == SyntaxKind.MARKDOWN_DOCUMENTATION_LINE || + lineKind == SyntaxKind.MARKDOWN_REFERENCE_DOCUMENTATION_LINE || + lineKind == SyntaxKind.MARKDOWN_DEPRECATION_DOCUMENTATION_LINE) { + NodeList elements = + ((MarkdownDocumentationLineNode) documentationLine).documentElements(); + elements.forEach(element -> description.append(element.toSourceCode())); + } + } + return description.toString().strip(); + }) + .filter(doc -> !doc.isEmpty()); + } + + private Optional extractInlineComments(Node node) { + if (!extractComments) { + return Optional.empty(); + } + List comments = new ArrayList<>(); + // Extract leading minutiae (comments before the node) + node.leadingMinutiae().forEach(minutiae -> { + if (minutiae.kind() == SyntaxKind.COMMENT_MINUTIAE) { + String commentText = minutiae.text().strip(); + // Remove the leading "//" and trim + if (commentText.startsWith("//")) { + comments.add(commentText.substring(2).strip()); + } + } + }); + if (comments.isEmpty()) { + return Optional.empty(); + } + return Optional.of(String.join(System.lineSeparator(), comments)); + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/designmodelgenerator/core/CommonUtils.java b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/designmodelgenerator/core/CommonUtils.java index 97a1394a70..46fd9cdad9 100644 --- a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/designmodelgenerator/core/CommonUtils.java +++ b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/io/ballerina/designmodelgenerator/core/CommonUtils.java @@ -182,6 +182,21 @@ public static String getTypeSignature(TypeSymbol typeSymbol, ModuleInfo moduleIn return !newText.isEmpty() ? newText.toString() : text; } + /** + * Returns the processed type signature of the type symbol. It removes the organization and the package, and checks + * if it is the default module which will remove the prefix. + * + * @param typeSymbol the type symbol + * @param moduleInfo the module info from model-generator-commons + * @return the processed type signature + */ + public static String getTypeSignature(TypeSymbol typeSymbol, + io.ballerina.modelgenerator.commons.ModuleInfo moduleInfo) { + return getTypeSignature(typeSymbol, + new ModuleInfo(moduleInfo.org(), moduleInfo.packageName(), moduleInfo.moduleName(), + moduleInfo.version())); + } + public record ModuleInfo(String org, String packageName, String moduleName, String version) { public static ModuleInfo from(ModuleID moduleId) { diff --git a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/module-info.java b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/module-info.java index b8574e15c1..c43f9ecb18 100644 --- a/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/module-info.java +++ b/architecture-model-generator/modules/architecture-model-generator-core/src/main/java/module-info.java @@ -26,6 +26,7 @@ requires io.ballerina.model.generator.commons; requires io.ballerina.runtime; requires io.ballerina.toml; + requires org.eclipse.lsp4j; exports io.ballerina.architecturemodelgenerator.core; exports io.ballerina.architecturemodelgenerator.core.diagnostics; @@ -42,4 +43,5 @@ exports io.ballerina.projectservice.core.baltool; exports io.ballerina.copilotagent.core; exports io.ballerina.copilotagent.core.models; + exports io.ballerina.artifactsgenerator.codemap; } diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/DesignModelGeneratorService.java b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/DesignModelGeneratorService.java index 5cef8e2889..1e83403727 100644 --- a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/DesignModelGeneratorService.java +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/DesignModelGeneratorService.java @@ -20,12 +20,16 @@ import io.ballerina.artifactsgenerator.ArtifactsCache; import io.ballerina.artifactsgenerator.ArtifactsGenerator; +import io.ballerina.artifactsgenerator.codemap.CodeMapFilesTracker; +import io.ballerina.artifactsgenerator.codemap.CodeMapGenerator; import io.ballerina.designmodelgenerator.core.DesignModelGenerator; import io.ballerina.designmodelgenerator.core.model.DesignModel; import io.ballerina.designmodelgenerator.extension.request.ArtifactsRequest; +import io.ballerina.designmodelgenerator.extension.request.CodeMapRequest; import io.ballerina.designmodelgenerator.extension.request.GetDesignModelRequest; import io.ballerina.designmodelgenerator.extension.request.ProjectInfoRequest; import io.ballerina.designmodelgenerator.extension.response.ArtifactResponse; +import io.ballerina.designmodelgenerator.extension.response.CodeMapResponse; import io.ballerina.designmodelgenerator.extension.response.GetDesignModelResponse; import io.ballerina.designmodelgenerator.extension.response.ProjectInfoResponse; import io.ballerina.projects.Project; @@ -40,6 +44,7 @@ import org.eclipse.lsp4j.services.LanguageServer; import java.nio.file.Path; +import java.util.List; import java.util.concurrent.CompletableFuture; @JavaSPIService("org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService") @@ -100,6 +105,36 @@ public CompletableFuture artifacts(ArtifactsRequest request) { }); } + @JsonRequest + public CompletableFuture codemap(CodeMapRequest request) { + return CompletableFuture.supplyAsync(() -> { + CodeMapResponse response = new CodeMapResponse(); + try { + Path projectPath = Path.of(request.projectPath()); + WorkspaceManager workspaceManager = workspaceManagerProxy.get(); + Project project = workspaceManager.loadProject(projectPath); + + if (request.changesOnly()) { + String projectKey = projectPath.toUri().toString(); + List modifiedFiles = CodeMapFilesTracker.getInstance() + .getModifiedFiles(projectKey); + + if (modifiedFiles.isEmpty()) { + response.setFiles(java.util.Collections.emptyMap()); + } else { + response.setFiles(CodeMapGenerator.generateCodeMap(project, workspaceManager, modifiedFiles)); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + } + } else { + response.setFiles(CodeMapGenerator.generateCodeMap(project, workspaceManager)); + } + } catch (Throwable e) { + response.setError(e); + } + return response; + }); + } + @JsonRequest public CompletableFuture projectInfo(ProjectInfoRequest request) { return CompletableFuture.supplyAsync(() -> { diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/PublishCodeMapSubscriber.java b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/PublishCodeMapSubscriber.java new file mode 100644 index 0000000000..cdbaf7afa4 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/PublishCodeMapSubscriber.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026, 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.designmodelgenerator.extension; + +import io.ballerina.artifactsgenerator.codemap.CodeMapFilesTracker; +import org.ballerinalang.annotation.JavaSPIService; +import org.ballerinalang.langserver.commons.DocumentServiceContext; +import org.ballerinalang.langserver.commons.LanguageServerContext; +import org.ballerinalang.langserver.commons.client.ExtendedLanguageClient; +import org.ballerinalang.langserver.commons.eventsync.EventKind; +import org.ballerinalang.langserver.commons.eventsync.spi.EventSubscriber; + +import java.nio.file.Path; + +/** + * Tracks changed files for incremental code map generation. + * When a file is opened or changed, this subscriber records the file name. + * The tracked files are used when the client calls the codeMap API. + * + * @since 1.6.0 + */ +@JavaSPIService("org.ballerinalang.langserver.commons.eventsync.spi.EventSubscriber") +public class PublishCodeMapSubscriber implements EventSubscriber { + + public static final String NAME = "Publish code map subscriber"; + private static final String FILE_URI = "file"; + private static final String DID_CHANGE = "text/didChange"; + private static final String DID_OPEN = "text/didOpen"; + + @Override + public EventKind eventKind() { + return EventKind.PROJECT_UPDATE; + } + + @Override + public void onEvent(ExtendedLanguageClient client, DocumentServiceContext context, + LanguageServerContext serverContext) { + // Only track files on didChange and didOpen events + String operationName = context.operation().getName(); + if (!DID_CHANGE.equals(operationName) && !DID_OPEN.equals(operationName)) { + return; + } + + // Skip tracking for AI cloned projects and expression editor + if (!context.fileUri().startsWith(FILE_URI)) { + return; + } + + // Get the project key and relative file path + Path projectPath = context.workspace().projectRoot(context.filePath()); + if (projectPath == null) { + return; + } + String projectKey = projectPath.toUri().toString(); + String relativePath = projectPath.relativize(context.filePath()).toString(); + + // Track the changed file + CodeMapFilesTracker.getInstance().trackFile(projectKey, relativePath); + } + + @Override + public String getName() { + return NAME; + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/request/CodeMapRequest.java b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/request/CodeMapRequest.java new file mode 100644 index 0000000000..ceb8739d7a --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/request/CodeMapRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, 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.designmodelgenerator.extension.request; + +/** + * Record representing a request for code map. + * + * @param projectPath The path to the project for which code map is requested + * @param changesOnly If true, returns code map only for changed files since last request + * @since 1.6.0 + */ +public record CodeMapRequest(String projectPath, boolean changesOnly) { +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/response/CodeMapResponse.java b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/response/CodeMapResponse.java new file mode 100644 index 0000000000..83cac4a030 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/java/io/ballerina/designmodelgenerator/extension/response/CodeMapResponse.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, 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.designmodelgenerator.extension.response; + +import io.ballerina.artifactsgenerator.codemap.CodeMapFile; + +import java.util.Map; + +/** + * Represents the response for code map related operations. + * + * @since 1.6.0 + */ +public class CodeMapResponse extends AbstractResponse { + + private Map files; + + public Map getFiles() { + return files; + } + + public void setFiles(Map files) { + this.files = files; + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/resources/META-INF/services/org.ballerinalang.langserver.commons.eventsync.spi.EventSubscriber b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/resources/META-INF/services/org.ballerinalang.langserver.commons.eventsync.spi.EventSubscriber index 9aa609438c..6f254dd13a 100644 --- a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/resources/META-INF/services/org.ballerinalang.langserver.commons.eventsync.spi.EventSubscriber +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/main/resources/META-INF/services/org.ballerinalang.langserver.commons.eventsync.spi.EventSubscriber @@ -1 +1,2 @@ io.ballerina.designmodelgenerator.extension.PublishArtifactsSubscriber +io.ballerina.designmodelgenerator.extension.PublishCodeMapSubscriber diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/java/io/ballerina/designmodelgenerator/extension/CodeMapGeneratorTest.java b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/java/io/ballerina/designmodelgenerator/extension/CodeMapGeneratorTest.java new file mode 100644 index 0000000000..baec499cb2 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/java/io/ballerina/designmodelgenerator/extension/CodeMapGeneratorTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026, 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.designmodelgenerator.extension; + +import com.google.gson.JsonObject; +import io.ballerina.designmodelgenerator.extension.request.CodeMapRequest; +import io.ballerina.modelgenerator.commons.AbstractLSTest; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Tests for getting the code map for a package. + * + * @since 1.6.0 + */ +public class CodeMapGeneratorTest extends AbstractLSTest { + + @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); + CodeMapRequest request = new CodeMapRequest(getSourcePath(testConfig.source()), false); + JsonObject codeMapResponse = getResponseAndCloseFile(request, testConfig.source()); + JsonObject files = codeMapResponse.getAsJsonObject("files"); + + if (!files.equals(testConfig.output())) { + TestConfig updatedConfig = new TestConfig(testConfig.description(), testConfig.source(), files); +// updateConfig(configJsonPath, updatedConfig); + compareJsonElements(files, testConfig.output()); + Assert.fail(String.format("Failed test: '%s' (%s)", testConfig.description(), configJsonPath)); + } + } + + @Override + protected String getResourceDir() { + return "codemap"; + } + + @Override + protected Class clazz() { + return CodeMapGeneratorTest.class; + } + + @Override + protected String getServiceName() { + return "designModelService"; + } + + @Override + protected String getApiName() { + return "codemap"; + } + + + public record TestConfig(String description, String source, JsonObject output) { + + public String description() { + return description == null ? "" : description; + } + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/java/io/ballerina/designmodelgenerator/extension/PublishCodeMapSubscriberTest.java b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/java/io/ballerina/designmodelgenerator/extension/PublishCodeMapSubscriberTest.java new file mode 100644 index 0000000000..1f211a8d78 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/java/io/ballerina/designmodelgenerator/extension/PublishCodeMapSubscriberTest.java @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2026, 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.designmodelgenerator.extension; + +import com.google.gson.JsonObject; +import io.ballerina.artifactsgenerator.codemap.CodeMapFilesTracker; +import io.ballerina.designmodelgenerator.extension.request.CodeMapRequest; +import io.ballerina.modelgenerator.commons.AbstractLSTest; +import org.ballerinalang.langserver.LSContextOperation; +import org.ballerinalang.langserver.commons.DocumentServiceContext; +import org.ballerinalang.langserver.commons.LSOperation; +import org.ballerinalang.langserver.commons.workspace.WorkspaceDocumentException; +import org.ballerinalang.langserver.commons.workspace.WorkspaceManager; +import org.ballerinalang.langserver.contexts.ContextBuilder; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.mockito.Mockito; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Test cases for PublishCodeMapSubscriber and codeMap API with changesOnly mode. + * + * @since 1.6.0 + */ +public class PublishCodeMapSubscriberTest extends AbstractLSTest { + + private final PublishCodeMapSubscriber publishCodeMapSubscriber = new PublishCodeMapSubscriber(); + + @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); + + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + String sourcePath = getSourcePath(testConfig.source()); + Path filePath = Path.of(sourcePath); + String fileUri = filePath.toAbsolutePath().normalize().toUri().toString(); + + // Create document service context + DocumentServiceContext documentServiceContext = ContextBuilder.buildDocumentServiceContext( + fileUri, + workspaceManager, + LSContextOperation.TXT_DID_CHANGE, + languageServer.getServerContext() + ); + + // Simulate didChange notification + VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(); + List changeEvents = + List.of(new TextDocumentContentChangeEvent(getText(sourcePath))); + + try { + workspaceManager.didChange(filePath, + new DidChangeTextDocumentParams(versionedTextDocumentIdentifier, changeEvents)); + } catch (WorkspaceDocumentException e) { + Assert.fail("Error while sending didChange notification", e); + } + + // Invoke the subscriber to track the changed file + publishCodeMapSubscriber.onEvent( + null, // client is not used by this subscriber + documentServiceContext, + languageServer.getServerContext() + ); + + // Call the codeMap API with changesOnly=true and verify response + Path projectPath = workspaceManager.projectRoot(filePath); + CodeMapRequest request = new CodeMapRequest(projectPath.toString(), true); + JsonObject codeMapResponse = getResponse(request, "designModelService/codemap"); + JsonObject files = codeMapResponse.getAsJsonObject("files"); + + if (!files.equals(testConfig.output())) { + TestConfig updatedConfig = new TestConfig(testConfig.description(), testConfig.source(), files); +// updateConfig(configJsonPath, updatedConfig); + compareJsonElements(files, testConfig.output()); + Assert.fail(String.format("Failed test: '%s' (%s)", testConfig.description(), configJsonPath)); + } + } + + @Test + public void testNoChangesTracked() throws IOException { + // Clear any previously tracked files + String sourcePath = getSourcePath("project"); + Path projectPath = Path.of(sourcePath); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // Call codeMap with changesOnly=true without tracking any files - should return empty + CodeMapRequest request = new CodeMapRequest(sourcePath, true); + JsonObject codeMapResponse = getResponse(request, "designModelService/codemap"); + JsonObject files = codeMapResponse.getAsJsonObject("files"); + + Assert.assertTrue(files.entrySet().isEmpty(), + "Expected empty files response when no changes are tracked"); + } + + @Test + public void testSubscriberEventKind() { + Assert.assertEquals(publishCodeMapSubscriber.eventKind(), + org.ballerinalang.langserver.commons.eventsync.EventKind.PROJECT_UPDATE, + "Subscriber should respond to PROJECT_UPDATE events"); + } + + @Test + public void testSubscriberName() { + Assert.assertEquals(publishCodeMapSubscriber.getName(), + PublishCodeMapSubscriber.NAME, + "Subscriber name should match"); + } + + @Test + public void testSkipsAiUri() throws IOException { + // Test that AI URI files are skipped + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + String sourcePath = getSourcePath("project/main.bal"); + Path filePath = Path.of(sourcePath); + + // Clear tracker first + Path projectPath = workspaceManager.projectRoot(filePath); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // Create a mock context with AI URI + DocumentServiceContext mockContext = Mockito.mock(DocumentServiceContext.class); + LSOperation mockOperation = Mockito.mock(LSOperation.class); + Mockito.when(mockOperation.getName()).thenReturn("text/didChange"); + Mockito.when(mockContext.operation()).thenReturn(mockOperation); + Mockito.when(mockContext.fileUri()).thenReturn("ai://test/main.bal"); + + // This should not track the file due to AI URI (returns early before accessing workspace) + publishCodeMapSubscriber.onEvent(null, mockContext, languageServer.getServerContext()); + + // Verify nothing was tracked + List trackedFiles = CodeMapFilesTracker.getInstance().getModifiedFiles(projectKey); + Assert.assertTrue(trackedFiles.isEmpty(), "AI URI files should not be tracked"); + } + + @Test + public void testSkipsExprUri() throws IOException { + // Test that expr URI files are skipped + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + String sourcePath = getSourcePath("project/main.bal"); + Path filePath = Path.of(sourcePath); + + // Clear tracker first + Path projectPath = workspaceManager.projectRoot(filePath); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // Create a mock context with expr URI + DocumentServiceContext mockContext = Mockito.mock(DocumentServiceContext.class); + LSOperation mockOperation = Mockito.mock(LSOperation.class); + Mockito.when(mockOperation.getName()).thenReturn("text/didChange"); + Mockito.when(mockContext.operation()).thenReturn(mockOperation); + Mockito.when(mockContext.fileUri()).thenReturn("expr://test/main.bal"); + + // This should not track the file due to expr URI (returns early before accessing workspace) + publishCodeMapSubscriber.onEvent(null, mockContext, languageServer.getServerContext()); + + // Verify nothing was tracked + List trackedFiles = CodeMapFilesTracker.getInstance().getModifiedFiles(projectKey); + Assert.assertTrue(trackedFiles.isEmpty(), "Expr URI files should not be tracked"); + } + + @Test + public void testSkipsNonTrackedOperations() throws IOException { + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + String sourcePath = getSourcePath("project/main.bal"); + Path filePath = Path.of(sourcePath); + String fileUri = filePath.toAbsolutePath().normalize().toUri().toString(); + + // Create a context with a different operation (not didChange or didOpen) + DocumentServiceContext documentServiceContext = ContextBuilder.buildDocumentServiceContext( + fileUri, + workspaceManager, + LSContextOperation.TXT_HOVER, + languageServer.getServerContext() + ); + + // Clear tracker first + Path projectPath = workspaceManager.projectRoot(filePath); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // This should not track the file due to non-tracked operation + publishCodeMapSubscriber.onEvent(null, documentServiceContext, languageServer.getServerContext()); + + // Verify nothing was tracked + List trackedFiles = CodeMapFilesTracker.getInstance().getModifiedFiles(projectKey); + Assert.assertTrue(trackedFiles.isEmpty(), "Non-tracked operations should not track files"); + } + + @Test + public void testTracksDidOpenEvents() throws IOException { + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + String sourcePath = getSourcePath("project/main.bal"); + Path filePath = Path.of(sourcePath); + String fileUri = filePath.toAbsolutePath().normalize().toUri().toString(); + + // Clear tracker first + Path projectPath = workspaceManager.projectRoot(filePath); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // Create a context with didOpen operation + DocumentServiceContext documentServiceContext = ContextBuilder.buildDocumentServiceContext( + fileUri, + workspaceManager, + LSContextOperation.TXT_DID_OPEN, + languageServer.getServerContext() + ); + + // This should track the file due to didOpen operation + publishCodeMapSubscriber.onEvent(null, documentServiceContext, languageServer.getServerContext()); + + // Verify file was tracked + List trackedFiles = CodeMapFilesTracker.getInstance().getModifiedFiles(projectKey); + Assert.assertEquals(trackedFiles.size(), 1, "didOpen should track the file"); + Assert.assertTrue(trackedFiles.contains("main.bal"), "main.bal should be tracked"); + + // Cleanup + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + } + + @Test + public void testTracksBothDidChangeAndDidOpenEvents() throws IOException { + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + + // Get paths for two different files in the same project + String sourcePath1 = getSourcePath("project/main.bal"); + String sourcePath2 = getSourcePath("project/service.bal"); + Path filePath1 = Path.of(sourcePath1); + Path filePath2 = Path.of(sourcePath2); + + // Clear tracker first + Path projectPath = workspaceManager.projectRoot(filePath1); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // Trigger didOpen event for first file (simulating new file opened) + String fileUri1 = filePath1.toAbsolutePath().normalize().toUri().toString(); + DocumentServiceContext openContext = ContextBuilder.buildDocumentServiceContext( + fileUri1, workspaceManager, LSContextOperation.TXT_DID_OPEN, languageServer.getServerContext()); + publishCodeMapSubscriber.onEvent(null, openContext, languageServer.getServerContext()); + + // Trigger didChange event for second file (simulating file modification) + String fileUri2 = filePath2.toAbsolutePath().normalize().toUri().toString(); + DocumentServiceContext changeContext = ContextBuilder.buildDocumentServiceContext( + fileUri2, workspaceManager, LSContextOperation.TXT_DID_CHANGE, languageServer.getServerContext()); + publishCodeMapSubscriber.onEvent(null, changeContext, languageServer.getServerContext()); + + // Verify both files are tracked + List trackedFiles = CodeMapFilesTracker.getInstance().getModifiedFiles(projectKey); + Assert.assertEquals(trackedFiles.size(), 2, "Both didOpen and didChange should track files"); + Assert.assertTrue(trackedFiles.contains("main.bal"), "main.bal (opened) should be tracked"); + Assert.assertTrue(trackedFiles.contains("service.bal"), "service.bal (changed) should be tracked"); + + // Cleanup + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + } + + @Test + public void testMultipleFileAccumulation() throws IOException { + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + + // Get paths for two different files in the same project + String sourcePath1 = getSourcePath("project/main.bal"); + String sourcePath2 = getSourcePath("project/service.bal"); + Path filePath1 = Path.of(sourcePath1); + Path filePath2 = Path.of(sourcePath2); + + // Clear tracker first + Path projectPath = workspaceManager.projectRoot(filePath1); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // Trigger onEvent for first file + String fileUri1 = filePath1.toAbsolutePath().normalize().toUri().toString(); + DocumentServiceContext context1 = ContextBuilder.buildDocumentServiceContext( + fileUri1, workspaceManager, LSContextOperation.TXT_DID_CHANGE, languageServer.getServerContext()); + publishCodeMapSubscriber.onEvent(null, context1, languageServer.getServerContext()); + + // Trigger onEvent for second file + String fileUri2 = filePath2.toAbsolutePath().normalize().toUri().toString(); + DocumentServiceContext context2 = ContextBuilder.buildDocumentServiceContext( + fileUri2, workspaceManager, LSContextOperation.TXT_DID_CHANGE, languageServer.getServerContext()); + publishCodeMapSubscriber.onEvent(null, context2, languageServer.getServerContext()); + + // Verify both files are tracked + List trackedFiles = CodeMapFilesTracker.getInstance().getModifiedFiles(projectKey); + Assert.assertEquals(trackedFiles.size(), 2, "Both files should be tracked"); + Assert.assertTrue(trackedFiles.contains("main.bal"), "main.bal should be tracked"); + Assert.assertTrue(trackedFiles.contains("service.bal"), "service.bal should be tracked"); + + // Cleanup + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + } + + @Test + public void testConsecutiveEventsForSameFile() throws IOException { + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + String sourcePath = getSourcePath("project/main.bal"); + Path filePath = Path.of(sourcePath); + String fileUri = filePath.toAbsolutePath().normalize().toUri().toString(); + + // Clear tracker first + Path projectPath = workspaceManager.projectRoot(filePath); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // Trigger onEvent multiple times for the same file + DocumentServiceContext context = ContextBuilder.buildDocumentServiceContext( + fileUri, workspaceManager, LSContextOperation.TXT_DID_CHANGE, languageServer.getServerContext()); + + publishCodeMapSubscriber.onEvent(null, context, languageServer.getServerContext()); + publishCodeMapSubscriber.onEvent(null, context, languageServer.getServerContext()); + publishCodeMapSubscriber.onEvent(null, context, languageServer.getServerContext()); + + // Verify the file is tracked only once (no duplicates) + List trackedFiles = CodeMapFilesTracker.getInstance().getModifiedFiles(projectKey); + Assert.assertEquals(trackedFiles.size(), 1, "File should be tracked only once despite multiple events"); + Assert.assertTrue(trackedFiles.contains("main.bal"), "main.bal should be tracked"); + + // Cleanup + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + } + + @Test + public void testSameFileNameInDifferentModulesTrackedSeparately() { + // Negative test: changing root types.bal should NOT track modules/mod1/types.bal + String projectKey = "file:///test/project/"; + CodeMapFilesTracker tracker = CodeMapFilesTracker.getInstance(); + + // Clear tracker first + tracker.clearModifiedFiles(projectKey); + + // Track only the root module file + tracker.trackFile(projectKey, "types.bal"); + + // Verify only root file is tracked, submodule file should NOT be present + List trackedFiles = tracker.getModifiedFiles(projectKey); + Assert.assertEquals(trackedFiles.size(), 1, + "Only one file should be tracked"); + Assert.assertTrue(trackedFiles.contains("types.bal"), + "Root types.bal should be tracked"); + Assert.assertFalse(trackedFiles.contains("modules/mod1/types.bal"), + "Submodule types.bal should NOT be tracked when only root file changed"); + + // Cleanup + tracker.clearModifiedFiles(projectKey); + } + + @Test + public void testStateClearingAfterRetrieval() throws IOException { + WorkspaceManager workspaceManager = languageServer.getWorkspaceManager(); + String sourcePath = getSourcePath("project/main.bal"); + Path filePath = Path.of(sourcePath); + String fileUri = filePath.toAbsolutePath().normalize().toUri().toString(); + + // Clear tracker first + Path projectPath = workspaceManager.projectRoot(filePath); + String projectKey = projectPath.toUri().toString(); + CodeMapFilesTracker.getInstance().clearModifiedFiles(projectKey); + + // Track a file + DocumentServiceContext context = ContextBuilder.buildDocumentServiceContext( + fileUri, workspaceManager, LSContextOperation.TXT_DID_CHANGE, languageServer.getServerContext()); + publishCodeMapSubscriber.onEvent(null, context, languageServer.getServerContext()); + + // Call codeMap API with changesOnly=true (this should consume the tracked changes) + CodeMapRequest request = new CodeMapRequest(projectPath.toString(), true); + JsonObject codeMapResponse = getResponse(request, "designModelService/codemap"); + JsonObject files = codeMapResponse.getAsJsonObject("files"); + Assert.assertFalse(files.entrySet().isEmpty(), "First call should return tracked changes"); + + // Call codeMap API again - should return empty since changes were consumed + JsonObject secondResponse = getResponse(request, "designModelService/codemap"); + JsonObject secondFiles = secondResponse.getAsJsonObject("files"); + Assert.assertTrue(secondFiles.entrySet().isEmpty(), + "Second call should return empty as changes were consumed by first call"); + } + + @Test + public void testProjectIsolation() { + // Test that changes in one project do not leak into another project + String projectKeyA = "file:///test/projectA/"; + String projectKeyB = "file:///test/projectB/"; + CodeMapFilesTracker tracker = CodeMapFilesTracker.getInstance(); + + // Clear both trackers + tracker.clearModifiedFiles(projectKeyA); + tracker.clearModifiedFiles(projectKeyB); + + // Track file in Project A only + tracker.trackFile(projectKeyA, "main.bal"); + tracker.trackFile(projectKeyA, "service.bal"); + + // Verify Project A has tracked files + List trackedFilesA = tracker.getModifiedFiles(projectKeyA); + Assert.assertEquals(trackedFilesA.size(), 2, "Project A should have 2 tracked files"); + + // Verify Project B has no tracked files (isolation) + List trackedFilesB = tracker.getModifiedFiles(projectKeyB); + Assert.assertTrue(trackedFilesB.isEmpty(), + "Project B should have no tracked files - changes should not leak between projects"); + + // Now track a file in Project B + tracker.trackFile(projectKeyB, "utils.bal"); + + // Verify Project A still has its original files (unchanged) + trackedFilesA = tracker.getModifiedFiles(projectKeyA); + Assert.assertEquals(trackedFilesA.size(), 2, "Project A should still have 2 tracked files"); + Assert.assertFalse(trackedFilesA.contains("utils.bal"), + "Project A should not contain Project B's file"); + + // Verify Project B has only its file + trackedFilesB = tracker.getModifiedFiles(projectKeyB); + Assert.assertEquals(trackedFilesB.size(), 1, "Project B should have 1 tracked file"); + Assert.assertTrue(trackedFilesB.contains("utils.bal"), "Project B should contain utils.bal"); + + // Cleanup + tracker.clearModifiedFiles(projectKeyA); + tracker.clearModifiedFiles(projectKeyB); + } + + @Override + protected String getResourceDir() { + return "codemap_changes"; + } + + @Override + protected Class clazz() { + return PublishCodeMapSubscriberTest.class; + } + + @Override + protected String getServiceName() { + return "designModelService"; + } + + @Override + protected String getApiName() { + return "codemap"; + } + + public record TestConfig(String description, String source, JsonObject output) { + + public String description() { + return description == null ? "" : description; + } + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/error.json b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/error.json new file mode 100644 index 0000000000..e98ed02715 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/error.json @@ -0,0 +1,26 @@ +{ + "description": "Ballerina error test", + "source": "error.bal", + "output": { + "error.bal": { + "artifacts": [ + { + "name": "MISSING[]", + "type": "VARIABLE", + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 2 + } + }, + "properties": {}, + "children": [] + } + ] + } + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/main.json b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/main.json new file mode 100644 index 0000000000..b6fc23d4c3 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/main.json @@ -0,0 +1,297 @@ +{ + "description": "Ballerina project test", + "source": "main.bal", + "output": { + "main.bal": { + "artifacts": [ + { + "name": "ballerina/http", + "type": "IMPORT", + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 22 + } + }, + "properties": { + "moduleName": "http", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "httpListener", + "type": "LISTENER", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 55 + } + }, + "properties": { + "type": "http:Listener", + "comment": "HTTP listener on port 8080" + }, + "children": [] + }, + { + "name": "User", + "type": "TYPE", + "range": { + "start": { + "line": 6, + "character": 0 + }, + "end": { + "line": 10, + "character": 3 + } + }, + "properties": { + "fields": [ + "id: int", + "name: string", + "email: string" + ], + "comment": "Sample record types", + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "CreateUserRequest", + "type": "TYPE", + "range": { + "start": { + "line": 12, + "character": 0 + }, + "end": { + "line": 15, + "character": 3 + } + }, + "properties": { + "fields": [ + "name: string", + "email: string" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "ErrorResponse", + "type": "TYPE", + "range": { + "start": { + "line": 17, + "character": 0 + }, + "end": { + "line": 19, + "character": 3 + } + }, + "properties": { + "fields": [ + "message: string" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "users", + "type": "VARIABLE", + "range": { + "start": { + "line": 22, + "character": 0 + }, + "end": { + "line": 22, + "character": 21 + } + }, + "properties": { + "type": "map", + "comment": "In-memory storage for demo" + }, + "children": [] + }, + { + "name": "nextId", + "type": "VARIABLE", + "range": { + "start": { + "line": 23, + "character": 0 + }, + "end": { + "line": 23, + "character": 15 + } + }, + "properties": { + "type": "int" + }, + "children": [] + }, + { + "name": "/api", + "type": "SERVICE", + "range": { + "start": { + "line": 26, + "character": 0 + }, + "end": { + "line": 70, + "character": 1 + } + }, + "properties": { + "basePath": "/api", + "listenerType": "http:Listener", + "comment": "REST API service" + }, + "children": [ + { + "name": "users", + "type": "FUNCTION", + "range": { + "start": { + "line": 29, + "character": 4 + }, + "end": { + "line": 31, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "User[]|error", + "comment": "GET /api/users - Get all users", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + }, + { + "name": "users/[int id]", + "type": "FUNCTION", + "range": { + "start": { + "line": 34, + "character": 4 + }, + "end": { + "line": 44, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "User|http:NotFound|error", + "comment": "GET /api/users/{id} - Get user by ID", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + }, + { + "name": "users", + "type": "FUNCTION", + "range": { + "start": { + "line": 47, + "character": 4 + }, + "end": { + "line": 56, + "character": 5 + } + }, + "properties": { + "accessor": "post", + "returns": "User|error", + "comment": "POST /api/users - Create a new user", + "modifiers": [ + "resource" + ], + "parameters": [ + "payload: CreateUserRequest" + ] + }, + "children": [] + }, + { + "name": "search", + "type": "FUNCTION", + "range": { + "start": { + "line": 59, + "character": 4 + }, + "end": { + "line": 64, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "User[]|error", + "comment": "GET /api/search - Search users by name (query parameter example)", + "modifiers": [ + "resource" + ], + "parameters": [ + "name: string" + ] + }, + "children": [] + }, + { + "name": "health", + "type": "FUNCTION", + "range": { + "start": { + "line": 67, + "character": 4 + }, + "end": { + "line": 69, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "string", + "comment": "GET /api/health - Health check endpoint", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + } + ] + } + ] + } + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/order_management_system.json b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/order_management_system.json new file mode 100644 index 0000000000..3c2260bfec --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/config/order_management_system.json @@ -0,0 +1,2375 @@ +{ + "description": "Ballerina Order Management System test", + "source": "order_management_system", + "output": { + "modules/product/constants.bal": { + "artifacts": [ + { + "name": "PRODUCT_ID_PREFIX", + "type": "VARIABLE", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 47 + } + }, + "properties": { + "comment": "Product module constants", + "modifiers": [ + "public" + ], + "value": "\"PROD\"", + "typeDescriptor": "string" + }, + "children": [] + }, + { + "name": "MIN_PRICE", + "type": "VARIABLE", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 38 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "value": "0.01", + "typeDescriptor": "decimal" + }, + "children": [] + }, + { + "name": "MIN_STOCK", + "type": "VARIABLE", + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 31 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "value": "0", + "typeDescriptor": "int" + }, + "children": [] + } + ] + }, + "modules/product/service.bal": { + "artifacts": [ + { + "name": "ballerina/http", + "type": "IMPORT", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 22 + } + }, + "properties": { + "moduleName": "http", + "comment": "Product service implementation", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "ballerina/time", + "type": "IMPORT", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 22 + } + }, + "properties": { + "moduleName": "time", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "ballerina/uuid", + "type": "IMPORT", + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "properties": { + "moduleName": "uuid", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "productStore", + "type": "VARIABLE", + "range": { + "start": { + "line": 6, + "character": 0 + }, + "end": { + "line": 6, + "character": 37 + } + }, + "properties": { + "modifiers": [ + "final" + ], + "type": "map" + }, + "children": [] + }, + { + "name": "ProductService", + "type": "CLASS", + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 128, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public", + "service" + ] + }, + "children": [ + { + "name": "getProduct", + "type": "FUNCTION", + "range": { + "start": { + "line": 11, + "character": 4 + }, + "end": { + "line": 17, + "character": 5 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "productId: string" + ], + "returns": "Product|http:NotFound" + }, + "children": [] + }, + { + "name": "products", + "type": "FUNCTION", + "range": { + "start": { + "line": 19, + "character": 4 + }, + "end": { + "line": 49, + "character": 5 + } + }, + "properties": { + "accessor": "post", + "returns": "Product|http:BadRequest", + "modifiers": [ + "resource" + ], + "parameters": [ + "request: ProductCreateRequest" + ] + }, + "children": [] + }, + { + "name": "products/[string productId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 51, + "character": 4 + }, + "end": { + "line": 57, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "Product|http:NotFound", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + }, + { + "name": "products", + "type": "FUNCTION", + "range": { + "start": { + "line": 59, + "character": 4 + }, + "end": { + "line": 70, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "Product[]", + "modifiers": [ + "resource" + ], + "parameters": [ + "category: string? = ()" + ] + }, + "children": [] + }, + { + "name": "products/[string productId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 72, + "character": 4 + }, + "end": { + "line": 119, + "character": 5 + } + }, + "properties": { + "accessor": "put", + "returns": "Product|http:NotFound|http:BadRequest", + "modifiers": [ + "resource" + ], + "parameters": [ + "request: ProductUpdateRequest" + ] + }, + "children": [] + }, + { + "name": "products/[string productId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 121, + "character": 4 + }, + "end": { + "line": 127, + "character": 5 + } + }, + "properties": { + "accessor": "delete", + "returns": "http:NoContent|http:NotFound", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + } + ] + } + ] + }, + "modules/product/types.bal": { + "artifacts": [ + { + "name": "Product", + "type": "TYPE", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 12, + "character": 3 + } + }, + "properties": { + "fields": [ + "productId: string", + "name: string", + "description: string", + "price: decimal", + "stockQuantity: int", + "category: string", + "status: product:ProductStatus", + "createdAt: string", + "updatedAt: string" + ], + "comment": "Product module type definitions", + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "ProductCreateRequest", + "type": "TYPE", + "range": { + "start": { + "line": 14, + "character": 0 + }, + "end": { + "line": 20, + "character": 3 + } + }, + "properties": { + "fields": [ + "name: string", + "description: string", + "price: decimal", + "stockQuantity: int", + "category: string" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "ProductUpdateRequest", + "type": "TYPE", + "range": { + "start": { + "line": 22, + "character": 0 + }, + "end": { + "line": 29, + "character": 3 + } + }, + "properties": { + "fields": [ + "name: string?", + "description: string?", + "price: decimal?", + "stockQuantity: int?", + "category: string?", + "status: product:ProductStatus?" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "ProductStatus", + "type": "TYPE", + "range": { + "start": { + "line": 31, + "character": 0 + }, + "end": { + "line": 35, + "character": 1 + } + }, + "properties": { + "fields": [ + "ACTIVE", + "INACTIVE", + "OUT_OF_STOCK" + ], + "typeDescriptor": "enum" + }, + "children": [] + } + ] + }, + "modules/product/utils.bal": { + "artifacts": [ + { + "name": "validatePrice", + "type": "FUNCTION", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "properties": { + "returns": "boolean", + "comment": "Product module utility functions", + "modifiers": [ + "public" + ], + "parameters": [ + "price: decimal" + ] + }, + "children": [] + }, + { + "name": "validateStock", + "type": "FUNCTION", + "range": { + "start": { + "line": 6, + "character": 0 + }, + "end": { + "line": 8, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "quantity: int" + ], + "returns": "boolean" + }, + "children": [] + }, + { + "name": "isProductAvailable", + "type": "FUNCTION", + "range": { + "start": { + "line": 10, + "character": 0 + }, + "end": { + "line": 12, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "stockQuantity: int" + ], + "returns": "boolean" + }, + "children": [] + } + ] + }, + "modules/order/constants.bal": { + "artifacts": [ + { + "name": "ORDER_ID_PREFIX", + "type": "VARIABLE", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 44 + } + }, + "properties": { + "comment": "Order module constants", + "modifiers": [ + "public" + ], + "value": "\"ORD\"", + "typeDescriptor": "string" + }, + "children": [] + }, + { + "name": "MIN_ORDER_QUANTITY", + "type": "VARIABLE", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 40 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "value": "1", + "typeDescriptor": "int" + }, + "children": [] + }, + { + "name": "ZERO_AMOUNT", + "type": "VARIABLE", + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 39 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "value": "0.0", + "typeDescriptor": "decimal" + }, + "children": [] + } + ] + }, + "modules/order/service.bal": { + "artifacts": [ + { + "name": "order_management_system.customer", + "type": "IMPORT", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 40 + } + }, + "properties": { + "moduleName": "order_management_system.customer", + "comment": "Order service implementation" + }, + "children": [] + }, + { + "name": "order_management_system.product", + "type": "IMPORT", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 39 + } + }, + "properties": { + "moduleName": "order_management_system.product" + }, + "children": [] + }, + { + "name": "ballerina/http", + "type": "IMPORT", + "range": { + "start": { + "line": 5, + "character": 0 + }, + "end": { + "line": 5, + "character": 22 + } + }, + "properties": { + "moduleName": "http", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "ballerina/time", + "type": "IMPORT", + "range": { + "start": { + "line": 6, + "character": 0 + }, + "end": { + "line": 6, + "character": 22 + } + }, + "properties": { + "moduleName": "time", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "ballerina/uuid", + "type": "IMPORT", + "range": { + "start": { + "line": 7, + "character": 0 + }, + "end": { + "line": 7, + "character": 22 + } + }, + "properties": { + "moduleName": "uuid", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "orderStore", + "type": "VARIABLE", + "range": { + "start": { + "line": 9, + "character": 0 + }, + "end": { + "line": 9, + "character": 33 + } + }, + "properties": { + "modifiers": [ + "final" + ], + "type": "map" + }, + "children": [] + }, + { + "name": "OrderService", + "type": "CLASS", + "range": { + "start": { + "line": 11, + "character": 0 + }, + "end": { + "line": 143, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public", + "service" + ] + }, + "children": [ + { + "name": "customerService", + "type": "FIELD", + "range": { + "start": { + "line": 14, + "character": 4 + }, + "end": { + "line": 14, + "character": 59 + } + }, + "properties": { + "modifiers": [ + "private", + "final" + ], + "type": "customer:CustomerService" + }, + "children": [] + }, + { + "name": "productService", + "type": "FIELD", + "range": { + "start": { + "line": 15, + "character": 4 + }, + "end": { + "line": 15, + "character": 56 + } + }, + "properties": { + "modifiers": [ + "private", + "final" + ], + "type": "product:ProductService" + }, + "children": [] + }, + { + "name": "init", + "type": "FUNCTION", + "range": { + "start": { + "line": 17, + "character": 4 + }, + "end": { + "line": 20, + "character": 5 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "customerService: customer:CustomerService", + "productService: product:ProductService" + ], + "returns": "()" + }, + "children": [] + }, + { + "name": "getOrder", + "type": "FUNCTION", + "range": { + "start": { + "line": 22, + "character": 4 + }, + "end": { + "line": 28, + "character": 5 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "orderId: string" + ], + "returns": "Order|http:NotFound" + }, + "children": [] + }, + { + "name": "orders", + "type": "FUNCTION", + "range": { + "start": { + "line": 30, + "character": 4 + }, + "end": { + "line": 84, + "character": 5 + } + }, + "properties": { + "accessor": "post", + "returns": "Order|http:BadRequest|http:NotFound", + "modifiers": [ + "resource" + ], + "parameters": [ + "request: OrderCreateRequest" + ] + }, + "children": [] + }, + { + "name": "orders/[string orderId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 86, + "character": 4 + }, + "end": { + "line": 92, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "Order|http:NotFound", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + }, + { + "name": "orders", + "type": "FUNCTION", + "range": { + "start": { + "line": 94, + "character": 4 + }, + "end": { + "line": 112, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "Order[]", + "modifiers": [ + "resource" + ], + "parameters": [ + "customerId: string? = ()", + "status: OrderStatus? = ()" + ] + }, + "children": [] + }, + { + "name": "orders/[string orderId]/status", + "type": "FUNCTION", + "range": { + "start": { + "line": 114, + "character": 4 + }, + "end": { + "line": 130, + "character": 5 + } + }, + "properties": { + "accessor": "put", + "returns": "Order|http:NotFound|http:BadRequest", + "modifiers": [ + "resource" + ], + "parameters": [ + "request: OrderUpdateStatusRequest" + ] + }, + "children": [] + }, + { + "name": "orders/[string orderId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 132, + "character": 4 + }, + "end": { + "line": 142, + "character": 5 + } + }, + "properties": { + "accessor": "delete", + "returns": "http:NoContent|http:NotFound|http:BadRequest", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + } + ] + } + ] + }, + "modules/order/types.bal": { + "artifacts": [ + { + "name": "Order", + "type": "TYPE", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 11, + "character": 3 + } + }, + "properties": { + "fields": [ + "orderId: string", + "customerId: string", + "items: order:OrderItem[]", + "totalAmount: decimal", + "status: order:OrderStatus", + "shippingAddress: string", + "createdAt: string", + "updatedAt: string" + ], + "comment": "Order module type definitions", + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "OrderItem", + "type": "TYPE", + "range": { + "start": { + "line": 13, + "character": 0 + }, + "end": { + "line": 19, + "character": 3 + } + }, + "properties": { + "fields": [ + "productId: string", + "productName: string", + "quantity: int", + "unitPrice: decimal", + "subtotal: decimal" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "OrderCreateRequest", + "type": "TYPE", + "range": { + "start": { + "line": 21, + "character": 0 + }, + "end": { + "line": 25, + "character": 3 + } + }, + "properties": { + "fields": [ + "customerId: string", + "items: order:OrderItemRequest[]", + "shippingAddress: string" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "OrderItemRequest", + "type": "TYPE", + "range": { + "start": { + "line": 27, + "character": 0 + }, + "end": { + "line": 30, + "character": 3 + } + }, + "properties": { + "fields": [ + "productId: string", + "quantity: int" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "OrderUpdateStatusRequest", + "type": "TYPE", + "range": { + "start": { + "line": 32, + "character": 0 + }, + "end": { + "line": 34, + "character": 3 + } + }, + "properties": { + "fields": [ + "status: order:OrderStatus" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "OrderStatus", + "type": "TYPE", + "range": { + "start": { + "line": 36, + "character": 0 + }, + "end": { + "line": 43, + "character": 1 + } + }, + "properties": { + "fields": [ + "PENDING", + "CONFIRMED", + "PROCESSING", + "SHIPPED", + "DELIVERED", + "CANCELLED" + ], + "typeDescriptor": "enum" + }, + "children": [] + } + ] + }, + "modules/order/utils.bal": { + "artifacts": [ + { + "name": "calculateSubtotal", + "type": "FUNCTION", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 5, + "character": 1 + } + }, + "properties": { + "returns": "decimal", + "comment": "Order module utility functions", + "modifiers": [ + "public" + ], + "parameters": [ + "quantity: int", + "unitPrice: decimal" + ] + }, + "children": [] + }, + { + "name": "calculateTotalAmount", + "type": "FUNCTION", + "range": { + "start": { + "line": 7, + "character": 0 + }, + "end": { + "line": 13, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "items: OrderItem[]" + ], + "returns": "decimal" + }, + "children": [] + }, + { + "name": "validateOrderQuantity", + "type": "FUNCTION", + "range": { + "start": { + "line": 15, + "character": 0 + }, + "end": { + "line": 17, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "quantity: int" + ], + "returns": "boolean" + }, + "children": [] + }, + { + "name": "canCancelOrder", + "type": "FUNCTION", + "range": { + "start": { + "line": 19, + "character": 0 + }, + "end": { + "line": 21, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "status: OrderStatus" + ], + "returns": "boolean" + }, + "children": [] + } + ] + }, + "modules/payment/constants.bal": { + "artifacts": [ + { + "name": "PAYMENT_ID_PREFIX", + "type": "VARIABLE", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 46 + } + }, + "properties": { + "comment": "Payment module constants", + "modifiers": [ + "public" + ], + "value": "\"PAY\"", + "typeDescriptor": "string" + }, + "children": [] + }, + { + "name": "TRANSACTION_ID_PREFIX", + "type": "VARIABLE", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 50 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "value": "\"TXN\"", + "typeDescriptor": "string" + }, + "children": [] + } + ] + }, + "modules/payment/service.bal": { + "artifacts": [ + { + "name": "order_management_system.'order", + "type": "IMPORT", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 38 + } + }, + "properties": { + "moduleName": "order_management_system.'order", + "comment": "Payment service implementation" + }, + "children": [] + }, + { + "name": "ballerina/http", + "type": "IMPORT", + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "properties": { + "moduleName": "http", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "ballerina/time", + "type": "IMPORT", + "range": { + "start": { + "line": 5, + "character": 0 + }, + "end": { + "line": 5, + "character": 22 + } + }, + "properties": { + "moduleName": "time", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "ballerina/uuid", + "type": "IMPORT", + "range": { + "start": { + "line": 6, + "character": 0 + }, + "end": { + "line": 6, + "character": 22 + } + }, + "properties": { + "moduleName": "uuid", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "paymentStore", + "type": "VARIABLE", + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 8, + "character": 37 + } + }, + "properties": { + "modifiers": [ + "final" + ], + "type": "map" + }, + "children": [] + }, + { + "name": "PaymentService", + "type": "CLASS", + "range": { + "start": { + "line": 10, + "character": 0 + }, + "end": { + "line": 115, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public", + "service" + ] + }, + "children": [ + { + "name": "orderService", + "type": "FIELD", + "range": { + "start": { + "line": 13, + "character": 4 + }, + "end": { + "line": 13, + "character": 51 + } + }, + "properties": { + "modifiers": [ + "private", + "final" + ], + "type": "'order:OrderService" + }, + "children": [] + }, + { + "name": "init", + "type": "FUNCTION", + "range": { + "start": { + "line": 15, + "character": 4 + }, + "end": { + "line": 17, + "character": 5 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "orderService: 'order:OrderService" + ], + "returns": "()" + }, + "children": [] + }, + { + "name": "payments", + "type": "FUNCTION", + "range": { + "start": { + "line": 19, + "character": 4 + }, + "end": { + "line": 58, + "character": 5 + } + }, + "properties": { + "accessor": "post", + "returns": "Payment|http:BadRequest|http:NotFound", + "modifiers": [ + "resource" + ], + "parameters": [ + "request: PaymentCreateRequest" + ] + }, + "children": [] + }, + { + "name": "payments/[string paymentId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 60, + "character": 4 + }, + "end": { + "line": 68, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "Payment|http:NotFound", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + }, + { + "name": "payments", + "type": "FUNCTION", + "range": { + "start": { + "line": 70, + "character": 4 + }, + "end": { + "line": 90, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "Payment[]", + "modifiers": [ + "resource" + ], + "parameters": [ + "orderId: string? = ()", + "customerId: string? = ()" + ] + }, + "children": [] + }, + { + "name": "payments/[string paymentId]/refund", + "type": "FUNCTION", + "range": { + "start": { + "line": 92, + "character": 4 + }, + "end": { + "line": 114, + "character": 5 + } + }, + "properties": { + "accessor": "post", + "returns": "Payment|http:NotFound|http:BadRequest", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + } + ] + } + ] + }, + "modules/payment/types.bal": { + "artifacts": [ + { + "name": "Payment", + "type": "TYPE", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 12, + "character": 3 + } + }, + "properties": { + "fields": [ + "paymentId: string", + "orderId: string", + "customerId: string", + "amount: decimal", + "paymentMethod: payment:PaymentMethod", + "status: payment:PaymentStatus", + "transactionId: string?", + "createdAt: string", + "updatedAt: string" + ], + "comment": "Payment module type definitions", + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "PaymentCreateRequest", + "type": "TYPE", + "range": { + "start": { + "line": 14, + "character": 0 + }, + "end": { + "line": 18, + "character": 3 + } + }, + "properties": { + "fields": [ + "orderId: string", + "paymentMethod: payment:PaymentMethod", + "paymentDetails: payment:PaymentDetails" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "PaymentDetails", + "type": "TYPE", + "range": { + "start": { + "line": 20, + "character": 0 + }, + "end": { + "line": 25, + "character": 3 + } + }, + "properties": { + "fields": [ + "cardNumber: string?", + "cardHolderName: string?", + "expiryDate: string?", + "cvv: string?" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "PaymentMethod", + "type": "TYPE", + "range": { + "start": { + "line": 27, + "character": 0 + }, + "end": { + "line": 33, + "character": 1 + } + }, + "properties": { + "fields": [ + "CREDIT_CARD", + "DEBIT_CARD", + "PAYPAL", + "BANK_TRANSFER", + "CASH_ON_DELIVERY" + ], + "typeDescriptor": "enum" + }, + "children": [] + }, + { + "name": "PaymentStatus", + "type": "TYPE", + "range": { + "start": { + "line": 35, + "character": 0 + }, + "end": { + "line": 41, + "character": 1 + } + }, + "properties": { + "fields": [ + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED", + "REFUNDED" + ], + "typeDescriptor": "enum" + }, + "children": [] + } + ] + }, + "modules/payment/utils.bal": { + "artifacts": [ + { + "name": "ballerina/uuid", + "type": "IMPORT", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 22 + } + }, + "properties": { + "moduleName": "uuid", + "comment": "Payment module utility functions", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "generateTransactionId", + "type": "FUNCTION", + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 6, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [], + "returns": "string" + }, + "children": [] + }, + { + "name": "processPayment", + "type": "FUNCTION", + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 24, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "paymentMethod: PaymentMethod", + "paymentDetails: PaymentDetails" + ], + "returns": "boolean" + }, + "children": [] + }, + { + "name": "canRefundPayment", + "type": "FUNCTION", + "range": { + "start": { + "line": 26, + "character": 0 + }, + "end": { + "line": 28, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "status: PaymentStatus" + ], + "returns": "boolean" + }, + "children": [] + } + ] + }, + "modules/customer/constants.bal": { + "artifacts": [ + { + "name": "CUSTOMER_ID_PREFIX", + "type": "VARIABLE", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 48 + } + }, + "properties": { + "comment": "Customer module constants", + "modifiers": [ + "public" + ], + "value": "\"CUST\"", + "typeDescriptor": "string" + }, + "children": [] + }, + { + "name": "MIN_NAME_LENGTH", + "type": "VARIABLE", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 37 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "value": "2", + "typeDescriptor": "int" + }, + "children": [] + }, + { + "name": "MAX_NAME_LENGTH", + "type": "VARIABLE", + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 38 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "value": "50", + "typeDescriptor": "int" + }, + "children": [] + } + ] + }, + "modules/customer/service.bal": { + "artifacts": [ + { + "name": "ballerina/http", + "type": "IMPORT", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 22 + } + }, + "properties": { + "moduleName": "http", + "comment": "Customer service implementation", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "ballerina/time", + "type": "IMPORT", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 22 + } + }, + "properties": { + "moduleName": "time", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "ballerina/uuid", + "type": "IMPORT", + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "properties": { + "moduleName": "uuid", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "customerStore", + "type": "VARIABLE", + "range": { + "start": { + "line": 6, + "character": 0 + }, + "end": { + "line": 6, + "character": 39 + } + }, + "properties": { + "modifiers": [ + "final" + ], + "type": "map" + }, + "children": [] + }, + { + "name": "CustomerService", + "type": "CLASS", + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 120, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public", + "service" + ] + }, + "children": [ + { + "name": "getCustomer", + "type": "FUNCTION", + "range": { + "start": { + "line": 11, + "character": 4 + }, + "end": { + "line": 17, + "character": 5 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "customerId: string" + ], + "returns": "Customer|http:NotFound" + }, + "children": [] + }, + { + "name": "customers", + "type": "FUNCTION", + "range": { + "start": { + "line": 19, + "character": 4 + }, + "end": { + "line": 49, + "character": 5 + } + }, + "properties": { + "accessor": "post", + "returns": "Customer|http:BadRequest|http:InternalServerError", + "modifiers": [ + "resource" + ], + "parameters": [ + "request: CustomerCreateRequest" + ] + }, + "children": [] + }, + { + "name": "customers/[string customerId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 51, + "character": 4 + }, + "end": { + "line": 57, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "Customer|http:NotFound", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + }, + { + "name": "customers", + "type": "FUNCTION", + "range": { + "start": { + "line": 59, + "character": 4 + }, + "end": { + "line": 61, + "character": 5 + } + }, + "properties": { + "accessor": "get", + "returns": "Customer[]", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + }, + { + "name": "customers/[string customerId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 63, + "character": 4 + }, + "end": { + "line": 111, + "character": 5 + } + }, + "properties": { + "accessor": "put", + "returns": "Customer|http:NotFound|http:BadRequest", + "modifiers": [ + "resource" + ], + "parameters": [ + "request: CustomerUpdateRequest" + ] + }, + "children": [] + }, + { + "name": "customers/[string customerId]", + "type": "FUNCTION", + "range": { + "start": { + "line": 113, + "character": 4 + }, + "end": { + "line": 119, + "character": 5 + } + }, + "properties": { + "accessor": "delete", + "returns": "http:NoContent|http:NotFound", + "modifiers": [ + "resource" + ], + "parameters": [] + }, + "children": [] + } + ] + } + ] + }, + "modules/customer/types.bal": { + "artifacts": [ + { + "name": "Customer", + "type": "TYPE", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 11, + "character": 3 + } + }, + "properties": { + "fields": [ + "customerId: string", + "firstName: string", + "lastName: string", + "email: string", + "phone: string", + "address: customer:Address", + "createdAt: string", + "updatedAt: string" + ], + "comment": "Customer module type definitions", + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "Address", + "type": "TYPE", + "range": { + "start": { + "line": 13, + "character": 0 + }, + "end": { + "line": 19, + "character": 3 + } + }, + "properties": { + "fields": [ + "street: string", + "city: string", + "state: string", + "zipCode: string", + "country: string" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "CustomerCreateRequest", + "type": "TYPE", + "range": { + "start": { + "line": 21, + "character": 0 + }, + "end": { + "line": 27, + "character": 3 + } + }, + "properties": { + "fields": [ + "firstName: string", + "lastName: string", + "email: string", + "phone: string", + "address: customer:Address" + ], + "typeDescriptor": "record" + }, + "children": [] + }, + { + "name": "CustomerUpdateRequest", + "type": "TYPE", + "range": { + "start": { + "line": 29, + "character": 0 + }, + "end": { + "line": 35, + "character": 3 + } + }, + "properties": { + "fields": [ + "firstName: string?", + "lastName: string?", + "email: string?", + "phone: string?", + "address: customer:Address?" + ], + "typeDescriptor": "record" + }, + "children": [] + } + ] + }, + "modules/customer/utils.bal": { + "artifacts": [ + { + "name": "validateEmail", + "type": "FUNCTION", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 5, + "character": 1 + } + }, + "properties": { + "returns": "boolean", + "comment": "Customer module utility functions", + "modifiers": [ + "public" + ], + "parameters": [ + "email: string" + ] + }, + "children": [] + }, + { + "name": "validatePhone", + "type": "FUNCTION", + "range": { + "start": { + "line": 7, + "character": 0 + }, + "end": { + "line": 10, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "phone: string" + ], + "returns": "boolean" + }, + "children": [] + }, + { + "name": "validateName", + "type": "FUNCTION", + "range": { + "start": { + "line": 12, + "character": 0 + }, + "end": { + "line": 15, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [ + "name: string" + ], + "returns": "boolean" + }, + "children": [] + } + ] + }, + "main.bal": { + "artifacts": [ + { + "name": "order_management_system.'order", + "type": "IMPORT", + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 38 + } + }, + "properties": { + "moduleName": "order_management_system.'order", + "comment": "Main entry point for Order Management System" + }, + "children": [] + }, + { + "name": "order_management_system.customer", + "type": "IMPORT", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 40 + } + }, + "properties": { + "moduleName": "order_management_system.customer" + }, + "children": [] + }, + { + "name": "order_management_system.payment", + "type": "IMPORT", + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 4, + "character": 39 + } + }, + "properties": { + "moduleName": "order_management_system.payment" + }, + "children": [] + }, + { + "name": "order_management_system.product", + "type": "IMPORT", + "range": { + "start": { + "line": 5, + "character": 0 + }, + "end": { + "line": 5, + "character": 39 + } + }, + "properties": { + "moduleName": "order_management_system.product" + }, + "children": [] + }, + { + "name": "ballerina/http", + "type": "IMPORT", + "range": { + "start": { + "line": 7, + "character": 0 + }, + "end": { + "line": 7, + "character": 22 + } + }, + "properties": { + "moduleName": "http", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "port", + "type": "VARIABLE", + "range": { + "start": { + "line": 9, + "character": 0 + }, + "end": { + "line": 9, + "character": 29 + } + }, + "properties": { + "modifiers": [ + "configurable" + ], + "type": "int" + }, + "children": [] + }, + { + "name": "main", + "type": "FUNCTION", + "range": { + "start": { + "line": 11, + "character": 0 + }, + "end": { + "line": 25, + "character": 1 + } + }, + "properties": { + "modifiers": [ + "public" + ], + "parameters": [], + "returns": "error?" + }, + "children": [] + } + ] + } + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/error.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/error.bal new file mode 100644 index 0000000000..23636079ef --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/error.bal @@ -0,0 +1 @@ +er diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/main.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/main.bal new file mode 100644 index 0000000000..475d804aca --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/main.bal @@ -0,0 +1,71 @@ +import ballerina/http; + +// HTTP listener on port 8080 +listener http:Listener httpListener = check new (8080); + +// Sample record types +type User record {| + int id; + string name; + string email; +|}; + +type CreateUserRequest record {| + string name; + string email; +|}; + +type ErrorResponse record {| + string message; +|}; + +// In-memory storage for demo +map users = {}; +int nextId = 1; + +// REST API service +service /api on httpListener { + + // GET /api/users - Get all users + resource function get users() returns User[]|error { + return users.toArray(); + } + + // GET /api/users/{id} - Get user by ID + resource function get users/[int id]() returns User|http:NotFound|error { + User? user = users[id.toString()]; + if user is () { + return { + body: { + message: string `User with id ${id} not found` + } + }; + } + return user; + } + + // POST /api/users - Create a new user + resource function post users(CreateUserRequest payload) returns User|error { + User newUser = { + id: nextId, + name: payload.name, + email: payload.email + }; + users[nextId.toString()] = newUser; + nextId = nextId + 1; + return newUser; + } + + // GET /api/search - Search users by name (query parameter example) + resource function get search(@http:Query string name) returns User[]|error { + User[] results = from User user in users + where user.name.toLowerAscii().includes(name.toLowerAscii()) + select user; + return results; + } + + // GET /api/health - Health check endpoint + resource function get health() returns string { + return "Service is running"; + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/Ballerina.toml b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/Ballerina.toml new file mode 100644 index 0000000000..a3b01bef4a --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "test" +name = "order_management_system" +version = "0.1.0" +distribution = "2201.13.1" + +[build-options] +observabilityIncluded = true diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/main.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/main.bal new file mode 100644 index 0000000000..87384b5c20 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/main.bal @@ -0,0 +1,26 @@ +// Main entry point for Order Management System + +import order_management_system.'order; +import order_management_system.customer; +import order_management_system.payment; +import order_management_system.product; + +import ballerina/http; + +configurable int port = 9090; + +public function main() returns error? { + http:Listener httpListener = check new (port); + + customer:CustomerService customerService = new (); + product:ProductService productService = new (); + 'order:OrderService orderService = new (customerService, productService); + payment:PaymentService paymentService = new (orderService); + + check httpListener.attach(httpService = customerService, name = "customer"); + check httpListener.attach(httpService = productService, name = "product"); + check httpListener.attach(httpService = orderService, name = "order"); + check httpListener.attach(httpService = paymentService, name = "payment"); + + check httpListener.'start(); +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/constants.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/constants.bal new file mode 100644 index 0000000000..e75ffcc9b3 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/constants.bal @@ -0,0 +1,5 @@ +// Customer module constants + +public const string CUSTOMER_ID_PREFIX = "CUST"; +public const int MIN_NAME_LENGTH = 2; +public const int MAX_NAME_LENGTH = 50; diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/service.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/service.bal new file mode 100644 index 0000000000..30f9715455 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/service.bal @@ -0,0 +1,121 @@ +// Customer service implementation + +import ballerina/http; +import ballerina/time; +import ballerina/uuid; + +final map customerStore = {}; + +public service class CustomerService { + *http:Service; + + public function getCustomer(string customerId) returns Customer|http:NotFound { + Customer? customer = customerStore[customerId]; + if customer is () { + return {body: "Customer not found"}; + } + return customer; + } + + resource function post customers(CustomerCreateRequest request) returns Customer|http:BadRequest|http:InternalServerError { + if !validateName(name = request.firstName) { + return {body: "Invalid first name"}; + } + if !validateName(name = request.lastName) { + return {body: "Invalid last name"}; + } + if !validateEmail(email = request.email) { + return {body: "Invalid email format"}; + } + if !validatePhone(phone = request.phone) { + return {body: "Invalid phone format"}; + } + + string customerId = string `${CUSTOMER_ID_PREFIX}-${uuid:createType1AsString()}`; + string currentTime = time:utcToString(time:utcNow()); + + Customer customer = { + customerId: customerId, + firstName: request.firstName, + lastName: request.lastName, + email: request.email, + phone: request.phone, + address: request.address, + createdAt: currentTime, + updatedAt: currentTime + }; + + customerStore[customerId] = customer; + return customer; + } + + resource function get customers/[string customerId]() returns Customer|http:NotFound { + Customer? customer = customerStore[customerId]; + if customer is () { + return {body: "Customer not found"}; + } + return customer; + } + + resource function get customers() returns Customer[] { + return customerStore.toArray(); + } + + resource function put customers/[string customerId](CustomerUpdateRequest request) returns Customer|http:NotFound|http:BadRequest { + Customer? existingCustomer = customerStore[customerId]; + if existingCustomer is () { + return {body: "Customer not found"}; + } + + Customer updatedCustomer = existingCustomer.clone(); + + string? firstName = request?.firstName; + if firstName is string { + if !validateName(name = firstName) { + return {body: "Invalid first name"}; + } + updatedCustomer.firstName = firstName; + } + + string? lastName = request?.lastName; + if lastName is string { + if !validateName(name = lastName) { + return {body: "Invalid last name"}; + } + updatedCustomer.lastName = lastName; + } + + string? email = request?.email; + if email is string { + if !validateEmail(email = email) { + return {body: "Invalid email format"}; + } + updatedCustomer.email = email; + } + + string? phone = request?.phone; + if phone is string { + if !validatePhone(phone = phone) { + return {body: "Invalid phone format"}; + } + updatedCustomer.phone = phone; + } + + Address? address = request?.address; + if address is Address { + updatedCustomer.address = address; + } + + updatedCustomer.updatedAt = time:utcToString(time:utcNow()); + customerStore[customerId] = updatedCustomer; + return updatedCustomer; + } + + resource function delete customers/[string customerId]() returns http:NoContent|http:NotFound { + Customer? removedCustomer = customerStore.removeIfHasKey(customerId); + if removedCustomer is () { + return {body: "Customer not found"}; + } + return http:NO_CONTENT; + } +} \ No newline at end of file diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/types.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/types.bal new file mode 100644 index 0000000000..e2f2c53dfa --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/types.bal @@ -0,0 +1,36 @@ +// Customer module type definitions + +public type Customer record {| + string customerId; + string firstName; + string lastName; + string email; + string phone; + Address address; + string createdAt; + string updatedAt; +|}; + +public type Address record {| + string street; + string city; + string state; + string zipCode; + string country; +|}; + +public type CustomerCreateRequest record {| + string firstName; + string lastName; + string email; + string phone; + Address address; +|}; + +public type CustomerUpdateRequest record {| + string? firstName?; + string? lastName?; + string? email?; + string? phone?; + Address? address?; +|}; diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/utils.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/utils.bal new file mode 100644 index 0000000000..095ef8aef3 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/customer/utils.bal @@ -0,0 +1,16 @@ +// Customer module utility functions + +public function validateEmail(string email) returns boolean { + string:RegExp emailPattern = re `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`; + return emailPattern.isFullMatch(email); +} + +public function validatePhone(string phone) returns boolean { + string:RegExp phonePattern = re `^\+?[1-9]\d{1,14}$`; + return phonePattern.isFullMatch(phone); +} + +public function validateName(string name) returns boolean { + int nameLength = name.length(); + return nameLength >= MIN_NAME_LENGTH && nameLength <= MAX_NAME_LENGTH; +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/constants.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/constants.bal new file mode 100644 index 0000000000..5fb07542aa --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/constants.bal @@ -0,0 +1,5 @@ +// Order module constants + +public const string ORDER_ID_PREFIX = "ORD"; +public const int MIN_ORDER_QUANTITY = 1; +public const decimal ZERO_AMOUNT = 0.0; diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/service.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/service.bal new file mode 100644 index 0000000000..6e09ca35cf --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/service.bal @@ -0,0 +1,144 @@ +// Order service implementation + +import order_management_system.customer; +import order_management_system.product; + +import ballerina/http; +import ballerina/time; +import ballerina/uuid; + +final map orderStore = {}; + +public service class OrderService { + *http:Service; + + private final customer:CustomerService customerService; + private final product:ProductService productService; + + public function init(customer:CustomerService customerService, product:ProductService productService) { + self.customerService = customerService; + self.productService = productService; + } + + public function getOrder(string orderId) returns Order|http:NotFound { + Order? 'order = orderStore[orderId]; + if 'order is () { + return {body: "Order not found"}; + } + return 'order; + } + + resource function post orders(OrderCreateRequest request) returns Order|http:BadRequest|http:NotFound { + customer:Customer|http:NotFound customerResult = self.customerService.getCustomer(customerId = request.customerId); + if customerResult is http:NotFound { + return {body: "Customer not found"}; + } + + OrderItem[] orderItems = []; + + foreach OrderItemRequest itemRequest in request.items { + if !validateOrderQuantity(quantity = itemRequest.quantity) { + return {body: string `Invalid quantity for product ${itemRequest.productId}`}; + } + + product:Product|http:NotFound productResult = self.productService.getProduct(productId = itemRequest.productId); + if productResult is http:NotFound { + return {body: string `Product ${itemRequest.productId} not found`}; + } + + product:Product productData = productResult; + + if productData.stockQuantity < itemRequest.quantity { + return {body: string `Insufficient stock for product ${productData.name}`}; + } + + decimal subtotal = calculateSubtotal(quantity = itemRequest.quantity, unitPrice = productData.price); + + OrderItem orderItem = { + productId: productData.productId, + productName: productData.name, + quantity: itemRequest.quantity, + unitPrice: productData.price, + subtotal: subtotal + }; + + orderItems.push(orderItem); + } + + decimal totalAmount = calculateTotalAmount(items = orderItems); + string orderId = string `${ORDER_ID_PREFIX}-${uuid:createType1AsString()}`; + string currentTime = time:utcToString(time:utcNow()); + + Order 'order = { + orderId: orderId, + customerId: request.customerId, + items: orderItems, + totalAmount: totalAmount, + status: PENDING, + shippingAddress: request.shippingAddress, + createdAt: currentTime, + updatedAt: currentTime + }; + + orderStore[orderId] = 'order; + return 'order; + } + + resource function get orders/[string orderId]() returns Order|http:NotFound { + Order? 'order = orderStore[orderId]; + if 'order is () { + return {body: "Order not found"}; + } + return 'order; + } + + resource function get orders(string? customerId = (), OrderStatus? status = ()) returns Order[] { + Order[] orders = orderStore.toArray(); + + if customerId is string { + Order[] filteredOrders = from Order ord in orders + where ord.customerId == customerId + select ord; + orders = filteredOrders; + } + + if status is OrderStatus { + Order[] filteredOrders = from Order ord in orders + where ord.status == status + select ord; + orders = filteredOrders; + } + + return orders; + } + + resource function put orders/[string orderId]/status(OrderUpdateStatusRequest request) returns Order|http:NotFound|http:BadRequest { + Order? existingOrder = orderStore[orderId]; + if existingOrder is () { + return {body: "Order not found"}; + } + + if request.status == CANCELLED && !canCancelOrder(status = existingOrder.status) { + return {body: "Cannot cancel order in current status"}; + } + + Order updatedOrder = existingOrder.clone(); + updatedOrder.status = request.status; + updatedOrder.updatedAt = time:utcToString(time:utcNow()); + + orderStore[orderId] = updatedOrder; + return updatedOrder; + } + + resource function delete orders/[string orderId]() returns http:NoContent|http:NotFound|http:BadRequest { + Order? existingOrder = orderStore[orderId]; + if existingOrder is () { + return {body: "Order not found"}; + } + + if !canCancelOrder(status = existingOrder.status) { + return {body: "Cannot delete order in current status"}; + } + return http:NO_CONTENT; + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/types.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/types.bal new file mode 100644 index 0000000000..529f674de5 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/types.bal @@ -0,0 +1,44 @@ +// Order module type definitions + +public type Order record {| + string orderId; + string customerId; + OrderItem[] items; + decimal totalAmount; + OrderStatus status; + string shippingAddress; + string createdAt; + string updatedAt; +|}; + +public type OrderItem record {| + string productId; + string productName; + int quantity; + decimal unitPrice; + decimal subtotal; +|}; + +public type OrderCreateRequest record {| + string customerId; + OrderItemRequest[] items; + string shippingAddress; +|}; + +public type OrderItemRequest record {| + string productId; + int quantity; +|}; + +public type OrderUpdateStatusRequest record {| + OrderStatus status; +|}; + +public enum OrderStatus { + PENDING, + CONFIRMED, + PROCESSING, + SHIPPED, + DELIVERED, + CANCELLED +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/utils.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/utils.bal new file mode 100644 index 0000000000..572c221243 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/order/utils.bal @@ -0,0 +1,22 @@ +// Order module utility functions + +public function calculateSubtotal(int quantity, decimal unitPrice) returns decimal { + decimal quantityDecimal = quantity; + return quantityDecimal * unitPrice; +} + +public function calculateTotalAmount(OrderItem[] items) returns decimal { + decimal total = ZERO_AMOUNT; + foreach OrderItem item in items { + total = total + item.subtotal; + } + return total; +} + +public function validateOrderQuantity(int quantity) returns boolean { + return quantity >= MIN_ORDER_QUANTITY; +} + +public function canCancelOrder(OrderStatus status) returns boolean { + return status == PENDING || status == CONFIRMED; +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/constants.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/constants.bal new file mode 100644 index 0000000000..d8b0d62d30 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/constants.bal @@ -0,0 +1,4 @@ +// Payment module constants + +public const string PAYMENT_ID_PREFIX = "PAY"; +public const string TRANSACTION_ID_PREFIX = "TXN"; diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/service.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/service.bal new file mode 100644 index 0000000000..5505c7873e --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/service.bal @@ -0,0 +1,116 @@ +// Payment service implementation + +import order_management_system.'order; + +import ballerina/http; +import ballerina/time; +import ballerina/uuid; + +final map paymentStore = {}; + +public service class PaymentService { + *http:Service; + + private final 'order:OrderService orderService; + + public function init('order:OrderService orderService) { + self.orderService = orderService; + } + + resource function post payments(PaymentCreateRequest request) returns Payment|http:BadRequest|http:NotFound { + 'order:Order|http:NotFound orderResult = self.orderService.getOrder(orderId = request.orderId); + if orderResult is http:NotFound { + return {body: "Order not found"}; + } + + 'order:Order orderData = orderResult; + + boolean paymentSuccess = processPayment(paymentMethod = request.paymentMethod, paymentDetails = request.paymentDetails); + + string paymentId = string `${PAYMENT_ID_PREFIX}-${uuid:createType1AsString()}`; + string currentTime = time:utcToString(time:utcNow()); + + PaymentStatus initialStatus = PENDING; + string? transactionId = (); + + if paymentSuccess { + initialStatus = COMPLETED; + transactionId = generateTransactionId(); + } else { + initialStatus = FAILED; + } + + Payment payment = { + paymentId: paymentId, + orderId: request.orderId, + customerId: orderData.customerId, + amount: orderData.totalAmount, + paymentMethod: request.paymentMethod, + status: initialStatus, + transactionId: transactionId, + createdAt: currentTime, + updatedAt: currentTime + }; + + lock { + paymentStore[paymentId] = payment; + } + return payment; + } + + resource function get payments/[string paymentId]() returns Payment|http:NotFound { + lock { + Payment? payment = paymentStore[paymentId]; + if payment is () { + return {body: "Payment not found"}; + } + return payment.cloneReadOnly(); + } + } + + resource function get payments(string? orderId = (), string? customerId = ()) returns Payment[] { + lock { + Payment[] payments = paymentStore.toArray(); + + if orderId is string { + Payment[] filteredPayments = from Payment pay in payments + where pay.orderId == orderId + select pay; + payments = filteredPayments; + } + + if customerId is string { + Payment[] filteredPayments = from Payment pay in payments + where pay.customerId == customerId + select pay; + payments = filteredPayments; + } + + return payments.cloneReadOnly(); + } + } + + resource function post payments/[string paymentId]/refund() returns Payment|http:NotFound|http:BadRequest { + Payment? existingPayment = (); + lock { + existingPayment = paymentStore[paymentId]; + } + + if existingPayment is () { + return {body: "Payment not found"}; + } + + if !canRefundPayment(status = existingPayment.status) { + return {body: "Cannot refund payment in current status"}; + } + + Payment updatedPayment = existingPayment.clone(); + updatedPayment.status = REFUNDED; + updatedPayment.updatedAt = time:utcToString(time:utcNow()); + + lock { + paymentStore[paymentId] = updatedPayment.clone(); + } + return updatedPayment; + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/types.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/types.bal new file mode 100644 index 0000000000..4089fe1e54 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/types.bal @@ -0,0 +1,42 @@ +// Payment module type definitions + +public type Payment record {| + string paymentId; + string orderId; + string customerId; + decimal amount; + PaymentMethod paymentMethod; + PaymentStatus status; + string? transactionId?; + string createdAt; + string updatedAt; +|}; + +public type PaymentCreateRequest record {| + string orderId; + PaymentMethod paymentMethod; + PaymentDetails paymentDetails; +|}; + +public type PaymentDetails record {| + string? cardNumber?; + string? cardHolderName?; + string? expiryDate?; + string? cvv?; +|}; + +public enum PaymentMethod { + CREDIT_CARD, + DEBIT_CARD, + PAYPAL, + BANK_TRANSFER, + CASH_ON_DELIVERY +} + +public enum PaymentStatus { + PENDING, + PROCESSING, + COMPLETED, + FAILED, + REFUNDED +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/utils.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/utils.bal new file mode 100644 index 0000000000..6a959565a0 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/payment/utils.bal @@ -0,0 +1,29 @@ +// Payment module utility functions + +import ballerina/uuid; + +public function generateTransactionId() returns string { + return string `${TRANSACTION_ID_PREFIX}-${uuid:createType1AsString()}`; +} + +public function processPayment(PaymentMethod paymentMethod, PaymentDetails paymentDetails) returns boolean { + // Simulate payment processing logic + if paymentMethod == CASH_ON_DELIVERY { + return true; + } + + // For card payments, validate basic details + if paymentMethod == CREDIT_CARD || paymentMethod == DEBIT_CARD { + string? cardNumber = paymentDetails?.cardNumber; + string? cardHolderName = paymentDetails?.cardHolderName; + if cardNumber is () || cardHolderName is () { + return false; + } + } + + return true; +} + +public function canRefundPayment(PaymentStatus status) returns boolean { + return status == COMPLETED; +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/constants.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/constants.bal new file mode 100644 index 0000000000..61b901a9c8 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/constants.bal @@ -0,0 +1,5 @@ +// Product module constants + +public const string PRODUCT_ID_PREFIX = "PROD"; +public const decimal MIN_PRICE = 0.01; +public const int MIN_STOCK = 0; diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/service.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/service.bal new file mode 100644 index 0000000000..62f17b8dd9 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/service.bal @@ -0,0 +1,130 @@ +// Product service implementation + +import ballerina/http; +import ballerina/time; +import ballerina/uuid; + +final map productStore = {}; + +public service class ProductService { + *http:Service; + + public function getProduct(string productId) returns Product|http:NotFound { + Product? product = productStore[productId]; + if product is () { + return {body: "Product not found"}; + } + return product; + } + + resource function post products(ProductCreateRequest request) returns Product|http:BadRequest { + if !validatePrice(price = request.price) { + return {body: "Invalid price"}; + } + if !validateStock(quantity = request.stockQuantity) { + return {body: "Invalid stock quantity"}; + } + + string productId = string `${PRODUCT_ID_PREFIX}-${uuid:createType1AsString()}`; + string currentTime = time:utcToString(time:utcNow()); + + ProductStatus initialStatus = ACTIVE; + if !isProductAvailable(stockQuantity = request.stockQuantity) { + initialStatus = OUT_OF_STOCK; + } + + Product product = { + productId: productId, + name: request.name, + description: request.description, + price: request.price, + stockQuantity: request.stockQuantity, + category: request.category, + status: initialStatus, + createdAt: currentTime, + updatedAt: currentTime + }; + + productStore[productId] = product; + return product; + } + + resource function get products/[string productId]() returns Product|http:NotFound { + Product? product = productStore[productId]; + if product is () { + return {body: "Product not found"}; + } + return product; + } + + resource function get products(string? category = ()) returns Product[] { + Product[] products = productStore.toArray(); + + if category is string { + Product[] filteredProducts = from Product prod in products + where prod.category == category + select prod; + return filteredProducts; + } + + return products; + } + + resource function put products/[string productId](ProductUpdateRequest request) returns Product|http:NotFound|http:BadRequest { + Product? existingProduct = productStore[productId]; + if existingProduct is () { + return {body: "Product not found"}; + } + + Product updatedProduct = existingProduct.clone(); + + string? name = request?.name; + if name is string { + updatedProduct.name = name; + } + + string? description = request?.description; + if description is string { + updatedProduct.description = description; + } + + decimal? price = request?.price; + if price is decimal { + if !validatePrice(price = price) { + return {body: "Invalid price"}; + } + updatedProduct.price = price; + } + + int? stockQuantity = request?.stockQuantity; + if stockQuantity is int { + if !validateStock(quantity = stockQuantity) { + return {body: "Invalid stock quantity"}; + } + updatedProduct.stockQuantity = stockQuantity; + } + + string? category = request?.category; + if category is string { + updatedProduct.category = category; + } + + ProductStatus? status = request?.status; + if status is ProductStatus { + updatedProduct.status = status; + } + + updatedProduct.updatedAt = time:utcToString(time:utcNow()); + productStore[productId] = updatedProduct; + return updatedProduct; + } + + resource function delete products/[string productId]() returns http:NoContent|http:NotFound { + Product? removedProduct = productStore.removeIfHasKey(productId); + if removedProduct is () { + return {body: "Product not found"}; + } + return http:NO_CONTENT; + } +} + diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/types.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/types.bal new file mode 100644 index 0000000000..41493711db --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/types.bal @@ -0,0 +1,36 @@ +// Product module type definitions + +public type Product record {| + string productId; + string name; + string description; + decimal price; + int stockQuantity; + string category; + ProductStatus status; + string createdAt; + string updatedAt; +|}; + +public type ProductCreateRequest record {| + string name; + string description; + decimal price; + int stockQuantity; + string category; +|}; + +public type ProductUpdateRequest record {| + string? name?; + string? description?; + decimal? price?; + int? stockQuantity?; + string? category?; + ProductStatus? status?; +|}; + +public enum ProductStatus { + ACTIVE, + INACTIVE, + OUT_OF_STOCK +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/utils.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/utils.bal new file mode 100644 index 0000000000..70096bc3cf --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap/source/order_management_system/modules/product/utils.bal @@ -0,0 +1,13 @@ +// Product module utility functions + +public function validatePrice(decimal price) returns boolean { + return price >= MIN_PRICE; +} + +public function validateStock(int quantity) returns boolean { + return quantity >= MIN_STOCK; +} + +public function isProductAvailable(int stockQuantity) returns boolean { + return stockQuantity > 0; +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/config/project.json b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/config/project.json new file mode 100644 index 0000000000..7acb91a2ed --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/config/project.json @@ -0,0 +1,251 @@ +{ + "description": "Simple test project - test incremental code map", + "source": "project/main.bal", + "output": { + "main.bal": { + "artifacts": [ + { + "name": "ballerina/io", + "type": "IMPORT", + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 20 + } + }, + "properties": { + "moduleName": "io", + "orgName": "ballerina" + }, + "children": [] + }, + { + "name": "sayHello", + "type": "FUNCTION", + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 6, + "character": 1 + } + }, + "properties": { + "returns": "()", + "comment": "Basic function with no parameters and no return value", + "modifiers": [ + "public" + ], + "parameters": [], + "documentation": "Function with documentation" + }, + "children": [] + }, + { + "name": "greet", + "type": "FUNCTION", + "range": { + "start": { + "line": 9, + "character": 0 + }, + "end": { + "line": 13, + "character": 1 + } + }, + "properties": { + "returns": "()", + "comment": "Function with parameters", + "modifiers": [ + "public" + ], + "parameters": [ + "name: string" + ], + "documentation": "Function with,\nMultiple lines of documentation" + }, + "children": [] + }, + { + "name": "add", + "type": "FUNCTION", + "range": { + "start": { + "line": 16, + "character": 0 + }, + "end": { + "line": 18, + "character": 1 + } + }, + "properties": { + "returns": "int", + "comment": "Function with return value", + "modifiers": [ + "public" + ], + "parameters": [ + "a: int", + "b: int" + ] + }, + "children": [] + }, + { + "name": "divideAndRemainder", + "type": "FUNCTION", + "range": { + "start": { + "line": 21, + "character": 0 + }, + "end": { + "line": 25, + "character": 1 + } + }, + "properties": { + "returns": "[int, int]", + "comment": "Function with multiple return values", + "modifiers": [ + "public" + ], + "parameters": [ + "dividend: int", + "divisor: int" + ] + }, + "children": [] + }, + { + "name": "calculateInterest", + "type": "FUNCTION", + "range": { + "start": { + "line": 28, + "character": 0 + }, + "end": { + "line": 30, + "character": 1 + } + }, + "properties": { + "returns": "decimal", + "comment": "Function with default parameter values", + "modifiers": [ + "public" + ], + "parameters": [ + "principal: decimal", + "rate: decimal", + "years: int = 1" + ] + }, + "children": [] + }, + { + "name": "sum", + "type": "FUNCTION", + "range": { + "start": { + "line": 33, + "character": 0 + }, + "end": { + "line": 39, + "character": 1 + } + }, + "properties": { + "returns": "int", + "comment": "Function with rest parameters", + "modifiers": [ + "public" + ], + "parameters": [ + "numbers: int..." + ] + }, + "children": [] + }, + { + "name": "divide", + "type": "FUNCTION", + "range": { + "start": { + "line": 42, + "character": 0 + }, + "end": { + "line": 47, + "character": 1 + } + }, + "properties": { + "returns": "float|error", + "comment": "Function with error handling", + "modifiers": [ + "public" + ], + "parameters": [ + "a: int", + "b: int" + ] + }, + "children": [] + }, + { + "name": "fn\\#with\\-Identifiers", + "type": "FUNCTION", + "range": { + "start": { + "line": 50, + "character": 0 + }, + "end": { + "line": 52, + "character": 1 + } + }, + "properties": { + "parameters": [], + "returns": "()" + }, + "children": [] + }, + { + "name": "main", + "type": "FUNCTION", + "range": { + "start": { + "line": 55, + "character": 0 + }, + "end": { + "line": 74, + "character": 1 + } + }, + "properties": { + "returns": "()", + "comment": "Main function", + "modifiers": [ + "public" + ], + "parameters": [] + }, + "children": [] + } + ] + } + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/Ballerina.toml b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/Ballerina.toml new file mode 100644 index 0000000000..9dad5b6703 --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "wso2" +name = "test" +version = "0.1.0" +distribution = "2201.13.1" + +[build-options] +observabilityIncluded = true diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/main.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/main.bal new file mode 100644 index 0000000000..2c4521e82d --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/main.bal @@ -0,0 +1,75 @@ +import ballerina/io; + +// Basic function with no parameters and no return value +# Function with documentation +public function sayHello() { + io:println("Hello, World!"); +} + +// Function with parameters +# Function with, +# Multiple lines of documentation +public function greet(string name) { + io:println("Hello, " + name + "!"); +} + +// Function with return value +public function add(int a, int b) returns int { + return a + b; +} + +// Function with multiple return values +public function divideAndRemainder(int dividend, int divisor) returns [int, int] { + int quotient = dividend / divisor; + int remainder = dividend % divisor; + return [quotient, remainder]; +} + +// Function with default parameter values +public function calculateInterest(decimal principal, decimal rate, int years = 1) returns decimal { + return principal * rate * years / 100; +} + +// Function with rest parameters +public function sum(int... numbers) returns int { + int total = 0; + foreach int num in numbers { + total += num; + } + return total; +} + +// Function with error handling +public function divide(int a, int b) returns float|error { + if (b == 0) { + return error("Division by zero"); + } + return a / b; +} + + +function fn\#with\-Identifiers() { + +} + +// Main function +public function main() { + sayHello(); + greet("Ballerina"); + + int result = add(5, 3); + io:println("5 + 3 = " + result.toString()); + + decimal interest = calculateInterest(1000, 5.5); + io:println("Interest: " + interest.toString()); + + int total = sum(1, 2, 3, 4, 5); + io:println("Sum: " + total.toString()); + + var divResult = divide(10, 2); + if (divResult is float) { + io:println("10 รท 2 = " + divResult.toString()); + } else { + io:println("Error: " + divResult.message()); + } +} diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/service.bal b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/service.bal new file mode 100644 index 0000000000..666521814f --- /dev/null +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/codemap_changes/source/project/service.bal @@ -0,0 +1,78 @@ +import ballerina/http; +import ballerina/log; + +// Define the service +service /root/path\-id on new http:Listener(9090) { + + // Resource function with escaped characters + resource function get\#gre eting() returns string { + return "Hello, World! Welcome to Ballerina Service"; + } + + resource function get gre\#eting() returns string { + return "Hello, World! Welcome to Ballerina Service"; + } + + // Resource function to handle GET requests at the root path + resource function post greeting(string name) returns string { + return "Hello, " + name + "! Welcome to Ballerina Service"; + } + + // Resource function with path parameter + resource function get echo/[string message]() returns string { + return "Echo: " + message; + } + + // Resource function to handle POST requests + resource function post data(@http:Payload json payload) returns json { + return { + "message": "Data received successfully", + "data": payload + }; + } +} + +public listener http:Listener securedEP = new (9091, + secureSocket = { + key: { + certFile: "../resource/path/to/public.crt", + keyFile: "../resource/path/to/private.key" + } + } +); + +listener http:Listener refListener = securedEP; + +final http:Client httpClient = check new (""); + +@display { + label: "BIT Service" +} +service / on securedEP, securedEP, new http:Listener(9092) { + + function init() returns error? { + } + + resource function get greeting() returns json|http:InternalServerError { + do { + json j = check httpClient->/; + log:printInfo(j.toJsonString()); + } on fail error e { + log:printError("Error: ", 'error = e); + return http:INTERNAL_SERVER_ERROR; + } + } +} + +service /api/v1 on refListener { + + resource function get path() returns json|http:InternalServerError { + do { + return 0; + } on fail error e { + log:printError("Error: ", 'error = e); + return http:INTERNAL_SERVER_ERROR; + } + } +} + diff --git a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/testng.xml b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/testng.xml index 2ffb621c03..5fdb7f388f 100644 --- a/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/testng.xml +++ b/architecture-model-generator/modules/architecture-model-generator-ls-extension/src/test/resources/testng.xml @@ -26,8 +26,10 @@ under the License. + +