Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add logic to rerun validations and show validations in correct file instead of virtual one. #6070

Merged
merged 1 commit into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/salesforcedx-vscode-apex/package.json
madhur310 marked this conversation as resolved.
Show resolved Hide resolved
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 =~ /.\\.externalServiceRegistration-meta\\.(xml)?$/ && !sf:is_esr_decomposed) || 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)?$/ && !sf:is_esr_decomposed) || 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';
139 changes: 139 additions & 0 deletions packages/salesforcedx-vscode-apex/src/commands/oasDocumentChecker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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;
if (!openApiDocument) {
throw nls.localize('no_oas_doc_in_file');
}
} 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the parameter is needed here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I don't pass in this, it would check the 'RestResource' and fail. We cannot remove that check as it guards which cls files can be used to create OAS docs.

): 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_processing_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
Loading