Skip to content

Commit

Permalink
Add Ballerina language support (#3584)
Browse files Browse the repository at this point in the history
* Add Ballerina language support

* Remove project subpath

* Add review suggestions

* Add debugger related changes

* Add minor review comments

* Add ballerina tests

* Add ballerina to test workflows

* Add ballerina related changes

* Add ballerina script type

* Fix lint issues

* Migrate from bal init to bal new
  • Loading branch information
xlight05 authored Jul 14, 2023
1 parent fb67f66 commit cd2e632
Show file tree
Hide file tree
Showing 38 changed files with 830 additions and 22 deletions.
25 changes: 24 additions & 1 deletion .azure-pipelines/common/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ steps:
displayName: 'Start X Virtual Frame Buffer'
condition: eq(variables['Agent.OS'], 'Linux')

# Tempoary workaround for https://github.com/ballerina-platform/ballerina-distribution/issues/4537
- script: |
curl -o ballerina.deb 'https://dist.ballerina.io/downloads/2201.6.0/ballerina-2201.6.0-swan-lake-linux-x64.deb'
sudo dpkg -i ballerina.deb
rm -f ballerina.deb
displayName: Install Ballerina(Linux)
condition: eq(variables['Agent.OS'], 'Linux')

- script: |
curl -o ballerina.pkg 'https://dist.ballerina.io/downloads/2201.6.0/ballerina-2201.6.0-swan-lake-macos-x64.pkg'
sudo installer -pkg ballerina.pkg -target /
rm -f ballerina.pkg
echo '##vso[task.prependpath]/Library/Ballerina/bin'
displayName: Install Ballerina(MacOS)
condition: eq(variables['Agent.OS'], 'Darwin')

- script: |
curl -o ballerina.msi https://dist.ballerina.io/downloads/2201.6.0/ballerina-2201.6.0-swan-lake-windows-x64.msi
msiexec /i ballerina.msi /quiet /qr /L*V "C:\Temp\msilog.log"
del ballerina.msi
echo "##vso[task.setvariable variable=PATH]C:\Program Files\Ballerina\bin;$(PATH)"
displayName: Install Ballerina(Windows)
condition: eq(variables['Agent.OS'], 'Windows_NT')

- task: UsePythonVersion@0
displayName: 'Use Python 3.7.x'
inputs:
Expand Down Expand Up @@ -46,4 +70,3 @@ steps:
testResultsFiles: '*-results.xml'
testRunTitle: '$(Agent.OS)'
condition: succeededOrFailed()

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,7 @@
"PowerShell",
"Python",
"TypeScript",
"Ballerina",
"Custom"
],
"description": "%azureFunctions.projectLanguage%",
Expand All @@ -825,6 +826,7 @@
"",
"",
"",
"",
""
]
},
Expand Down
1 change: 1 addition & 0 deletions resources/backupTemplates/ballerina/bindings/bindings.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"en":{"temp_category_api":"API & Webhooks","temp_category_core":"Core","temp_category_dataProcessing":"Data Processing","HttpTrigger_description":"A function that will be run whenever it receives an HTTP request, responding based on data in the body or query string","BlobTrigger_description":"A function that will be run whenever a blob is added to a specified container","CosmosDBTrigger_description":"A function that will be run whenever documents change in a document collection","QueueTrigger_description":"A function that will be run whenever a message is added to a specified Azure Storage queue","TimerTrigger_description":"A function that will be run on a specified schedule","variables_storageConnStringLabel":"Storage account connection","variables_appSettingsHelp":"The name of the app setting containing your storage account connection string.","variables_selectConnection":"Click select to choose a connection","variables_parameterName":"The parameter name must be an alphanumeric string of any number of characters and cannot start with a number.","timerTrigger_displayName":"Timer","timerTrigger_name_help":"The name used to identify this trigger in your code","timerTrigger_name_label":"Timestamp parameter name","timerTrigger_schedule_help":"Enter a cron expression of the format '{second} {minute} {hour} {day} {month} {day of week}' to specify the schedule.","timerTrigger_schedule_label":"Schedule","queueTrigger_displayName":"Azure Queue Storage","queueTrigger_queueName_help":"Name of the queue from which the message will be read","queueTrigger_name_label":"Message parameter name","queueTrigger_queueName_errorText":"Queue name must start and end with a letter or number, and it can contain only lowercase letters, numbers, and the hyphen. The name must be 3 to 63 characters.","queueTrigger_name_help":"The name used to identify this trigger in your code","queueTrigger_queueName_label":"Queue name","blobTrigger_displayName":"Azure Blob Storage","blobTrigger_name_label":"Blob parameter name","blobTrigger_name_help":"The name used to identify this trigger in your code","blobTrigger_path_label":"Path","blobTrigger_path_help":"The path within your storage account that the trigger will monitor.","blobTrigger_path_errorText":"Your blob path must be a container name of 3 to 63 characters, followed by a blob name of 1 to 1,024 characters. The blob name can be any combination of characters and can contain a maximum of 254 path segments, which are separated by the forward slash (/).","httpTrigger_displayName":"HTTP","httpTrigger_name_label":"Request parameter name","httpTrigger_name_help":"The name used to identify this trigger in your code","httpTrigger_authLevel_label":"Authorization level","httpTrigger_authLevel_help":"Authorization level controls whether the function requires an API key and which key to use; Function uses a function key; Admin uses your master key. The function and master keys are found in the 'keys' management panel on the portal, when your function is selected. For user-based authentication, go to Function App Settings.","httpTrigger_methods_label":"Selected HTTP methods","httpTrigger_methods_help":"These are the only methods to which this function will respond.","httpTrigger_methodRule_label":"Allowed HTTP methods","httpTrigger_route_label":"Route template","httpTrigger_route_help":"The route template setting allows you to change the URI that triggers this function. The values should be a relative path. Path segments may be treated as parameters by surrounding them with curly braces. For example: customer/{customerId}","httpTrigger_methodRule_help":"HttpTrigger can respond to any HTTP method. If you wish to restrict support to specific methods, choose the 'Selected methods' option.","httpTrigger_mode_label":"Mode","httpTrigger_mode_help":"The mode of the trigger. \"Standard\" means that the request will be standard HTTP with no additional semantics. \"Webhook\" means that the request will be processed according to a specified webhook type.","cosmosDB_trigger_displayName":"Azure Cosmos DB","cosmosDBIn_collectionName_help":"Name of the collection to be monitored.","cosmosDBIn_collectionName_label":"Collection name","cosmosDBIn_connection_help":"The name of the App Setting containing the connection string to the service that contains the collection to be monitored.","cosmosDBIn_connection_label":"Cosmos DB account connection","cosmosDBIn_databaseName_help":"Name of the Cosmos DB database that includes the collection to be monitored.","cosmosDBIn_databaseName_label":"Database name","cosmosDBIn_leaseCollectionName_help":"Name of the collection to store the leases.","cosmosDBIn_leaseCollectionName_label":"Collection name for leases","cosmosDBIn_leaseDatabaseName_help":"Name of the database that includes the collection to store the leases.","cosmosDBIn_leaseDatabaseName_label":"Database name for leases","cosmosDBIn_name_help":"The name used to identify this binding in your code","cosmosDBIn_name_label":"Document collection parameter name","cosmosDBIn_createIfNotExists_help":"Checks for existence and automatically creates the leases collection.","cosmosDBIn_createIfNotExists_label":"Create lease collection if it does not exist"}}
121 changes: 121 additions & 0 deletions resources/backupTemplates/ballerina/templates/templates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
[
{
"id": "HttpTrigger-Ballerina",
"runtime": "2",
"files": {
"%functionName%.bal": "import ballerinax/azure.functions;\n\n// This function gets triggered by an HTTP call with the name query parameter and returns a processed HTTP output to the caller.\n@functions:HttpTrigger{\n authLevel: \"%authLevel%\"\n}\nlistener functions:HttpListener httpListener = new functions:HttpListener();\nservice /%functionName% on httpListener {\n resource function get .(string name) returns string {\n return \"Hello, \" + name + \"!\";\n }\n}\n"
},
"metadata": {
"defaultFunctionName": "httpTrigger",
"description": "$HttpTrigger_description",
"name": "HTTP trigger",
"language": "Ballerina",
"triggerType": "httpTrigger",
"category": [
"$temp_category_core",
"$temp_category_api"
],
"categoryStyle": "http",
"enabledInTryMode": true,
"userPrompt": [
"authLevel"
]
}
},
{
"id": "BlobTrigger-Ballerina",
"runtime": "2",
"files": {
"%functionName%.bal": "import ballerinax/azure.functions;\nimport ballerina/log;\n\n// The following Function will be invoked when a new blob added to the specified blob storage.\n@functions:BlobTrigger {\n path: \"%path%\",\n connection: \"%connection%\"\n}\nlistener functions:BlobListener blobListener = new functions:BlobListener();\n\nservice \"%functionName%\" on blobListener {\n remote function onUpdate(byte[] blobIn) {\n log:printInfo(\"Blob Store updated with file of \" + blobIn.length().toString() + \" bytes\");\n }\n}\n"
},
"metadata": {
"defaultFunctionName": "blobTrigger",
"description": "$BlobTrigger_description",
"name": "Blob trigger",
"language": "Ballerina",
"triggerType": "blobTrigger",
"category": [
"$temp_category_core",
"$temp_category_dataProcessing"
],
"categoryStyle": "blob",
"enabledInTryMode": true,
"userPrompt": [
"connection",
"path"
]
}
},
{
"id": "CosmosDBTrigger-Ballerina",
"runtime": "2",
"files": {
"%functionName%.bal": "import ballerina/log;\nimport ballerinax/azure.functions;\n\n// The following Function will be invoked when an entry is added to CosmosDB collection.\n@functions:CosmosDBTrigger {connectionStringSetting: \"%connectionStringSetting%\", databaseName: \"%databaseName%\", collectionName: \"%collectionName%\"}\nlistener functions:CosmosDBListener cosmosEp = new ();\n\ntype Users record {\n string id;\n string name;\n};\n\nservice \"%functionName%\" on cosmosEp {\n remote function onUpdate(Users[] users) {\n log:printInfo(users.toJsonString());\n }\n}\n"
},
"metadata": {
"defaultFunctionName": "cosmosTrigger",
"description": "$CosmosDBTrigger_description",
"name": "CosmosDB trigger",
"language": "Ballerina",
"triggerType": "cosmosDBTrigger",
"category": [
"$temp_category_core",
"$temp_category_dataProcessing"
],
"categoryStyle": "cosmosDB",
"enabledInTryMode": true,
"userPrompt": [
"connectionStringSetting",
"databaseName",
"collectionName"
]
}
},
{
"id": "QueueTrigger-Ballerina",
"runtime": "2",
"files": {
"%functionName%.bal": "import ballerina/log;\nimport ballerinax/azure.functions;\n\n\n// The following Function will be executed when a message is added to the queue storage.\n@functions:QueueTrigger {\n queueName: \"%queueName%\",\n connection: \"%connection%\"\n}\nlistener functions:QueueListener queueListener = new functions:QueueListener();\n\nservice \"%functionName%\" on queueListener {\n remote function onMessage(string message) {\n log:printInfo(\"Queue message received: \" + message);\n }\n}\n"
},
"metadata": {
"defaultFunctionName": "queueTrigger",
"description": "$QueueTrigger_description",
"name": "Queue trigger",
"language": "Ballerina",
"triggerType": "queueTrigger",
"category": [
"$temp_category_core",
"$temp_category_dataProcessing"
],
"categoryStyle": "queue",
"enabledInTryMode": true,
"userPrompt": [
"connection",
"queueName"
]
}
},
{
"id": "TimerTrigger-Ballerina",
"runtime": "2",
"files": {
"%functionName%.bal": "import ballerina/time;\nimport ballerina/log;\nimport ballerinax/azure.functions;\n\n// The following function will be invoked periodically according to the schedule given.\n@functions:TimerTrigger {schedule: \"%schedule%\"}\nlistener functions:TimerListener timerEp = new ();\n\nservice \"%functionName%\" on timerEp {\n remote function onTrigger(functions:TimerMetadata metadata) {\n log:printInfo(\"Function Executed at \" + time:utcToString(time:utcNow()));\n }\n}\n"
},
"metadata": {
"defaultFunctionName": "timerTrigger",
"description": "$TimerTrigger_description",
"name": "Timer trigger",
"language": "Ballerina",
"triggerType": "timerTrigger",
"category": [
"$temp_category_core",
"$temp_category_dataProcessing"
],
"categoryStyle": "timer",
"enabledInTryMode": true,
"userPrompt": [
"schedule"
]
}
}
]
1 change: 1 addition & 0 deletions resources/backupTemplates/ballerina/version.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.0.0
2 changes: 2 additions & 0 deletions src/LocalResourceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ async function getCompiledProjectInfo(context: IActionContext, projectPath: stri
} else {
return { compiledProjectPath: path.join(projectPath, getJavaDebugSubpath(functionAppName, buildTool)), isIsolated: false };
}
} else if (projectLanguage === ProjectLanguage.Ballerina) {
return { compiledProjectPath: path.join(projectPath, "target", "azure_functions"), isIsolated: false };
} else {
return undefined;
}
Expand Down
8 changes: 8 additions & 0 deletions src/commands/createFunction/FunctionSubWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { IFunctionTemplate } from '../../templates/IFunctionTemplate';
import { isNodeV4Plus, isPythonV2Plus } from '../../utils/programmingModelUtils';
import { addBindingSettingSteps } from '../addBinding/settingSteps/addBindingSettingSteps';
import { JavaPackageNameStep } from '../createNewProject/javaSteps/JavaPackageNameStep';
import { BallerinaFunctionCreateStep } from './ballerinaSteps/BallerinaFunctionCreateStep';
import { BallerinaFunctionNameStep } from './ballerinaSteps/BallerinaFunctionNameStep';
import { DotnetFunctionCreateStep } from './dotnetSteps/DotnetFunctionCreateStep';
import { DotnetFunctionNameStep } from './dotnetSteps/DotnetFunctionNameStep';
import { DotnetNamespaceStep } from './dotnetSteps/DotnetNamespaceStep';
Expand Down Expand Up @@ -45,6 +47,9 @@ export class FunctionSubWizard {
case ProjectLanguage.Java:
promptSteps.push(new JavaPackageNameStep(), new JavaFunctionNameStep());
break;
case ProjectLanguage.Ballerina:
promptSteps.push(new BallerinaFunctionNameStep());
break;
case ProjectLanguage.CSharp:
case ProjectLanguage.FSharp:
promptSteps.push(new DotnetFunctionNameStep(), new DotnetNamespaceStep());
Expand Down Expand Up @@ -81,6 +86,9 @@ export class FunctionSubWizard {
case ProjectLanguage.TypeScript:
executeSteps.push(new TypeScriptFunctionCreateStep());
break;
case ProjectLanguage.Ballerina:
executeSteps.push(new BallerinaFunctionCreateStep());
break;
default:
if (isV2PythonModel) {
executeSteps.push(new PythonFunctionCreateStep());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzExtFsExtra, nonNullProp } from '@microsoft/vscode-azext-utils';
import * as path from 'path';
import { FunctionCreateStepBase } from '../FunctionCreateStepBase';
import { IBallerinaFunctionTemplate, IBallerinaFunctionWizardContext } from './IBallerinaFunctionWizardContext';

export class BallerinaFunctionCreateStep extends FunctionCreateStepBase<IBallerinaFunctionWizardContext> {
public async executeCore(context: IBallerinaFunctionWizardContext): Promise<string> {
const functionPath = context.projectPath;
await AzExtFsExtra.ensureDir(functionPath);

const functionName = nonNullProp(context, 'functionName');
const fileName = `${functionName}.bal`;

const template: IBallerinaFunctionTemplate = nonNullProp(context, 'functionTemplate');
await Promise.all(Object.keys(template.templateFiles).map(async f => {
let contents = template.templateFiles[f];
contents = contents.replace(/%functionName%/g, functionName);

for (const setting of template.userPromptedSettings) {
// the setting name keys are lowercased
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
contents = contents.replace(new RegExp(`%${setting.name}%`, 'g'), context[setting.name.toLowerCase()]);
}

await AzExtFsExtra.writeFile(path.join(functionPath, fileName), contents);
}));

return path.join(functionPath, fileName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzExtFsExtra } from '@microsoft/vscode-azext-utils';
import { localize } from "../../../localize";
import { IFunctionTemplate } from '../../../templates/IFunctionTemplate';
import { nonNullProp } from '../../../utils/nonNull';
import { getBallerinaFunctionFilePath, getBallerinaPackagePath, IBallerinaProjectWizardContext } from '../../createNewProject/ballerinaSteps/IBallerinaProjectWizardContext';
import { FunctionNameStepBase } from '../FunctionNameStepBase';
import { IFunctionWizardContext } from '../IFunctionWizardContext';

export class BallerinaFunctionNameStep extends FunctionNameStepBase<IFunctionWizardContext & IBallerinaProjectWizardContext> {
protected async getUniqueFunctionName(context: IFunctionWizardContext & IBallerinaProjectWizardContext): Promise<string | undefined> {
const template: IFunctionTemplate = nonNullProp(context, 'functionTemplate');
return await this.getUniqueFsPath(getBallerinaPackagePath(context.projectPath), template.defaultFunctionName, '.bal');
}

protected async validateFunctionNameCore(context: IFunctionWizardContext & IBallerinaProjectWizardContext, name: string): Promise<string | undefined> {
if (await AzExtFsExtra.pathExists(getBallerinaFunctionFilePath(context.projectPath, name))) {
return localize('existingError', 'A function with name "{0}" already exists in package "{1}".', name);
} else {
return undefined;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IFunctionTemplate } from '../../../templates/IFunctionTemplate';
import { IFunctionWizardContext } from '../IFunctionWizardContext';

export interface IBallerinaFunctionWizardContext extends IFunctionWizardContext {
functionTemplate?: IBallerinaFunctionTemplate;
}

export interface IBallerinaFunctionTemplate extends IFunctionTemplate {
templateFiles: { [filename: string]: string };
}
Loading

0 comments on commit cd2e632

Please sign in to comment.