Skip to content

Commit

Permalink
feat: add logic to rerun validations and show validations in correct …
Browse files Browse the repository at this point in the history
…file
  • Loading branch information
madhur310 committed Feb 11, 2025
1 parent 603a821 commit ce0b687
Show file tree
Hide file tree
Showing 14 changed files with 280 additions and 95 deletions.
16 changes: 16 additions & 0 deletions packages/salesforcedx-vscode-apex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,20 @@
{
"command": "sf.create.apex.action.class",
"when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/ && salesforcedx-einstein-gpt.isEnabled"
},
{
"command": "sf.validate.oas.document",
"when": "sf:project_opened && sf:has_target_org && (resource =~ /.\\.(xml)?$/ || resource =~ /.\\.(yaml)?$/) && salesforcedx-einstein-gpt.isEnabled"
}
],
"explorer/context": [
{
"command": "sf.create.apex.action.class",
"when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/ && resourcePath =~ /classes/ && salesforcedx-einstein-gpt.isEnabled"
},
{
"command": "sf.validate.oas.document",
"when": "sf:project_opened && sf:has_target_org && (resource =~ /.\\.externalServiceRegistration-meta\\.(xml)?$/ || resource =~ /.\\.(yaml)?$/) && salesforcedx-einstein-gpt.isEnabled"
}
],
"view/title": [
Expand Down Expand Up @@ -264,6 +272,10 @@
{
"command": "sf.create.apex.action.class",
"when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/ && resourcePath =~ /classes/ && salesforcedx-einstein-gpt.isEnabled"
},
{
"command": "sf.validate.oas.document",
"when": "sf:project_opened && sf:has_target_org && (resource =~ /.\\.externalServiceRegistration-meta\\.(xml)?$/ || resource =~ /.\\.(yaml)?$/) && salesforcedx-einstein-gpt.isEnabled"
}
]
},
Expand Down Expand Up @@ -375,6 +387,10 @@
{
"command": "sf.create.apex.action.class",
"title": "%create_openapi_doc_class%"
},
{
"command": "sf.validate.oas.document",
"title": "%validate_oas_document%"
}
],
"configuration": {
Expand Down
1 change: 1 addition & 0 deletions packages/salesforcedx-vscode-apex/package.nls.ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"collapse_tests_title": "SFDX: Apex テストを隠す",
"create_openapi_doc_method": "SFDX: Create OpenAPI Document from Selected Method",
"create_openapi_doc_class": "SFDX: Create OpenAPI Document from This Class (Beta)",
"validate_oas_document": "SFDX: Validate OpenAPI Document (Beta)",
"enable-apex-ls-error-to-telemetry": "Allow the Apex Language Server to collect telemetry of errors",
"go_to_definition_title": "定義に移動",
"java_home_description": "Specifies the folder path to the Java 11, Java 17, or Java 21 runtime used to launch the Apex Language Server. Note on Windows the backslashes must be escaped.\n\nMac Example: `/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home`\n\nWindows Example: `C:\\\\Program Files\\\\Zulu\\\\zulu-17`\n\nLinux Example: `/usr/lib/jvm/java-21-openjdk-amd64`",
Expand Down
1 change: 1 addition & 0 deletions packages/salesforcedx-vscode-apex/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"collapse_tests_title": "SFDX: Collapse All Apex Tests",
"create_openapi_doc_method": "SFDX: Create OpenAPI Document from Selected Method",
"create_openapi_doc_class": "SFDX: Create OpenAPI Document from This Class (Beta)",
"validate_oas_document": "SFDX: Validate OpenAPI Document (Beta)",
"enable-apex-ls-error-to-telemetry": "Allow the Apex Language Server to collect telemetry of errors",
"go_to_definition_title": "Go to Definition",
"java_home_description": "Specifies the folder path to the Java 11, Java 17, or Java 21 runtime used to launch the Apex Language Server. Note on Windows the backslashes must be escaped.\n\nMac Example: `/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home`\n\nWindows Example: `C:\\\\Program Files\\\\Zulu\\\\zulu-17`\n\nLinux Example: `/usr/lib/jvm/java-21-openjdk-amd64`",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,28 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { SfProject } from '@salesforce/core-bundle';
import { notificationService, WorkspaceContextUtil, workspaceUtils } from '@salesforce/salesforcedx-utils-vscode';
import { RegistryAccess } from '@salesforce/source-deploy-retrieve-bundle';
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
import * as fs from 'fs';
import { OpenAPIV3 } from 'openapi-types';
import * as path from 'path';
import * as vscode from 'vscode';
import { parse, stringify } from 'yaml';
import { stringify } from 'yaml';
import { workspaceContext } from '../context';
import { nls } from '../messages';
import { OasProcessor } from '../oas/documentProcessorPipeline/oasProcessor';
import { BidRule, PromptGenerationOrchestrator } from '../oas/promptGenerationOrchestrator';
import {
ApexClassOASEligibleResponse,
ApexClassOASGatherContextResponse,
ApexOASInfo,
ExternalServiceOperation
} from '../oas/schemas';
import { ApexOASInfo, ExternalServiceOperation } from '../oas/schemas';
import { getTelemetryService } from '../telemetry/telemetry';
import { MetadataOrchestrator } from './metadataOrchestrator';
import { checkIfESRIsDecomposed, createProblemTabEntriesForOasDocument, processOasDocument } from './oasUtils';
export class ApexActionController {
private isESRDecomposed: boolean = false;
constructor(private metadataOrchestrator: MetadataOrchestrator) {}

public async initialize(extensionContext: vscode.ExtensionContext) {
await WorkspaceContextUtil.getInstance().initialize(extensionContext);
this.isESRDecomposed = await this.checkIfESRIsDecomposed();
this.isESRDecomposed = await checkIfESRIsDecomposed();
}

/**
Expand Down Expand Up @@ -88,13 +82,13 @@ export class ApexActionController {
);

// Step 7: Process the OAS document
const processedOasDoc = await this.processOasDocument(openApiDocument, context, eligibilityResult);
const processedOasResult = await processOasDocument(openApiDocument, context, eligibilityResult);

// Step 8: Write OpenAPI Document to File
progress.report({ message: nls.localize('write_openapi_document') });
await this.saveOasAsEsrMetadata(processedOasDoc, fullPath[1]);
await this.saveOasAsEsrMetadata(processedOasResult.yaml, fullPath[1]);

// Step 7: If the user chose to merge, open a diff between the original and new ESR files
// Step 9: If the user chose to merge, open a diff between the original and new ESR files
if (fullPath[0] !== fullPath[1]) {
void this.openDiffFile(fullPath[0], fullPath[1], 'Manual Diff of ESR XML Files');

Expand All @@ -108,7 +102,14 @@ export class ApexActionController {
}
}

// Step 8: Call Mulesoft extension if installed
// Step: 10 Create entries in problems tab for generated file
createProblemTabEntriesForOasDocument(
this.isESRDecomposed ? this.replaceXmlToYaml(fullPath[0]) : fullPath[0],
processedOasResult,
this.isESRDecomposed
);

// Step 11: Call Mulesoft extension if installed
const callMulesoftExtension = async () => {
if (await this.isCommandAvailable('mule-dx-api.open-api-project')) {
try {
Expand Down Expand Up @@ -149,17 +150,6 @@ export class ApexActionController {
}
};

private processOasDocument = async (
oasDoc: string,
context: ApexClassOASGatherContextResponse,
eligibleResult: ApexClassOASEligibleResponse
): Promise<OpenAPIV3.Document> => {
const parsed = parse(oasDoc);
const oasProcessor = new OasProcessor(context, parsed, eligibleResult);
const processResult = await oasProcessor.process();
return processResult.yaml;
};

/**
* Handles errors by showing a notification and sending telemetry data.
* @param error - The error to handle.
Expand Down Expand Up @@ -468,21 +458,6 @@ export class ApexActionController {
return operations.filter((operation): operation is ExternalServiceOperation => operation !== null);
};

/**
* Reads sfdx-project.json and checks if decomposeExternalServiceRegistrationBeta is enabled.
* @returns boolean - true if sfdx-project.json contains decomposeExternalServiceRegistrationBeta
*/
private checkIfESRIsDecomposed = async (): Promise<boolean> => {
const projectPath = workspaceUtils.getRootWorkspacePath();
const sfProject = await SfProject.resolve(projectPath);
const sfdxProjectJson = sfProject.getSfProjectJson();
if (sfdxProjectJson.getContents().sourceBehaviorOptions?.includes('decomposeExternalServiceRegistrationBeta')) {
return true;
}

return false;
};

/**
* Builds the YAML file for the ESR using safeOasSpec as the contents.
* @param esrXmlPath - The path to the ESR XML file.
Expand Down
1 change: 1 addition & 0 deletions packages/salesforcedx-vscode-apex/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export {
} from './apexTestRunCodeAction';
export { apexTestSuiteAdd, apexTestSuiteCreate, apexTestSuiteRun } from './apexTestSuite';
export { createApexActionFromMethod, createApexActionFromClass } from './createApexAction';
export { validateOpenApiDocument } from './oasDocumentChecker';
export { launchApexReplayDebuggerWithCurrentFile } from './launchApexReplayDebuggerWithCurrentFile';
136 changes: 136 additions & 0 deletions packages/salesforcedx-vscode-apex/src/commands/oasDocumentChecker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2025, salesforce.com, inc.
* All rights reserved.
* 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, WorkspaceContextUtil } from '@salesforce/salesforcedx-utils-vscode';
import { XMLParser } from 'fast-xml-parser';
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { nls } from '../messages';
import { getTelemetryService } from '../telemetry/telemetry';
import { checkIfESRIsDecomposed, createProblemTabEntriesForOasDocument, processOasDocument } from './oasUtils';
// This class runs the validation and correction logic on Oas Documents
export class OasDocumentChecker {
private isESRDecomposed: boolean = false;
private static _instance: OasDocumentChecker;

private constructor() {}

public static get Instance() {
// Do you need arguments? Make it a regular static method instead.
return this._instance || (this._instance = new this());
}

public async initialize(extensionContext: vscode.ExtensionContext) {
await WorkspaceContextUtil.getInstance().initialize(extensionContext);
this.isESRDecomposed = await checkIfESRIsDecomposed();
}

/**
* Validates an OpenAPI Document.
* @param isClass - Indicates if the action is for a class or a method.
*/
public validateOasDocument = async (sourceUri: vscode.Uri | vscode.Uri[]): Promise<void> => {
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'SFDX: Running validations on OAS Document',
cancellable: true
},
async () => {
if (Array.isArray(sourceUri)) {
throw nls.localize('invalid_file_for_generating_oas_doc');
}

const fullPath = sourceUri ? sourceUri.fsPath : vscode.window.activeTextEditor?.document.uri.fsPath || '';

// Step 1: Validate eligibility
if (!this.isFilePathEligible(fullPath)) {
throw nls.localize('invalid_file_for_generating_oas_doc');
}
// Step 2: Extract openAPI document if embedded inside xml
let openApiDocument: string;
if (fullPath.endsWith('.xml')) {
const xmlContent = fs.readFileSync(fullPath, 'utf8');
const parser = new XMLParser();
const jsonObj = parser.parse(xmlContent);
openApiDocument = jsonObj.ExternalServiceRegistration?.schema;
} else {
openApiDocument = fs.readFileSync(fullPath, 'utf8');
}
// Step 3: Process the OAS document
const processedOasResult = await processOasDocument(openApiDocument, undefined, undefined, true);

// Step 4: Report/Refresh problems found
createProblemTabEntriesForOasDocument(fullPath, processedOasResult, this.isESRDecomposed);

const telemetryService = await getTelemetryService();
// Step 5: Notify Success
notificationService.showInformationMessage(
nls.localize('check_openapi_doc_succeeded', path.basename(fullPath))
);
telemetryService.sendEventData('OasValidationSucceeded');
}
);
} catch (error: any) {
void this.handleError(error, 'OasValidationFailed');
}
};

