Skip to content

Commit

Permalink
Generate Azure Functions from API (#121)
Browse files Browse the repository at this point in the history
* Generate function from OpenAPI

* Small fixes

* Release 0.1.6

* Small fixes

* Samll fixes and changes
  • Loading branch information
RupengLiu authored Oct 12, 2020
1 parent bdd12c9 commit 7033cc3
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 9 deletions.
31 changes: 30 additions & 1 deletion package-lock.json

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

29 changes: 23 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "vscode-apimanagement",
"displayName": "Azure API Management",
"description": "An Azure API Management extension for Visual Studio Code.",
"version": "0.1.6",
"version": "0.1.7",
"publisher": "ms-azuretools",
"icon": "resources/azure-apim.png",
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
Expand Down Expand Up @@ -69,7 +69,8 @@
"onCommand:azureApiManagement.copyDockerRunCommand",
"onCommand:azureApiManagement.generateKubernetesDeployment",
"onCommand:azureApiManagement.generateNewGatewayToken",
"onCommand:azureApiManagement.debugPolicy"
"onCommand:azureApiManagement.debugPolicy",
"onCommand:azureApiManagement.generateFunctions"
],
"main": "main",
"contributes": {
Expand Down Expand Up @@ -334,6 +335,11 @@
"command": "azureApiManagement.generateNewGatewayToken",
"title": "%azureApiManagement.generateNewGatewayToken%",
"category": "Azure API Management"
},
{
"command": "azureApiManagement.generateFunctions",
"title": "%azureApiManagement.generateFunctions%",
"category": "Azure API Management"
}
],
"viewsContainers": {
Expand Down Expand Up @@ -459,20 +465,25 @@
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApi",
"group": "2@2"
},
{
"command": "azureApiManagement.generateFunctions",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApi && config.azureApiManagement.enableCreateFunctionApp == true",
"group": "3@1"
},
{
"command": "azureApiManagement.extractApi",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApi",
"group": "3@1"
"group": "4@1"
},
{
"command": "azureApiManagement.Refresh",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApi",
"group": "4@1"
"group": "5@1"
},
{
"command": "azureApiManagement.deleteApi",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApi",
"group": "5@1"
"group": "6@1"
},
{
"command": "azureApiManagement.Refresh",
Expand Down Expand Up @@ -569,6 +580,11 @@
"type": "boolean",
"default": false,
"description": "%azureApiManagement.advancedPolicyAuthoringExperience%"
},
"azureApiManagement.enableCreateFunctionApp": {
"type": "boolean",
"default": false,
"description": "%azureApiManagement.enableCreateFunctionApp%"
}
}
}
Expand Down Expand Up @@ -632,7 +648,8 @@
"vscode-debugadapter": "^1.39.1",
"vscode-debugprotocol": "^1.39.0",
"vscode-nls": "^4.1.2",
"ws": "^7.2.3"
"ws": "^7.2.3",
"xregexp": "^4.3.0"
},
"extensionDependencies": [
"ms-vscode.azure-account",
Expand Down
4 changes: 3 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@
"azureApiManagement.copyDockerRunCommand": "Copy Docker Run Command",
"azureApiManagement.generateKubernetesDeployment": "Generate Kubernetes Deployment File",
"azureApiManagement.generateNewGatewayToken": "Generate New Gateway Token",
"azureApiManagement.debugPolicy": "Start Policy Debugging"
"azureApiManagement.debugPolicy": "Start Policy Debugging",
"azureApiManagement.generateFunctions": "Generate Azure Functions",
"azureApiManagement.enableCreateFunctionApp": "Enables creating function app from API"
}
241 changes: 241 additions & 0 deletions src/commands/generateFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import * as fse from 'fs-extra';
import * as path from 'path';
import * as vscode from 'vscode';
import { OpenDialogOptions, ProgressLocation, Uri, window, workspace } from "vscode";
import { DialogResponses } from 'vscode-azureextensionui';
import XRegExp = require('xregexp');
import { ApiTreeItem } from "../explorer/ApiTreeItem";
import { OpenApiEditor } from "../explorer/editors/openApi/OpenApiEditor";
import { ext } from "../extensionVariables";
import { localize } from "../localize";
import { cpUtils } from "../utils/cpUtils";
import { openUrl } from '../utils/openUrl';

