Skip to content

Commit

Permalink
feat(vscode): Introduce creation of rules code project (#4920)
Browse files Browse the repository at this point in the history
* Add rules engine option in placeholder and add files

* Add decision making to load correct project type files

* Update project structure

* Update rules files

* Copy rules files when creating project

* Update rules code files

* Add logic to copy cs file and replace instances of method name in xml file

* Move rule set file step

* Update dropdown placeholder and functions file

* Add rules code workflow template and create reusable functions

* Update properties for workflow definition and start unit testing

* Add more unit tests

* Add vscode extension to coverage report in PR

---------

Co-authored-by: Travis Harris <hartra344@users.noreply.github.com>
  • Loading branch information
ccastrotrejo and hartra344 committed Jun 5, 2024
1 parent cdfa7ae commit 916c529
Show file tree
Hide file tree
Showing 24 changed files with 811 additions and 232 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/coverage-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ jobs:
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]
- run: pnpm turbo run test:lib --cache-dir=.turbo
- name: Run lib unit tests
run: pnpm turbo run test:lib --cache-dir=.turbo
- name: Run extension unit tests
run: pnpm turbo run test:extension-unit --cache-dir=.turbo
- name: Create code coverage report
run: |
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"libs/*/coverage/cobertura-coverage.xml" -targetdir:CodeCoverage -reporttypes:'Cobertura;MarkdownSummaryGithub'
reportgenerator -reports:"libs/*/coverage/cobertura-coverage.xml;apps/vs-code-designer/coverage/cobertura-coverage.xml" -targetdir:CodeCoverage -reporttypes:'Cobertura;MarkdownSummaryGithub'
- name: Generate Coverage Report
uses: clearlyip/code-coverage-report-action@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../../../constants';
import { localize } from '../../../../localize';
import { setLocalAppSetting } from '../../../utils/appSettings/localSettings';
import { getCodelessWorkflowTemplate } from '../../../utils/codeless/templates';
import {
addFolderToBuildPath,
addNugetPackagesToBuildFile,
Expand Down Expand Up @@ -48,29 +49,7 @@ export class CodelessWorkflowCreateStep extends WorkflowCreateStepBase<IFunction
const template: IWorkflowTemplate = nonNullProp(context, 'functionTemplate');
const functionPath: string = path.join(context.projectPath, nonNullProp(context, 'functionName'));

const emptyStatefulDefinition: StandardApp = {
definition: {
$schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#',
actions: {},
contentVersion: '1.0.0.0',
outputs: {},
triggers: {},
},
kind: 'Stateful',
};

const emptyStatelessDefinition: StandardApp = {
definition: {
$schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#',
actions: {},
contentVersion: '1.0.0.0',
outputs: {},
triggers: {},
},
kind: 'Stateless',
};

const codelessDefinition: StandardApp = template?.id === workflowType.stateful ? emptyStatefulDefinition : emptyStatelessDefinition;
const codelessDefinition: StandardApp = getCodelessWorkflowTemplate(template?.id === workflowType.stateful);

const workflowJsonFullPath: string = path.join(functionPath, workflowFileName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ export class NewCodeProjectTypeStep extends AzureWizardPromptStep<IProjectWizard
context.language = ProjectLanguage.JavaScript;

// Create directories based on user choices
const { workspacePath, isCustomCodeLogicApp } = context;
await this.createDirectories(context, workspacePath, isCustomCodeLogicApp);
const { workspacePath, isWorkspaceWithFunctions } = context;
await this.createDirectories(context, workspacePath, isWorkspaceWithFunctions);
}

/**
* Checks if this step should prompt the user
* @param context - Project wizard context containing user selections and settings
* @returns True if user should be prompted, otherwise false
*/
public shouldPrompt(_context: IProjectWizardContext): boolean {
public shouldPrompt(): boolean {
return true;
}

Expand All @@ -65,8 +65,8 @@ export class NewCodeProjectTypeStep extends AzureWizardPromptStep<IProjectWizard
const promptSteps: AzureWizardPromptStep<IProjectWizardContext>[] = [];
const executeSteps: AzureWizardExecuteStep<IProjectWizardContext>[] = [];

if (context.isCustomCodeLogicApp) {
await this.setupCustomCodeLogicApp(context, executeSteps, promptSteps);
if (context.isWorkspaceWithFunctions) {
await this.setupCustomLogicApp(context, executeSteps, promptSteps);
} else {
await this.setupRegularLogicApp(context, executeSteps, promptSteps);
}
Expand All @@ -78,14 +78,14 @@ export class NewCodeProjectTypeStep extends AzureWizardPromptStep<IProjectWizard
* Creates required directories for the project
* @param context - Project wizard context
* @param workspacePath - Root path of the workspace
* @param isCustomCodeLogicApp - Flag to check if it's a custom code Logic App
* @param isWorkspaceWithFunctions - Flag to check if it's a workspace with functions
*/
private async createDirectories(context: IProjectWizardContext, workspacePath: string, isCustomCodeLogicApp: boolean): Promise<void> {
private async createDirectories(context: IProjectWizardContext, workspacePath: string, isWorkspaceWithFunctions: boolean): Promise<void> {
await fs.ensureDir(workspacePath);
context.customWorkspaceFolderPath = workspacePath;

let logicAppFolderName = 'LogicApp';
if (!isCustomCodeLogicApp && context.isCustomCodeLogicApp !== null && context.logicAppName) {
if (!isWorkspaceWithFunctions && context.logicAppName) {
logicAppFolderName = context.logicAppName;
}

Expand All @@ -96,7 +96,7 @@ export class NewCodeProjectTypeStep extends AzureWizardPromptStep<IProjectWizard
context.projectPath = logicAppFolderPath;
context.workspacePath = logicAppFolderPath;

if (isCustomCodeLogicApp) {
if (isWorkspaceWithFunctions) {
await this.setupCustomDirectories(context, workspacePath);
}
await this.createWorkspaceFile(context);
Expand All @@ -118,7 +118,7 @@ export class NewCodeProjectTypeStep extends AzureWizardPromptStep<IProjectWizard
* @param executeSteps - List of steps to execute
* @param promptSteps - List of steps to prompt
*/
private async setupCustomCodeLogicApp(
private async setupCustomLogicApp(
context: IProjectWizardContext,
executeSteps: AzureWizardExecuteStep<IProjectWizardContext>[],
promptSteps: AzureWizardPromptStep<IProjectWizardContext>[]
Expand Down Expand Up @@ -171,7 +171,7 @@ export class NewCodeProjectTypeStep extends AzureWizardPromptStep<IProjectWizard
const workspaceFolders = [];

// Add Functions folder first if it's a custom code code Logic App
if (context.isCustomCodeLogicApp) {
if (context.isWorkspaceWithFunctions) {
workspaceFolders.push({ name: 'Functions', path: './Function' });
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { ext } from '../../../../extensionVariables';
import { localize } from '../../../../localize';
import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils';
import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps';
import { ProjectType, type IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps';

export class SetLogicAppName extends AzureWizardPromptStep<IProjectWizardContext> {
public async prompt(context: IProjectWizardContext): Promise<void> {
Expand All @@ -25,6 +25,6 @@ export class SetLogicAppName extends AzureWizardPromptStep<IProjectWizardContext
}

public shouldPrompt(context: IProjectWizardContext): boolean {
return !context.isCustomCodeLogicApp && context.isCustomCodeLogicApp !== null;
return context.projectType === ProjectType.logicApp;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@
import { localize } from '../../../../localize';
import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils';
import type { IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps';
import { ProjectType, type IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps';

export class SetLogicAppType extends AzureWizardPromptStep<IProjectWizardContext> {
public async prompt(context: IProjectWizardContext): Promise<void> {
const picks: IAzureQuickPickItem<boolean>[] = [
{ label: localize('logicApp', 'Logic app'), data: false },
{ label: localize('logicAppCustomCode', 'Logic app with custom code project'), data: true },
const picks: IAzureQuickPickItem<ProjectType>[] = [
{ label: localize('logicApp', 'Logic app'), data: ProjectType.logicApp },
{ label: localize('logicAppCustomCode', 'Logic app with custom code project'), data: ProjectType.customCode },
{ label: localize('logicAppRulesEngine', 'Logic app with rules engine project (preview)'), data: ProjectType.rulesEngine },
];

const placeHolder = localize('logicAppProjectTemplatePlaceHolder', 'Select a project template for your logic app workspace');
context.isCustomCodeLogicApp = (await context.ui.showQuickPick(picks, { placeHolder })).data;
context.projectType = (await context.ui.showQuickPick(picks, { placeHolder })).data;
context.isWorkspaceWithFunctions = context.projectType !== ProjectType.logicApp;
}

public shouldPrompt(context: IProjectWizardContext): boolean {
return context.isCustomCodeLogicApp === undefined;
return context.projectType === undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { FunctionConfigFile } from './FunctionConfigFile';
import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils';
import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps';
import { ProjectType } from '@microsoft/vscode-extension-logic-apps';
import * as fs from 'fs-extra';
import * as path from 'path';

Expand All @@ -15,6 +16,21 @@ export class InvokeFunctionProjectSetup extends AzureWizardPromptStep<IProjectWi
// Hide the step count in the wizard UI
public hideStepCount = true;

private csFileName = {
[ProjectType.customCode]: 'FunctionsFile',
[ProjectType.rulesEngine]: 'RulesFunctionsFile',
};

private templateFileName = {
[ProjectType.customCode]: 'FunctionsProj',
[ProjectType.rulesEngine]: 'RulesFunctionsProj',
};

private templateFolderName = {
[ProjectType.customCode]: 'FunctionProjectTemplate',
[ProjectType.rulesEngine]: 'RuleSetProjectTemplate',
};

/**
* Prompts the user to set up an Azure Function project.
* @param context The project wizard context.
Expand All @@ -27,34 +43,40 @@ export class InvokeFunctionProjectSetup extends AzureWizardPromptStep<IProjectWi
// Define the functions folder path using the context property of the wizard
const functionFolderPath = context.functionFolderPath;

// Define the type of project in the workspace
const projectType = context.projectType;

// Create the .cs file inside the functions folder
await this.createCsFile(functionFolderPath, methodName, namespace);
await this.createCsFile(functionFolderPath, methodName, namespace, projectType);

// Create the .cs files inside the functions folders for rule code projects
await this.createRulesFiles(functionFolderPath, projectType);

// Create the .csproj file inside the functions folder
await this.createCsprojFile(functionFolderPath, methodName);
await this.createCsprojFile(functionFolderPath, methodName, projectType);

// Generate the Visual Studio Code configuration files in the specified folder.
const createConfigFiles = new FunctionConfigFile();
await createConfigFiles.prompt(context);
}

/**
* Determines whether the user should be prompted to set up an Azure Function project.
* @param context The project wizard context.
* @returns True if the user has not yet set up an Azure Function project, false otherwise.
* Determines whether the prompt should be displayed.
* @returns {boolean} True if the prompt should be displayed, false otherwise.
*/
public shouldPrompt(_context: IProjectWizardContext): boolean {
public shouldPrompt(): boolean {
return true;
}

/**
* Creates the .cs file inside the functions folder.
* @param functionFolderPath The path to the functions folder.
* @param methodName The name of the method.
* @param namespace The name of the namespace.
* @param functionFolderPath - The path to the functions folder.
* @param methodName - The name of the method.
* @param namespace - The name of the namespace.
* @param projectType - The workspace projet type.
*/
private async createCsFile(functionFolderPath: string, methodName: string, namespace: string): Promise<void> {
const templatePath = path.join(__dirname, 'assets', 'FunctionProjectTemplate', 'FunctionsFile');
private async createCsFile(functionFolderPath: string, methodName: string, namespace: string, projectType: ProjectType): Promise<void> {
const templatePath = path.join(__dirname, 'assets', this.templateFolderName[projectType], this.csFileName[projectType]);
const templateContent = await fs.readFile(templatePath, 'utf-8');

const csFilePath = path.join(functionFolderPath, `${methodName}.cs`);
Expand All @@ -65,15 +87,30 @@ export class InvokeFunctionProjectSetup extends AzureWizardPromptStep<IProjectWi

/**
* Creates a .csproj file for a specific Azure Function.
* @param functionFolderPath The path to the folder where the .csproj file will be created.
* @param methodName The name of the Azure Function.
* @param functionFolderPath - The path to the folder where the .csproj file will be created.
* @param methodName - The name of the Azure Function.
* @param projectType - The workspace projet type.
*/
private async createCsprojFile(functionFolderPath: string, methodName: string): Promise<void> {
const templatePath = path.join(__dirname, 'assets', 'FunctionProjectTemplate', 'FunctionsProj');
private async createCsprojFile(functionFolderPath: string, methodName: string, projectType: ProjectType): Promise<void> {
const templatePath = path.join(__dirname, 'assets', this.templateFolderName[projectType], this.templateFileName[projectType]);
const templateContent = await fs.readFile(templatePath, 'utf-8');

const csprojFilePath = path.join(functionFolderPath, `${methodName}.csproj`);
const csprojFileContent = templateContent.replace(/<%= methodName %>/g, methodName);
await fs.writeFile(csprojFilePath, csprojFileContent);
}

/**
* Creates the rules files for the project.
* @param {string} functionFolderPath - The path of the function folder.
* @param {string} projectType - The type of the project.
* @returns A promise that resolves when the rules files are created.
*/
private async createRulesFiles(functionFolderPath: string, projectType: ProjectType): Promise<void> {
if (projectType === ProjectType.rulesEngine) {
const csTemplatePath = path.join(__dirname, 'assets', 'RuleSetProjectTemplate', 'ContosoPurchase');
const csRuleSetPath = path.join(functionFolderPath, 'ContosoPurchase.cs');
await fs.copyFile(csTemplatePath, csRuleSetPath);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as fse from 'fs-extra';
import * as path from 'path';
import type { MessageItem } from 'vscode';
import { validateDotNetIsInstalled } from '../../../dotnet/validateDotNetInstalled';
import { getFunctionWorkflowTemplate } from '../../../../utils/codeless/templates';

// This class creates a new workflow for a codeless Azure Function project
export class CodelessFunctionWorkflow extends WorkflowCreateStepBase<IFunctionWizardContext> {
Expand All @@ -59,89 +60,12 @@ export class CodelessFunctionWorkflow extends WorkflowCreateStepBase<IFunctionWi
const functionPath: string = path.join(context.projectPath, nonNullProp(context, 'functionName'));
const methodName = context.methodName;

// Create empty stateful and stateless definition objects
const emptyStatefulDefinition: StandardApp = {
definition: {
$schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#',
actions: {
Call_a_local_function_in_this_logic_app: {
type: 'InvokeFunction',
inputs: {
functionName: `${methodName}`,
parameters: {
zipCode: 85396,
temperatureScale: 'Celsius',
},
},
runAfter: {},
},
Response: {
type: 'Response',
kind: 'http',
inputs: {
statusCode: 200,
body: "@body('Call_a_local_function_in_this_logic_app')",
},
runAfter: {
Call_a_local_function_in_this_logic_app: ['Succeeded'],
},
},
},
triggers: {
When_a_HTTP_request_is_received: {
type: 'Request',
kind: 'Http',
inputs: {},
},
},
contentVersion: '1.0.0.0',
outputs: {},
},
kind: 'Stateful',
};

const emptyStatelessDefinition: StandardApp = {
definition: {
$schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#',
actions: {
Call_a_local_function_in_this_logic_app: {
type: 'InvokeFunction',
inputs: {
functionName: `${methodName}`,
parameters: {
zipCode: 85396,
temperatureScale: 'Celsius',
},
},
runAfter: {},
},
Response: {
type: 'Response',
kind: 'http',
inputs: {
statusCode: 200,
body: "@body('Call_a_local_function_in_this_logic_app')",
},
runAfter: {
Call_a_local_function_in_this_logic_app: ['Succeeded'],
},
},
},
triggers: {
When_a_HTTP_request_is_received: {
type: 'Request',
kind: 'Http',
inputs: {},
},
},
contentVersion: '1.0.0.0',
outputs: {},
},
kind: 'Stateless',
};

// Determine which definition object to use based on the type of workflow template
const codelessDefinition: StandardApp = template?.id === workflowType.stateful ? emptyStatefulDefinition : emptyStatelessDefinition;
const codelessDefinition: StandardApp = getFunctionWorkflowTemplate(
methodName,
template?.id === workflowType.stateful,
context.projectType
);

// Write the workflow definition to a JSON file
const workflowJsonFullPath: string = path.join(functionPath, workflowFileName);
Expand Down
Loading

0 comments on commit 916c529

Please sign in to comment.