/**
* Handles errors by showing a notification and sending telemetry data.
* @param error - The error to handle.
* @param telemetryEvent - The telemetry event name.
*/
private handleError = async (error: any, telemetryEvent: string): Promise<void> => {
const telemetryService = await getTelemetryService();
const errorMessage = error instanceof Error ? error.message : String(error);
notificationService.showErrorMessage(`${nls.localize('check_openapi_doc_failed')}: ${errorMessage}`);
telemetryService.sendException(telemetryEvent, errorMessage);
};

private isFilePathEligible = (fullPath: string): boolean => {
// check if yaml or xml, else return false
if (!(fullPath.endsWith('.yaml') || fullPath.endsWith('.externalServiceRegistration-meta.xml'))) {
return false;
}

// if xml, check registrationProviderType to be ApexRest
if (fullPath.endsWith('.xml')) {
const xmlContent = fs.readFileSync(fullPath, 'utf8');
const parser = new XMLParser();
const jsonObj = parser.parse(xmlContent);
const registrationProviderType = jsonObj.ExternalServiceRegistration?.registrationProviderType;
if (registrationProviderType === 'Custom' || registrationProviderType === 'ApexRest') {
return true;
}
}

// if yaml, find the associated xml and look for registrationProviderType to be ApexRest
if (fullPath.endsWith('.yaml')) {
// check folder in which the file is present
const className = path.basename(fullPath).split('.')[0];
const dirName = path.dirname(fullPath);
const associatedXmlFileName = `${className}.externalServiceRegistration-meta.xml`;

const xmlContent = fs.readFileSync(path.join(dirName, associatedXmlFileName), 'utf8');
const parser = new XMLParser();
const jsonObj = parser.parse(xmlContent);
const registrationProviderType = jsonObj.ExternalServiceRegistration?.registrationProviderType;
if (registrationProviderType === 'Custom' || registrationProviderType === 'ApexRest') {
return true;
}
}

return false;
};
}

