diff --git a/package-lock.json b/package-lock.json index 380c6f9..4893a58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-apimanagement", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -30,6 +30,22 @@ "js-tokens": "^4.0.0" } }, + "@babel/runtime-corejs3": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz", + "integrity": "sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A==", + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + } + } + }, "@types/bluebird": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.30.tgz", @@ -1840,6 +1856,11 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" }, + "core-js-pure": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", + "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -9549,6 +9570,14 @@ "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", "integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==" }, + "xregexp": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", + "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", + "requires": { + "@babel/runtime-corejs3": "^7.8.3" + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index 90cac03..f21ed34 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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": { @@ -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": { @@ -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", @@ -569,6 +580,11 @@ "type": "boolean", "default": false, "description": "%azureApiManagement.advancedPolicyAuthoringExperience%" + }, + "azureApiManagement.enableCreateFunctionApp": { + "type": "boolean", + "default": false, + "description": "%azureApiManagement.enableCreateFunctionApp%" } } } @@ -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", diff --git a/package.nls.json b/package.nls.json index b3f3d6a..24e7467 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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" } \ No newline at end of file diff --git a/src/commands/generateFunctions.ts b/src/commands/generateFunctions.ts new file mode 100644 index 0000000..f15de18 --- /dev/null +++ b/src/commands/generateFunctions.ts @@ -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 { + if (!node) { + node = 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 { + 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 { + 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 => { + value = value ? value.trim() : ''; + return undefined; + } + })).trim(); +} + +async function askCSharpNamespace(): Promise { + 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 { + 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 { + 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 { + 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' +} diff --git a/src/explorer/ApiManagementProvider.ts b/src/explorer/ApiManagementProvider.ts index 6a64d19..9b46085 100644 --- a/src/explorer/ApiManagementProvider.ts +++ b/src/explorer/ApiManagementProvider.ts @@ -82,7 +82,7 @@ export class ApiManagementProvider extends SubscriptionTreeItem { wizardContext.email = this.root.userId; const advancedCreationKey: string = 'advancedCreation'; -// tslint:disable-next-line: strict-boolean-expressions + // tslint:disable-next-line: strict-boolean-expressions const advancedCreation: boolean = !!getWorkspaceSetting(advancedCreationKey); actionContext.properties.advancedCreation = String(advancedCreation); if (!advancedCreation) { diff --git a/src/extension.ts b/src/extension.ts index 00558f9..69853ff 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ import { debugPolicy } from './commands/debugPolicies/debugPolicy'; import { deleteNode } from './commands/deleteNode'; import { copyDockerRunCommand, generateKubernetesDeployment } from './commands/deployGateway'; import { extractAPI, extractService } from './commands/extract'; +import { generateFunctions } from './commands/generateFunctions'; import { generateNewGatewayToken } from './commands/generateNewGatewayToken'; import { importFunctionApp } from './commands/importFunctionApp/importFunctionApp'; import { importFunctionAppToApi } from './commands/importFunctionApp/importFunctionApp'; @@ -106,6 +107,8 @@ export function activateInternal(context: vscode.ExtensionContext) { registerCommand('azureApiManagement.openExtensionWorkspaceFolder', openWorkingFolder); registerCommand('azureApiManagement.initializeExtensionWorkspaceFolder', setupWorkingFolder); + registerCommand('azureApiManagement.generateFunctions', async (node: ApiTreeItem) => await generateFunctions(node)); + registerEditors(context); activate(context); // activeta debug context