Skip to content

Commit

Permalink
chore: refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
CristiCanizales committed Nov 18, 2024
1 parent 7340e91 commit 805e939
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 130 deletions.
12 changes: 8 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"ts-jest": "^29.1.1",
"ts-loader": "^9.3.0",
"ts-node": "10.9.2",
"typescript": "^5.6.2"
"typescript": "^5.6.2",
"yaml": "2.6.0"
},
"scripts": {
"postinstall": "npm run bootstrap && npm run reformat && npm run check:peer-deps && npm run check:typescript-project-references",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import { notificationService, workspaceUtils } from '@salesforce/salesforcedx-ut
import * as fs from 'fs';
import { OpenAPIV3 } from 'openapi-types'; // Adjust the import path as necessary
import * as path from 'path';
import { QuickPickItem } from 'vscode';
import * as vscode from 'vscode';
import { nls } from '../messages';
import { stringify } from 'yaml';
import { getTelemetryService } from '../telemetry/telemetry';
import { MetadataOrchestrator } from './metadataOrchestrator';
import { MetadataOrchestrator, MethodMetadata } from './metadataOrchestrator';

export class ApexActionController {
constructor(
Expand All @@ -23,32 +22,8 @@ export class ApexActionController {
increment?: number | undefined;
}>
) {}
public listApexMethods = (apexClassPath: string): Promise<QuickPickItem[]> => {
// Read the content of the Apex class file
const fileContent = fs.readFileSync(apexClassPath).toString();

// Regular expression to match method declarations in Apex
const methodRegExp = /@[\w]+\s*\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+(\w+)\s*\(/g;

const methods: QuickPickItem[] = [];
let match;

// Extract all method names that match the regular expression
while ((match = methodRegExp.exec(fileContent)) !== null) {
const methodName = match[3];
methods.push({
label: methodName,
description: apexClassPath
});
}

// Sort the methods alphabetically by name
methods.sort((a, b) => a.label.localeCompare(b.label));

return Promise.resolve(methods);
};

public createApexActionFromMethod = async (methodIdentifier: string): Promise<void> => {
public createApexActionFromMethod = async (): Promise<void> => {
const telemetryService = await getTelemetryService();
const progressReporter: Progress<any> = {
report: value => {
Expand All @@ -58,32 +33,46 @@ export class ApexActionController {
}
};
try {
// Step 1: Validate Method
if (!this.isMethodEligible(methodIdentifier)) {
// // Step 0: Validate Method
// if (!this.isMethodEligible(methodIdentifier)) {
// void notificationService.showErrorMessage(
// '`Method ${methodIdentifier} is not eligible for Apex Action creation.`'
// );
// throw new Error(`Method ${methodIdentifier} is not eligible for Apex Action creation.`);
// }

// Step 1: Extract Metadata
progressReporter.report({ message: 'Extracting metadata.' });
const metadata = this.metadataOrchestrator.extractMethodMetadata();
if (!metadata) {
void notificationService.showErrorMessage('Failed to extract metadata from selected method.');
throw new Error('Failed to extract metadata from selected method.');
}

// Step 2: Validate Method
if (!this.metadataOrchestrator.validateAuraEnabledMethod(metadata.isAuraEnabled)) {
void notificationService.showErrorMessage(
'`Method ${methodIdentifier} is not eligible for Apex Action creation.`'
`Method ${metadata.name} is not eligible for Apex Action creation. It is NOT annotated with @AuraEnabled.`
);
throw new Error(
`Method ${metadata.name} is not eligible for Apex Action creation. It is NOT annotated with @AuraEnabled.`
);
throw new Error(`Method ${methodIdentifier} is not eligible for Apex Action creation.`);
}

// Step 2: Extract Metadata
progressReporter.report({ message: 'Extracting metadata.' });
const metadata = await this.metadataOrchestrator.extractMethodMetadata(methodIdentifier);

// Step 3: Generate OpenAPI Document
progressReporter.report({ message: 'Generating OpenAPI document.' });
const openApiDocument = this.generateOpenAPIDocument(metadata);

// Step 4: Write OpenAPI Document to File
const openApiFilePath = `${methodIdentifier}_openapi.json`;
const openApiFilePath = `${metadata.name}_openapi.yml`;
await this.saveDocument(openApiFilePath, openApiDocument);

// Step 6: Notify Success
notificationService.showInformationMessage(`Apex Action created for method: ${methodIdentifier}`);
telemetryService.sendEventData('ApexActionCreated', { method: methodIdentifier });
notificationService.showInformationMessage(`Apex Action created for method: ${metadata.name}.`);
telemetryService.sendEventData('ApexActionCreated', { method: metadata.name });
} catch (error) {
// Error Handling
notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}`);
notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}.`);
telemetryService.sendException('ApexActionCreationFailed', error);
throw error;
}
Expand All @@ -100,26 +89,38 @@ export class ApexActionController {
fs.mkdirSync(openAPIdocumentsPath);
}
const saveLocation = path.join(openAPIdocumentsPath, fileName);
fs.writeFileSync(saveLocation, JSON.stringify(content));
fs.writeFileSync(saveLocation, content);
await vscode.workspace.openTextDocument(saveLocation).then((newDocument: any) => {
void vscode.window.showTextDocument(newDocument);
});
};

public generateOpenAPIDocument = (metadata: any): OpenAPIV3.Document => {
public generateOpenAPIDocument = (metadata: MethodMetadata): string => {
// Placeholder for OpenAPI generation logic
return {
// ProgressNotification.show(execution, cancellationTokenSource);
const openAPIDocument: OpenAPIV3.Document = {
openapi: '3.0.0',
info: { title: 'Apex Actions', version: '1.0.0' },
paths: {
[`/apex/${metadata}`]: {
[`/apex/${metadata.name}`]: {
post: {
summary: `Invoke ${metadata}`,
operationId: metadata,
responses: { 200: { description: 'Success' } }
operationId: metadata.name,
summary: `Invoke ${metadata.name}`,
parameters: metadata.parameters as unknown as (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[],
responses: {
200: {
description: 'Success',
content: {
'application/json': { schema: { type: metadata.returnType as OpenAPIV3.NonArraySchemaObjectType } }
}
}
}
}
}
}
};

// Convert the OpenAPI document to YAML
return stringify(openAPIDocument);
};
}
67 changes: 3 additions & 64 deletions packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,13 @@
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { notificationService } from '@salesforce/salesforcedx-utils-vscode';
import { readFileSync } from 'fs';
import * as vscode from 'vscode';
import { ApexActionController } from './apexActionController';
import { MetadataOrchestrator } from './metadataOrchestrator';

const metadataOrchestrator = new MetadataOrchestrator();
const controller = new ApexActionController(metadataOrchestrator);

const validateAuraEnabledMethod = async (
filePath: string,
cursorPosition: vscode.Position,
selectedMethod: string
): Promise<void> => {
const lineNumber = cursorPosition.line;
// Read the content of the Apex class file
const fileContent = readFileSync(filePath, 'utf-8');
const lines = fileContent.split('\n');

// Start from the current line and search upward for the method declaration
for (let i = lineNumber; i >= 0; i--) {
const line = lines[i].trim();

if (line.includes(selectedMethod)) continue;
// Check if the line contains @AuraEnabled
if (line.includes('@AuraEnabled')) {
return;
}

// Check if the line contains a method declaration (regex matches methods)
const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+\w+\s*\(/;
if (methodRegex.test(line)) {
notificationService.showWarningMessage(`The method "${selectedMethod}" is NOT annotated with @AuraEnabled.`);
throw Error(`The method "${selectedMethod}" is NOT annotated with @AuraEnabled.`);
}
}
};

export const createApexActionFromMethod = async (methodIdentifier: any): Promise<void> => {
// Step 1: Prompt User to Select a Method
// const selectedMethod = await controller.listApexMethods();
const editor = vscode.window.activeTextEditor;
if (!editor) {
notificationService.showErrorMessage('No active editor detected');
throw Error('No active editor detected');
}

const document = editor.document;
let selectedMethod;
const cursorPosition = editor.selection.active; // Get cursor position
const lineText = document.lineAt(cursorPosition.line).text.trim(); // Get the line content

// Regular expression to match a method declaration and extract its name
const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+(\w+)\s*\(/;

const match = methodRegex.exec(lineText);
if (match) {
selectedMethod = match[3]; // The third capture group is the method name
}

if (!selectedMethod) {
notificationService.showErrorMessage('No method selected');
return;
}

const filePath = methodIdentifier.path;
await validateAuraEnabledMethod(filePath, cursorPosition, selectedMethod);

// Step 2: Call Controller
await controller.createApexActionFromMethod(selectedMethod);
export const createApexActionFromMethod = async (): Promise<void> => {
// Call Controller
await controller.createApexActionFromMethod();
};
112 changes: 98 additions & 14 deletions packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,109 @@
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { notificationService } from '@salesforce/salesforcedx-utils-vscode';
import * as vscode from 'vscode';
export interface MethodMetadata {
name: string;
parameters: Parameter[];
returnType: string;
isAuraEnabled: boolean;
}
export interface Parameter {
name: string;
in: string;
required: boolean;
description: string;
schema: { type: string };
}
export class MetadataOrchestrator {
constructor() {
// Initialization code here
}

public async orchestrate(): Promise<void> {
try {
// Orchestration logic here
} catch (error) {
console.error('Error during orchestration:', error);
public extractMethodMetadata = (): MethodMetadata | undefined => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
notificationService.showErrorMessage('No active editor detected.');
return;
}
}
public async extractMethodMetadata(methodIdentifier: any): Promise<string | undefined> {
try {
// Logic to extract method metadata here
return methodIdentifier;
} catch (error) {
console.error('Error extracting method metadata:', error);

const document = editor.document;
const cursorPosition = editor.selection.active;
const lines = document.getText().split('\n');
const currentLineIndex = cursorPosition.line;

let methodSignature = '';
let isAuraEnabled = false;

// Check if the preceding line contains @AuraEnabled
if (currentLineIndex > 0 && lines[currentLineIndex - 1].includes('@AuraEnabled')) {
isAuraEnabled = true;
}
return undefined;
}

// Traverse lines starting from the cursor position to construct the method signature
for (let i = currentLineIndex; i < lines.length; i++) {
const line = lines[i].trim();
methodSignature += ` ${line}`;

// Stop once the closing parenthesis is reached
if (line.includes(')')) {
break;
}
}

if (!methodSignature) {
notificationService.showWarningMessage('No valid method found at cursor position.');
return;
}

// Parse the method signature
const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?([\w<>\[\]]+)\s+(\w+)\s*\((.*?)\)/s;
const match = methodRegex.exec(methodSignature);
if (!match) {
notificationService.showWarningMessage('Failed to parse method signature.');
throw Error('Failed to parse method signature.');
}
const returnType = match[3];
const methodName = match[4];
const parametersRaw = match[5] ? match[5].split(',').map(param => param.trim()) : [];
const parameters = parametersRaw.map(param => {
const [type, name] = param.split(/\s+/);
return {
name,
in: 'query',
required: true,
description: `The ${name} parameter of type ${type}.`,
schema: { type: this.mapApexTypeToJsonType(type) }
};
});
return {
name: methodName,
parameters,
returnType,
isAuraEnabled
};
};
private mapApexTypeToJsonType = (apexType: string): string => {
switch (apexType.toLowerCase()) {
case 'string':
return 'string';
case 'integer':
case 'int':
case 'long':
return 'integer';
case 'boolean':
return 'boolean';
case 'decimal':
case 'double':
case 'float':
return 'number';
default:
return 'string';
}
};

public validateAuraEnabledMethod = (isAuraEnabled: boolean): boolean => {
return isAuraEnabled;
};
}

0 comments on commit 805e939

Please sign in to comment.