const formattingCharacter: string = '\\p{Cf}';
const connectingCharacter: string = '\\p{Pc}';
const decimalDigitCharacter: string = '\\p{Nd}';
const combiningCharacter: string = '\\p{Mn}|\\p{Mc}';
const letterCharacter: string = '\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}';
const identifierPartCharacter: string = `${letterCharacter}|${decimalDigitCharacter}|${connectingCharacter}|${combiningCharacter}|${formattingCharacter}`;
const identifierStartCharacter: string = `(${letterCharacter}|_)`;
const identifierOrKeyword: string = `${identifierStartCharacter}(${identifierPartCharacter})*`;
// tslint:disable-next-line: no-unsafe-any
const identifierRegex: RegExp = XRegExp(`^${identifierOrKeyword}$`);
// Keywords: https://github.com/dotnet/csharplang/blob/master/spec/lexical-structure.md#keywords
const keywords: string[] = ['abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch', 'char', 'checked', 'class', 'const', 'continue', 'decimal', 'default', 'delegate', 'do', 'double', 'else', 'enum', 'event', 'explicit', 'extern', 'false', 'finally', 'fixed', 'float', 'for', 'foreach', 'goto', 'if', 'implicit', 'in', 'int', 'interface', 'internal', 'is', 'lock', 'long', 'namespace', 'new', 'null', 'object', 'operator', 'out', 'override', 'params', 'private', 'protected', 'public', 'readonly', 'ref', 'return', 'sbyte', 'sealed', 'short', 'sizeof', 'stackalloc', 'static', 'string', 'struct', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong', 'unchecked', 'unsafe', 'ushort', 'using', 'virtual', 'void', 'volatile', 'while'];

// tslint:disable: no-non-null-assertion
export async function generateFunctions(node?: ApiTreeItem): Promise<void> {
if (!node) {
node = <ApiTreeItem>await ext.tree.showTreeItemPicker(ApiTreeItem.contextValue);
}

ext.outputChannel.show();

const openApiEditor: OpenApiEditor = new OpenApiEditor();
const openAPIDocString = await openApiEditor.getData(node);

if (!await validateAutorestInstalled()) {
return;
}

// tslint:disable-next-line: no-unsafe-any
const languages : string[] = Object.keys(languageTypes).map(key => languageTypes[key]);
const language = await ext.ui.showQuickPick(
languages.map((s) => {return { label: s, description: '', detail: '' }; }), { placeHolder: "Select language", canPickMany: false});

if (!await checkEnvironmentInstalled(language.label)) {
throw new Error(`You haven't installed '${language.label}' on your machine, please install '${language.label}' to continue.`);
}

let namespace = "";
if (language.label === languageTypes.Java) {
namespace = await askJavaNamespace();
} else if (language.label === languageTypes.CSharp) {
namespace = await askCSharpNamespace();
}

const uris = await askFolder();

const openAPIFilePath = path.join(uris[0].fsPath, `${node!.apiContract.name}.json`);

await window.withProgress(
{
location: ProgressLocation.Notification,
title: localize("openAPI", `Downloading OpenAPI Document for API '${node.apiContract.name}'...`),
cancellable: false
},
async () => {
await fse.writeFile(openAPIFilePath, openAPIDocString);
}
).then(async () => {
window.showInformationMessage(localize("openAPIDownloaded", `Downloaded OpenAPI Document for API '${node!.apiContract.name} successfully.`));
});

await window.withProgress(
{
location: ProgressLocation.Notification,
title: localize("generateFunctions", `Generating functions for API '${node.apiContract.name}'...`),
cancellable: false
},
async () => {
const args: string[] = [];
args.push(`--input-file:${openAPIFilePath} --version:3.0.6314`);
args.push(`--output-folder:${uris[0].fsPath}`);

switch (language.label) {
case languageTypes.TypeScript:
args.push('--azure-functions-typescript');
args.push('--no-namespace-folders:True');
break;
case languageTypes.CSharp:
args.push(`--namespace:${namespace}`);
args.push('--azure-functions-csharp');
break;
case languageTypes.Java:
args.push(`--namespace:${namespace}`);
args.push('--azure-functions-java');
break;
case languageTypes.Python:
args.push('--azure-functions-python');
args.push('--no-namespace-folders:True');
args.push('--no-async');
break;
default:
throw new Error(localize("notSupported", "Not a supported language. We currently support C#, Java, Python, and Typescript"));
}

ext.outputChannel.show();
await cpUtils.executeCommand(ext.outputChannel, undefined, 'autorest', ...args);
await promptOpenFileFolder(uris[0].fsPath);
}
).then(async () => {
window.showInformationMessage(localize("openAPIDownloaded", `Generated Azure Functions app for API '${node!.apiContract.name} successfully.`));
});
}

async function askFolder(): Promise<Uri[]> {
const openDialogOptions: OpenDialogOptions = {
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: "Functions Location",
filters: {
JSON: ["json"]
}
};
const rootPath = workspace.rootPath;
if (rootPath) {
openDialogOptions.defaultUri = Uri.file(rootPath);
}
return await ext.ui.showOpenDialog(openDialogOptions);
}

async function askJavaNamespace(): Promise<string> {
const namespacePrompt: string = localize('namespacePrompt', 'Enter Java namespace folder.');
const defaultName = "com.microsoft.azure.stencil";
return (await ext.ui.showInputBox({
prompt: namespacePrompt,
value: defaultName,
validateInput: async (value: string | undefined): Promise<string | undefined> => {
value = value ? value.trim() : '';
return undefined;
}
})).trim();
}

async function askCSharpNamespace(): Promise<string> {
const namespacePrompt: string = localize('namespacePrompt', 'Enter CSharp namespace folder.');
const defaultName = "Microsoft.Azure.Stencil";
return (await ext.ui.showInputBox({
prompt: namespacePrompt,
value: defaultName,
validateInput: validateCSharpNamespace
})).trim();
}

function validateCSharpNamespace(value: string | undefined): string | undefined {
if (!value) {
return localize('cSharpNamespacError', 'The CSharp namespace cannot be empty.');
}

const identifiers: string[] = value.split('.');
for (const identifier of identifiers) {
if (identifier === '') {
return localize('cSharpExtraPeriod', 'Leading or trailing "." character is not allowed.');
} else if (!identifierRegex.test(identifier)) {
return localize('cSharpInvalidCharacters', 'The identifier "{0}" contains invalid characters.', identifier);
} else if (keywords.find((s: string) => s === identifier.toLowerCase()) !== undefined) {
return localize('cSharpKeywordWarning', 'The identifier "{0}" is a reserved keyword.', identifier);
}
}

return undefined;
}

async function validateAutorestInstalled(): Promise<boolean> {
try {
await cpUtils.executeCommand(undefined, undefined, 'autorest', '--version');
} catch (error) {
const message: string = localize('autorestNotFound', 'Failed to find "autorest" | Extension needs AutoRest to generate a function app from an OpenAPI specification. Click "Learn more" for more details on installation steps.');
window.showErrorMessage(message, DialogResponses.learnMore).then(async result => {
if (result === DialogResponses.learnMore) {
await openUrl('https://aka.ms/autorest');
}
});

return false;
}

return true;
}

async function promptOpenFileFolder(filePath: string): Promise<void> {
const yes: vscode.MessageItem = { title: localize('open', 'Yes') };
const no: vscode.MessageItem = { title: localize('notOpen', 'No') };
const message: string = localize('openFolder', 'Do you want to open the folder for the generated files?');
window.showInformationMessage(message, yes, no).then(async result => {
if (result === yes) {
vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(filePath), true);
}
});
}

async function checkEnvironmentInstalled(language: string): Promise<boolean> {
let command = "";
switch (language) {
case languageTypes.CSharp: {
command = "dotnet -h";
break;
}
case languageTypes.Java: {
command = "java -version";
break;
}
case languageTypes.Python: {
command = "python --version";
break;
}
case languageTypes.TypeScript: {
command = "tsc --version";
break;
}
default: {
throw new Error("Invalid Language Type.");
}
}

try {
await cpUtils.executeCommand(undefined, undefined, command);
return true;
} catch (error) {
return false;
}
}

enum languageTypes {
Python = 'Python',
CSharp = 'CSharp',
TypeScript = 'TypeScript',
Java = 'Java'
}
Loading

0 comments on commit 7033cc3

Please sign in to comment.