export const validateOpenApiDocument = async (sourceUri: vscode.Uri | vscode.Uri[]): Promise<void> => {
const oasDocumentChecker = OasDocumentChecker.Instance;
await oasDocumentChecker.validateOasDocument(sourceUri);
};
71 changes: 71 additions & 0 deletions packages/salesforcedx-vscode-apex/src/commands/oasUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2025, salesforce.com, inc.
* All rights reserved.
* 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 { SfProject } from '@salesforce/core-bundle';
import { workspaceUtils } from '@salesforce/salesforcedx-utils-vscode';
import * as vscode from 'vscode';
import { parse } from 'yaml';
import { nls } from '../messages';
import OasProcessor from '../oas/documentProcessorPipeline';
import { ProcessorInputOutput } from '../oas/documentProcessorPipeline/processorStep';
import { ApexClassOASGatherContextResponse, ApexClassOASEligibleResponse } from '../oas/schemas';

export const processOasDocument = async (
oasDoc: string,
context?: ApexClassOASGatherContextResponse,
eligibleResult?: ApexClassOASEligibleResponse,
isRevalidation?: boolean
): Promise<ProcessorInputOutput> => {
if (isRevalidation || context?.classDetail.annotations.find(a => a.name === 'RestResource')) {
const parsed = parse(oasDoc);
const oasProcessor = new OasProcessor(parsed, eligibleResult);

const processResult = await oasProcessor.process();

return processResult;
}
throw nls.localize('invalid_file_for_generating_oas_doc');
};

export const createProblemTabEntriesForOasDocument = (
fullPath: string,
processedOasResult: ProcessorInputOutput,
isESRDecomposed: boolean
) => {
const uri = vscode.Uri.parse(fullPath);
OasProcessor.diagnosticCollection.clear();

const adjustErrors = processedOasResult.errors.map(result => {
// if embedded inside of ESR.xml then position is hardcoded because of `apexActionController.createESRObject`
const lineAdjustment = isESRDecomposed ? 0 : 4;
const startCharacterAdjustment = isESRDecomposed ? 0 : 11;
const range = new vscode.Range(
result.range.start.line + lineAdjustment,
result.range.start.character + result.range.start.line <= 1 ? startCharacterAdjustment : 0,
result.range.end.line + lineAdjustment,
result.range.end.character + result.range.start.line <= 1 ? startCharacterAdjustment : 0
);

return new vscode.Diagnostic(range, result.message, result.severity);
});
OasProcessor.diagnosticCollection.set(uri, adjustErrors);
};

/**
* Reads sfdx-project.json and checks if decomposeExternalServiceRegistrationBeta is enabled.
* @returns boolean - true if sfdx-project.json contains decomposeExternalServiceRegistrationBeta
*/
export const checkIfESRIsDecomposed = async (): Promise<boolean> => {
const projectPath = workspaceUtils.getRootWorkspacePath();
const sfProject = await SfProject.resolve(projectPath);
const sfdxProjectJson = sfProject.getSfProjectJson();
if (sfdxProjectJson.getContents().sourceBehaviorOptions?.includes('decomposeExternalServiceRegistrationBeta')) {
return true;
}

return false;
};
Loading

0 comments on commit ce0b687

Please sign in to comment.