diff --git a/package.json b/package.json index 3e4d495d3..e3a36b0b5 100644 --- a/package.json +++ b/package.json @@ -854,6 +854,7 @@ "PowerShell", "Python", "TypeScript", + "Ballerina", "Custom" ], "description": "%azureFunctions.projectLanguage%", @@ -867,6 +868,7 @@ "", "", "", + "", "" ] }, diff --git a/resources/backupTemplates/ballerina/bindings/bindings.json b/resources/backupTemplates/ballerina/bindings/bindings.json new file mode 100644 index 000000000..4e668a202 --- /dev/null +++ b/resources/backupTemplates/ballerina/bindings/bindings.json @@ -0,0 +1 @@ +{"$schema":"","contentVersion":"2016-03-04-alpha","variables":{"storageConnStringLabel":"$variables_storageConnStringLabel","appSettingsHelp":"$variables_appSettingsHelp","selectConnection":"$variables_selectConnection","parameterName":"$variables_parameterName"},"bindings":[{"type":"timerTrigger","displayName":"$timerTrigger_displayName","direction":"trigger","enabledInTryMode":true,"documentation":"## Settings for timer trigger\r\n\r\nThe settings provide a schedule expression. For example, the following schedule runs the function every minute:\r\n\r\n - `schedule`: Cron tab expression which defines schedule. The cron expression are evaluated against UTC time zone.\r\n - `name`: The variable name used in function code for the TimerTrigger. \r\n - `type`: must be *timerTrigger*\r\n - `direction`: must be *in*\r\n\r\nThe timer trigger handles multi-instance scale-out automatically: only a single instance of a particular timer function will be running across all instances.\r\n\r\n## Format of schedule expression\r\n\r\nThe schedule expression is a [CRON expression](http://en.wikipedia.org/wiki/Cron#CRON_expression) that includes 6 fields: `{second} {minute} {hour} {day} {month} {day of the week}`. \r\n\r\nNote that many of the cron expressions you find online omit the {second} field, so if you copy from one of those you'll have to adjust for the extra field. \r\n\r\nHere are some other schedule expression examples:\r\n\r\nTo trigger once every 5 minutes:\r\n\r\n```json\r\n\"schedule\": \"0 */5 * * * *\"\r\n```\r\n\r\nTo trigger once at the top of every hour:\r\n\r\n```json\r\n\"schedule\": \"0 0 * * * *\",\r\n```\r\n\r\nTo trigger once every two hours:\r\n\r\n```json\r\n\"schedule\": \"0 0 */2 * * *\",\r\n```\r\n\r\nTo trigger once every hour from 9 AM to 5 PM:\r\n\r\n```json\r\n\"schedule\": \"0 0 9-17 * * *\",\r\n```\r\n\r\nTo trigger At 9:30 AM every day:\r\n\r\n```json\r\n\"schedule\": \"0 30 9 * * *\",\r\n```\r\n\r\nTo trigger At 9:30 AM every weekday:\r\n\r\n```json\r\n\"schedule\": \"0 30 9 * * 1-5\",\r\n```\r\n\r\n## Timer trigger C# code example\r\n\r\nThis C# code example writes a single log each time the function is triggered.\r\n\r\n```csharp\r\npublic static void Run(TimerInfo myTimer, ILogger log)\r\n{\r\n log.LogInformation($\"C# Timer trigger function executed at: {DateTime.Now}\"); \r\n}\r\n```\r\n\r\n## Timer trigger JavaScript example\r\n\r\n```JavaScript\r\nmodule.exports = function(context, myTimer) {\r\n if (myTimer.IsPastDue)\r\n {\r\n context.log('JavaScript is running late!');\r\n }\r\n context.log(\"Timer last triggered at \" + myTimer.last);\r\n context.log(\"Timer triggered at \" + myTimer.next);\r\n \r\n context.done();\r\n}\r\n```","settings":[{"name":"name","value":"string","defaultValue":"myTimer","required":true,"label":"$timerTrigger_name_label","help":"$timerTrigger_name_help","validators":[{"expression":"^[a-zA-Z][a-zA-Z0-9]{0,127}$","errorText":"[variables('parameterName')]"}]},{"name":"schedule","value":"string","defaultValue":"0 * * * * *","required":true,"label":"$timerTrigger_schedule_label","help":"$timerTrigger_schedule_help","validators":[{"expression":"^(\\*|((([1-5]\\d)|\\d)(\\-(([1-5]\\d)|\\d)(\\/\\d+)?)?)(,((([1-5]\\d)|\\d)(\\-(([1-5]\\d)|\\d)(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|((([1-5]\\d)|\\d)(\\-(([1-5]\\d)|\\d)(\\/\\d+)?)?)(,((([1-5]\\d)|\\d)(\\-(([1-5]\\d)|\\d)(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|(((1\\d)|(2[0-3])|\\d)(\\-((1\\d)|(2[0-3])|\\d)(\\/\\d+)?)?)(,(((1\\d)|(2[0-3])|\\d)(\\-((1\\d)|(2[0-3])|\\d)(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|((([1-2]\\d)|(3[0-1])|[1-9])(\\-(([1-2]\\d)|(3[0-1])|[1-9])(\\/\\d+)?)?)(,((([1-2]\\d)|(3[0-1])|[1-9])(\\-(([1-2]\\d)|(3[0-1])|[1-9])(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|(([A-Za-z]+|(1[0-2])|[1-9])(\\-([A-Za-z]+|(1[0-2])|[1-9])(\\/\\d+)?)?)(,(([A-Za-z]+|(1[0-2])|[1-9])(\\-([A-Za-z]+|(1[0-2])|[1-9])(\\/\\d+)?)?))*)(\\/\\d+)? (\\*|(([A-Za-z]+|[0-6])(\\-([A-Za-z]+|[0-6])(\\/\\d+)?)?)(,(([A-Za-z]+|[0-6])(\\-([A-Za-z]+|[0-6])(\\/\\d+)?)?))*)(\\/\\d+)?$","errorText":"$timerTrigger_schedule_errorText"}]}]},{"type":"queueTrigger","displayName":"$queueTrigger_displayName","direction":"trigger","enabledInTryMode":true,"documentation":"#### Settings for storage queue trigger\r\n\r\n- `name` : The variable name used in function code for the queue or the queue message. \r\n- `queueName` : The name of the queue to poll. For queue naming rules, see [Naming Queues and Metadata](https://msdn.microsoft.com/library/dd179349.aspx).\r\n- `connection` : The name of an app setting that contains a storage connection string. If you leave `connection` empty, the trigger will work with the default storage connection string for the function app, which is specified by the AzureWebJobsStorage app setting.\r\n- `type` : Must be set to *queueTrigger*.\r\n- `direction` : Must be set to *in*. \r\n\r\n\r\n#### Additional metadata for Storage Queue trigger\r\n\r\nYou can get queue metadata in your function by using these variable names:\r\n\r\n* ExpirationTime\r\n* InsertionTime\r\n* NextVisibleTime\r\n* Id\r\n* PopReceipt\r\n* DequeueCount\r\n* QueueTrigger (another way to retrieve the queue message text as a string)\r\n\r\n#### C# types for Storage Queue trigger\r\n\r\nThe queue message can be deserialized to any of the following types:\r\n\r\n* Object (from JSON)\r\n* String\r\n* Byte array \r\n* `CloudQueueMessage` (C#) \r\n\r\n#### C# example for Storage Queue trigger\r\n\r\n```csharp\r\npublic static void Run(string myQueueItem, \r\n DateTimeOffset expirationTime, \r\n DateTimeOffset insertionTime, \r\n DateTimeOffset nextVisibleTime,\r\n string queueTrigger,\r\n string id,\r\n string popReceipt,\r\n int dequeueCount,\r\n ILogger log)\r\n{\r\n log.LogInformation($\"C# Queue trigger function processed: {myQueueItem}\\n\" +\r\n $\"queueTrigger={queueTrigger}\\n\" +\r\n $\"expirationTime={expirationTime}\\n\" +\r\n $\"insertionTime={insertionTime}\\n\" +\r\n $\"nextVisibleTime={nextVisibleTime}\\n\" +\r\n $\"id={id}\\n\" +\r\n $\"popReceipt={popReceipt}\\n\" + \r\n $\"dequeueCount={dequeueCount}\");\r\n}\r\n```\r\n\r\n#### JavaScript example for Storage Queue trigger\r\n\r\n```JavaScript\r\nmodule.exports = function(context, myQueueItem) {\r\n context.log('Dequeue Count: ' + context.bindingData.DequeueCount);\r\n\r\n // JavaScript supports returning strings or objects for queues\r\n if(typeof myQueueItem === 'string') {\r\n context.log('Trigger was a string! - ' + myQueueItem);\r\n } else if(typeof myQueueItem === 'object') {\r\n context.log('Trigger was an object! - \\n' + JSON.stringify(myQueueItem, null, ' '));\r\n }\r\n context.done(); //finish execution\r\n}\r\n```","extension":{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.10"},"settings":[{"name":"name","value":"string","defaultValue":"myQueueItem","required":true,"label":"$queueTrigger_name_label","help":"$queueTrigger_name_help","validators":[{"expression":"^[a-zA-Z][a-zA-Z0-9]{0,127}$","errorText":"[variables('parameterName')]"}]},{"name":"queueName","value":"string","defaultValue":"myqueue","required":true,"label":"$queueTrigger_queueName_label","help":"$queueTrigger_queueName_help","validators":[{"expression":"^[0-9a-z][a-z0-9-]{1,61}[0-9a-z]$|^[{][a-zA-Z0-9]{1,126}[}]$|^[%][a-zA-Z0-9]{1,126}[%]$","errorText":"$queueTrigger_queueName_errorText"}]},{"name":"connection","value":"string","resource":"Storage","required":true,"label":"[variables('storageConnStringLabel')]","help":"[variables('appSettingsHelp')]","placeholder":"[variables('selectConnection')]"}]},{"type":"blobTrigger","displayName":"$blobTrigger_displayName","direction":"trigger","enabledInTryMode":true,"documentation":"#### Settings for storage blob trigger\r\n\r\n- `name` : The variable name used in function code for the blob. \r\n- `path` : A path that specifies the container to monitor, and optionally a blob name pattern.\r\n- `connection` : The name of an app setting that contains a storage connection string. If you leave `connection` empty, the trigger will work with the default storage connection string for the function app, which is specified by the AzureWebJobsStorage app setting.\r\n- `type` : Must be set to *blobTrigger*.\r\n- `direction` : Must be set to *in*.\r\n\r\n#### Blob trigger name patterns\r\n\r\nYou can specify a blob name pattern in the `path` property. For example:\r\n\r\n```json\r\n\"path\": \"input/original-{name}\",\r\n```\r\n\r\nThis path would find a blob named *original-Blob1.txt* in the *input* container, and the value of the `name` variable in function code would be `Blob1`.\r\n\r\nAnother example:\r\n\r\n```json\r\n\"path\": \"input/{blobname}.{blobextension}\",\r\n```\r\n\r\nThis path would also find a blob named *original-Blob1.txt*, and the value of the `blobname` and `blobextension` variables in function code would be *original-Blob1* and *txt*.\r\n\r\nYou can restrict the types of blobs that trigger the function by specifying a pattern with a fixed value for the file extension. If you set the `path` to *samples/{name}.png*, only *.png* blobs in the *samples* container will trigger the function.\r\n\r\nIf you need to specify a name pattern for blob names that have curly braces in the name, double the curly braces. For example, if you want to find blobs in the *images* container that have names like this:\r\n\r\n\t\t{20140101}-soundfile.mp3\r\n\r\nuse this for the `path` property:\r\n\r\n\t\timages/{{20140101}}-{name}\r\n\r\nIn the example, the `name` variable value would be *soundfile.mp3*. \r\n\r\n#### Blob trigger supported types\r\n\r\nThe blob can be deserialized to any of the following types in JavaScript or C# functions:\r\n\r\n* Object (from JSON)\r\n* String\r\n\r\nIn C# functions you can also bind to any of the following types:\r\n\r\n* `TextReader`\r\n* `Stream`\r\n* `ICloudBlob`\r\n* `CloudBlockBlob`\r\n* `CloudPageBlob`\r\n* `CloudBlobContainer`\r\n* `CloudBlobDirectory`\r\n* `IEnumerable`\r\n* `IEnumerable`\r\n* Other types deserialized by [ICloudBlobStreamBinder](https://azure.microsoft.com/en-us/documentation/articles/websites-dotnet-webjobs-sdk-storage-blobs-how-to/)\r\n\r\n#### Blob trigger C# code example\r\n\r\n```csharp\r\npublic static void Run(string myBlob, ILogger log)\r\n{\r\n log.LogInformation($\"C# Blob trigger function processed: {myBlob}\");\r\n}\r\n```\r\n\r\n#### Blob trigger JavaScript example\r\n\r\n```JavaScript\r\nmodule.exports = function(context, myBlob) {\r\n context.log(myBlob);\r\n context.done();\r\n}\r\n```","extension":{"id":"Microsoft.Azure.WebJobs.Extensions.Storage","version":"3.0.10"},"settings":[{"name":"name","value":"string","defaultValue":"myBlob","required":true,"label":"$blobTrigger_name_label","help":"$blobTrigger_name_help","validators":[{"expression":"^[a-zA-Z][a-zA-Z0-9]{0,127}$","errorText":"[variables('parameterName')]"}]},{"name":"path","value":"string","defaultValue":"mycontainer","required":true,"label":"$blobTrigger_path_label","help":"$blobTrigger_path_help","validators":[{"expression":"(((^[a-z0-9](?:[a-z0-9]|(?:\\-(?!\\-))){1,61}[a-z0-9])|(^[{][a-zA-Z0-9]{1,126}[}])|(^[%][a-zA-Z0-9]{1,126}[%]))[\\/](\\S){0,1023}[^\\/]$)|(((^[a-z0-9](?:[a-z0-9]|(?:\\-(?!\\-))){1,61}[a-z0-9]$)|(^[{][a-zA-Z0-9]{1,126}[}]$)|(^[%][a-zA-Z0-9]{1,126}[%]$)))","errorText":"$blobTrigger_path_errorText"}]},{"name":"connection","value":"string","resource":"Storage","required":true,"label":"[variables('storageConnStringLabel')]","help":"[variables('appSettingsHelp')]","placeholder":"[variables('selectConnection')]"}]},{"type":"httpTrigger","displayName":"$httpTrigger_displayName","direction":"trigger","enabledInTryMode":true,"documentation":"## Settings for HTTP bindings\r\n\r\nThe settings provide properties that pertain to both the request and response.\r\n\r\nProperties for the HTTP request:\r\n\r\n- `name` : Variable name used in function code for the request object (or the request body in the case of JavaScript functions).\r\n- `type` : Must be set to *httpTrigger*.\r\n- `direction` : Must be set to *in*. \r\n- `authLevel` : Set to \"function\" to require the API key, \"anonymous\" to drop the API key requirement, or \"admin\" to require the master API key.\r\n\r\nProperties for the HTTP response:\r\n\r\n- `name` : Variable name used in function code for the response object.\r\n- `type` : Must be set to *http*.\r\n- `direction` : Must be set to *out*. \r\n\r\n## URL to trigger the function\r\n\r\nTo trigger a function, you send an HTTP request to a URL that is a combination of the function app URL and the function name:\r\n\r\n```\r\n https://{function app name}.azurewebsites.net/api/{function name} \r\n```\r\n\r\n## API keys\r\n\r\nBy default, an API key must be included with an HTTP request to trigger an HTTP function. The key can be included in a query string variable named `code`, or it can be included in an `x-functions-key` HTTP header. You can indicate that an API key is not required by setting the `authLevel` property to \"anonymous\" in the *function.json* file.\r\n\r\nYou can find API key values in the *D:\\home\\data\\Functions\\secrets* folder in the file system of the function app. The master key and function key are set in the *host.json* file, as shown in this example. \r\n\r\n```json\r\n{\r\n \"masterKey\": \"K6P2VxK6P2VxK6P2VxmuefWzd4ljqeOOZWpgDdHW269P2hb7OSJbDg==\",\r\n \"functionKey\": \"OBmXvc2K6P2VxK6P2VxK6P2VxVvCdB89gChyHbzwTS/YYGWWndAbmA==\"\r\n}\r\n```\r\n\r\nThe function key from *host.json* can be used to trigger any function but won't trigger a disabled function. The master key can be used to trigger any function and will trigger a function even if it's disabled. You can configure a function to require the master key by setting the `authLevel` property to \"admin\". \r\n\r\nIf the *secrets* folder contains a JSON file with the same name as a function, the `key` property in that file can also be used to trigger the function, and this key will only work with the function it refers to. For example, the API key for a function named `HttpTrigger` is specified in *HttpTrigger.json* in the *secrets* folder. Here is an example:\r\n\r\n```json\r\n{\r\n \"key\":\"0t04nmo37hmoir2rwk16skyb9xsug32pdo75oce9r4kg9zfrn93wn4cx0sxo4af0kdcz69a4i\"\r\n}\r\n```\r\n\r\n## Example C# code for an HTTP trigger function \r\n\r\n```csharp\r\nusing System.Net;\r\nusing System.Threading.Tasks;\r\n\r\npublic static async Task Run(HttpRequestMessage req, ILogger log)\r\n{\r\n log.LogInformation(\"C# HTTP trigger function processed a request.\");\r\n\r\n // parse query parameter\r\n string name = req.GetQueryNameValuePairs()\r\n .FirstOrDefault(q => string.Compare(q.Key, \"name\", true) == 0)\r\n .Value;\r\n\r\n // Get request body\r\n dynamic data = await req.Content.ReadAsAsync();\r\n\r\n // Set name to query string or body data\r\n name = name ?? data?.name;\r\n\r\n return name == null\r\n ? req.CreateResponse(HttpStatusCode.BadRequest, \"Please pass a name on the query string or in the request body\")\r\n : req.CreateResponse(HttpStatusCode.OK, \"Hello \" + name);\r\n}\r\n```\r\n\r\n## Example JavaScript code for an HTTP trigger function \r\n\r\nWe support an [express-like api](https://expressjs.com/en/4x/api.html#res) for JavaScript http triggers.\r\nSee supported methods for [context.req](/~https://github.com/Azure/azure-functions-host/blob/v1.x/src/WebJobs.Script/azurefunctions/http/request.js) and [context.res](/~https://github.com/Azure/azure-functions-host/blob/v1.x/src/WebJobs.Script/azurefunctions/http/response.js).\r\n\r\n```javascript\r\nmodule.exports = function(context, req) {\r\n context.log('JavaScript HTTP trigger function processed a request.');\r\n\r\n if (req.query.name || (req.body && req.body.name)) {\r\n // using the express api style\r\n context.res\r\n // set statusCode to 200\r\n .status(200)\r\n // set a header on the response\r\n .set(\"QuerySet\", req.query.name != undefined)\r\n // send will automatically call context.done\r\n .send(\"Hello \" + (req.query.name || req.body.name));\r\n } else {\r\n // alternate style\r\n context.res = {\r\n status: 400,\r\n body: \"Please pass a name on the query string or in the request body\"\r\n };\r\n context.done();\r\n }\r\n};\r\n```\r\n","settings":[{"name":"name","value":"string","defaultValue":"req","required":true,"label":"$httpTrigger_name_label","help":"$httpTrigger_name_help","validators":[{"expression":"^[a-zA-Z][a-zA-Z0-9]{0,127}$","errorText":"[variables('parameterName')]"}]},{"name":"route","value":"string","required":false,"label":"$httpTrigger_route_label","help":"$httpTrigger_route_help","validators":[]},{"name":"authLevel","value":"enum","required":true,"defaultValue":"function","enum":[{"value":"function","display":"Function"},{"value":"anonymous","display":"Anonymous"},{"value":"admin","display":"Admin"}],"label":"$httpTrigger_authLevel_label","help":"$httpTrigger_authLevel_help"},{"name":"methods","value":"checkBoxList","defaultValue":["get","post","delete","head","patch","put","options","trace"],"enum":[{"value":"get","display":"GET"},{"value":"post","display":"POST"},{"value":"delete","display":"DELETE"},{"value":"head","display":"HEAD"},{"value":"patch","display":"PATCH"},{"value":"put","display":"PUT"},{"value":"options","display":"OPTIONS"},{"value":"trace","display":"TRACE"}],"label":"$httpTrigger_methods_label","help":"$httpTrigger_methods_help"}],"rules":[{"name":"methodRule","type":"exclusivity","values":[{"value":"allMethods","display":"All methods","hiddenSettings":["methods"],"shownSettings":[]},{"value":"methods","display":"Selected methods","hiddenSettings":[],"shownSettings":["methods"]}],"label":"$httpTrigger_methodRule_label","help":"$httpTrigger_methodRule_help"}]},{"type":"cosmosDBTrigger","displayName":"$cosmosDB_trigger_displayName","direction":"trigger","enabledInTryMode":false,"documentation":"#### Settings for Cosmos DB trigger binding\r\n\r\nThe Cosmos DB Trigger leverages the [Cosmos DB Change Feed](https://docs.microsoft.com/azure/cosmos-db/change-feed) to listen for changes across partitions. It uses a **second collection** to store *leases* over the partitions.\r\n\r\nBoth the collection being monitored for changes and the collection that will hold the leases need to be available for the trigger to work.\r\n\r\nThe settings for an Azure Cosmos DB trigger specifies the following properties:\r\n\r\n- `type` : Must be set to *cosmosDBTrigger*.\r\n- `name` : The variable name used in function code for the list of documents. \r\n- `direction` : Must be set to *in*. \r\n- `databaseName` : The name of the database that holds the collection to monitor.\r\n- `collectionName` : The name of the collection to monitor.\r\n- `connectionStringSetting` *optional*: The name of an app setting that contains the connection string to the service which holds the collection to monitor. If `connectionStringSetting` is not set then the value of AzureWebJobsCosmosDBConnectionStringName setting is used.\r\n- `leaseConnectionStringSetting` : *optional*. The name of an app setting that contains the connection string to the service which holds the lease collection. If not set it will connect to the service defined by `connectionStringSetting`.\r\n- `leaseDatabaseName` : *optional*. The name of the database that holds the collection to store leases. If not set, it will use the value of `databaseName`.\r\n- `leaseCollectionName` : *optional*. The name of the collection to store leases. If not set, it will use \"leases\".\r\n- `createLeaseCollectionIfNotExists` : *optional*. true/false. Checks for existence and automatically creates the leases collection. Default is `false`.\r\n- `leaseCollectionThroughput` : *optional*. When `createLeaseCollectionIfNotExists` is set to `true`, defines the amount of Request Units to assign to the created lease collection.\r\n\r\n> Connection strings used for the Lease collection require **write permission**.\r\n\r\n#### Azure Cosmos DB trigger C# example\r\n \r\n\t#r \"Microsoft.Azure.Documents.Client\"\r\n\tusing Microsoft.Azure.Documents;\r\n\tusing System.Collections.Generic;\r\n\tusing System;\r\n\tpublic static void Run(IReadOnlyList input, ILogger log)\r\n\t{\r\n\t\tlog.LogInformation(\"Documents modified \" + input.Count);\r\n\t\tlog.LogInformation(\"First document Id \" + input[0].Id);\r\n\t}\r\n\r\n#### Azure Cosmos DB trigger JavaScript example\r\n\r\n\tmodule.exports = function (context, input) {\r\n\t\tcontext.log('First document Id modified : ', input[0].id);\r\n\r\n\t\tcontext.done();\r\n\t}\r\n","extension":{"id":"Microsoft.Azure.WebJobs.Extensions.CosmosDB","version":"3.0.5"},"settings":[{"name":"name","value":"string","defaultValue":"inputDocuments","required":true,"label":"$cosmosDBIn_name_label","help":"$cosmosDBIn_name_help","validators":[{"expression":"^[a-zA-Z][a-zA-Z0-9]{0,127}$","errorText":"[variables('parameterName')]"}]},{"name":"connectionStringSetting","value":"string","required":true,"resource":"DocumentDB","label":"$cosmosDBIn_connection_label","help":"$cosmosDBIn_connection_help","placeholder":"[variables('selectConnection')]"},{"name":"databaseName","value":"string","defaultValue":"","required":true,"label":"$cosmosDBIn_databaseName_label","help":"$cosmosDBIn_databaseName_help"},{"name":"collectionName","value":"string","defaultValue":"","required":true,"label":"$cosmosDBIn_collectionName_label","help":"$cosmosDBIn_collectionName_help"},{"name":"leaseCollectionName","value":"string","required":true,"label":"$cosmosDBIn_leaseCollectionName_label","help":"$cosmosDBIn_leaseCollectionName_help"},{"name":"createLeaseCollectionIfNotExists","value":"boolean","defaultValue":true,"required":true,"label":"$cosmosDBIn_createIfNotExists_label","help":"$cosmosDBIn_createIfNotExists_help"}]}]} diff --git a/resources/backupTemplates/ballerina/resources/Resources.json b/resources/backupTemplates/ballerina/resources/Resources.json new file mode 100644 index 000000000..b5f628df6 --- /dev/null +++ b/resources/backupTemplates/ballerina/resources/Resources.json @@ -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"}} diff --git a/resources/backupTemplates/ballerina/templates/templates.json b/resources/backupTemplates/ballerina/templates/templates.json new file mode 100644 index 000000000..c0ed5d036 --- /dev/null +++ b/resources/backupTemplates/ballerina/templates/templates.json @@ -0,0 +1 @@ +[{"id":"HttpTrigger-Ballerina","runtime":"2","files":{"%functionName%.bal":"import ballerinax/azure_functions as af;\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@af:HttpTrigger{\n authLevel: \"%authLevel%\"\n}\nlistener af:HttpListener httpListener = new af: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 as af;\nimport ballerina/log;\n\n// The following Function will be invoked when a new blob added to the specified blob storage.\n@af:BlobTrigger {\n path: \"%path%\",\n connection: \"%connection%\"\n}\nlistener af:BlobListener blobListener = new af: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 as af;\n\n// The following Function will be invoked when an entry is added to CosmosDB collection.\n@af:CosmosDBTrigger {connectionStringSetting: \"%connectionStringSetting%\", databaseName: \"%databaseName%\", collectionName: \"%collectionName%\"}\nlistener af: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 as af;\n\n\n// The following Function will be executed when a message is added to the queue storage.\n@af:QueueTrigger {\n queueName: \"%queueName%\",\n connection: \"%connection%\"\n}\nlistener af:QueueListener queueListener = new af: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 as af;\n\n// The following function will be invoked periodically according to the schedule given.\n@af:TimerTrigger {schedule: \"%schedule%\"}\nlistener af:TimerListener timerEp = new ();\n\nservice \"%functionName%\" on timerEp {\n remote function onTrigger(af: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"]}}] diff --git a/src/LocalResourceProvider.ts b/src/LocalResourceProvider.ts index bd4d5b955..4fdfa5ac7 100644 --- a/src/LocalResourceProvider.ts +++ b/src/LocalResourceProvider.ts @@ -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: projectPath, isIsolated: false }; } else { return undefined; } diff --git a/src/commands/createFunction/FunctionSubWizard.ts b/src/commands/createFunction/FunctionSubWizard.ts index ca00319dd..d11b1f422 100644 --- a/src/commands/createFunction/FunctionSubWizard.ts +++ b/src/commands/createFunction/FunctionSubWizard.ts @@ -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'; @@ -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()); @@ -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()); diff --git a/src/commands/createFunction/ballerinaSteps/BallerinaFunctionCreateStep.ts b/src/commands/createFunction/ballerinaSteps/BallerinaFunctionCreateStep.ts new file mode 100644 index 000000000..47f2dbd28 --- /dev/null +++ b/src/commands/createFunction/ballerinaSteps/BallerinaFunctionCreateStep.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + public async executeCore(context: IBallerinaFunctionWizardContext): Promise { + 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 + 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); + } +} diff --git a/src/commands/createFunction/ballerinaSteps/BallerinaFunctionNameStep.ts b/src/commands/createFunction/ballerinaSteps/BallerinaFunctionNameStep.ts new file mode 100644 index 000000000..afa0c65c7 --- /dev/null +++ b/src/commands/createFunction/ballerinaSteps/BallerinaFunctionNameStep.ts @@ -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 { + protected async getUniqueFunctionName(context: IFunctionWizardContext & IBallerinaProjectWizardContext): Promise { + 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 { + 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; + } + } +} diff --git a/src/commands/createFunction/ballerinaSteps/IBallerinaFunctionWizardContext.ts b/src/commands/createFunction/ballerinaSteps/IBallerinaFunctionWizardContext.ts new file mode 100644 index 000000000..047df16c5 --- /dev/null +++ b/src/commands/createFunction/ballerinaSteps/IBallerinaFunctionWizardContext.ts @@ -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 }; +} diff --git a/src/commands/createNewProject/NewProjectLanguageStep.ts b/src/commands/createNewProject/NewProjectLanguageStep.ts index 3b8d9ce55..13fb504f4 100644 --- a/src/commands/createNewProject/NewProjectLanguageStep.ts +++ b/src/commands/createNewProject/NewProjectLanguageStep.ts @@ -16,6 +16,7 @@ import { addInitVSCodeSteps } from '../initProjectForVSCode/InitVSCodeLanguageSt import { DotnetRuntimeStep } from './dotnetSteps/DotnetRuntimeStep'; import { IProjectWizardContext } from './IProjectWizardContext'; import { addJavaCreateProjectSteps } from './javaSteps/addJavaCreateProjectSteps'; +import { addBallerinaCreateProjectSteps } from './ballerinaSteps/addBallerinaCreateProjectSteps'; import { ProgrammingModelStep } from './ProgrammingModelStep'; import { CustomProjectCreateStep } from './ProjectCreateStep/CustomProjectCreateStep'; import { DotnetProjectCreateStep } from './ProjectCreateStep/DotnetProjectCreateStep'; @@ -48,6 +49,7 @@ export class NewProjectLanguageStep extends AzureWizardPromptStep): Promise { + await ballerinaUtils.validateBallerinaInstalled(context); + await super.executeCore(context, progress); + + const ballerinaTomlPath: string = path.join(context.projectPath, ballerinaTomlFileName); + if (await confirmOverwriteFile(context, ballerinaTomlPath)) { + await AzExtFsExtra.writeFile(ballerinaTomlPath, await this.getBallerinaTomlContent(context)); + } + } + + async getBallerinaTomlContent(context: IBallerinaProjectWizardContext): Promise { + return `[package] +org="${context.balOrgName}" +name="${context.balPackageName}" +version="${context.balVersion}" + +[build-options] +observabilityIncluded=true +cloud="azure_functions" +${context.balBackend === BallerinaBackend.native ? 'native=true' : ''} +`; + } +} + +const ballerinaGitIgnore: string = `target/`; + diff --git a/src/commands/createNewProject/ballerinaSteps/BallerinaBackendStep.ts b/src/commands/createNewProject/ballerinaSteps/BallerinaBackendStep.ts new file mode 100644 index 000000000..ce20c06cd --- /dev/null +++ b/src/commands/createNewProject/ballerinaSteps/BallerinaBackendStep.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; +import { BallerinaBackend } from "../../../constants"; +import { previewDescription } from "../../../constants-nls"; +import { localize } from "../../../localize"; +import { IBallerinaProjectWizardContext } from "./IBallerinaProjectWizardContext"; + + +export class BallerinaBackendStep extends AzureWizardPromptStep { + public static async setDefaultBuildTool(context: IBallerinaProjectWizardContext): Promise { + context.balBackend = BallerinaBackend.jvm; + } + + public async prompt(context: IBallerinaProjectWizardContext): Promise { + const picks: IAzureQuickPickItem[] = [ + { label: 'JVM', data: BallerinaBackend.jvm }, + { label: 'Native (Experimental)', description: previewDescription, data: BallerinaBackend.native }, + ]; + const placeHolder: string = localize('selectBallerinaBackend', 'Select the backend for Ballerina project'); + context.balBackend = (await context.ui.showQuickPick(picks, { placeHolder })).data; + } + + public shouldPrompt(context: IBallerinaProjectWizardContext): boolean { + return !context.balBackend; + } +} diff --git a/src/commands/createNewProject/ballerinaSteps/BallerinaPackageNameStep.ts b/src/commands/createNewProject/ballerinaSteps/BallerinaPackageNameStep.ts new file mode 100644 index 000000000..cb045bcac --- /dev/null +++ b/src/commands/createNewProject/ballerinaSteps/BallerinaPackageNameStep.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { InputBoxOptions } from "vscode"; +import { localize } from "../../../localize"; +import { IBallerinaProjectWizardContext } from "./IBallerinaProjectWizardContext"; + +export class BalPackageNameStep extends AzureWizardPromptStep { + public async prompt(context: IBallerinaProjectWizardContext): Promise { + const options: InputBoxOptions = { + placeHolder: localize('packageNamePlaceHolder', 'Package Name'), + prompt: localize('packageNamePrompt', 'Provide a name for the ballerina package'), + value: 'my_package' + }; + context.balPackageName = await context.ui.showInputBox(options); + } + + public shouldPrompt(context: IBallerinaProjectWizardContext): boolean { + return !context.balPackageName; + } +} diff --git a/src/commands/createNewProject/ballerinaSteps/BallerinaPackageOrgStep.ts b/src/commands/createNewProject/ballerinaSteps/BallerinaPackageOrgStep.ts new file mode 100644 index 000000000..b1dc84b3b --- /dev/null +++ b/src/commands/createNewProject/ballerinaSteps/BallerinaPackageOrgStep.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { InputBoxOptions } from "vscode"; +import { localize } from "../../../localize"; +import { IBallerinaProjectWizardContext } from "./IBallerinaProjectWizardContext"; + +export class BalPackageOrgStep extends AzureWizardPromptStep { + public async prompt(context: IBallerinaProjectWizardContext): Promise { + const options: InputBoxOptions = { + placeHolder: localize('packageOrgPlaceHolder', 'Package Organization'), + prompt: localize('packageOrgPrompt', 'Provide the organization name for the ballerina package'), + value: 'my_org' + }; + context.balOrgName = await context.ui.showInputBox(options); + } + + public shouldPrompt(context: IBallerinaProjectWizardContext): boolean { + return !context.balOrgName; + } +} diff --git a/src/commands/createNewProject/ballerinaSteps/BallerinaPackageVersionStep.ts b/src/commands/createNewProject/ballerinaSteps/BallerinaPackageVersionStep.ts new file mode 100644 index 000000000..87d1c0b7e --- /dev/null +++ b/src/commands/createNewProject/ballerinaSteps/BallerinaPackageVersionStep.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { InputBoxOptions } from "vscode"; +import { localize } from "../../../localize"; +import { IBallerinaProjectWizardContext } from "./IBallerinaProjectWizardContext"; + +export class BalPackageVersionStep extends AzureWizardPromptStep { + public async prompt(context: IBallerinaProjectWizardContext): Promise { + const options: InputBoxOptions = { + placeHolder: localize('packageVersionPlaceHolder', 'Package Version'), + prompt: localize('packageVersionPrompt', 'Provide the version of the ballerina package'), + value: '1.0.0' + }; + context.balVersion = await context.ui.showInputBox(options); + } + + public shouldPrompt(context: IBallerinaProjectWizardContext): boolean { + return !context.balVersion; + } +} diff --git a/src/commands/createNewProject/ballerinaSteps/IBallerinaProjectWizardContext.ts b/src/commands/createNewProject/ballerinaSteps/IBallerinaProjectWizardContext.ts new file mode 100644 index 000000000..ff527aa4d --- /dev/null +++ b/src/commands/createNewProject/ballerinaSteps/IBallerinaProjectWizardContext.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import { BallerinaBackend } from '../../../constants'; +import { IProjectWizardContext } from "../IProjectWizardContext"; + +export interface IBallerinaProjectWizardContext extends IProjectWizardContext { + balVersion?: string; + balOrgName: string; + balPackageName?: string; + balBackend?: BallerinaBackend; +} + +export function getBallerinaPackagePath(projectPath: string): string { + return path.join(projectPath); +} + +export function getBallerinaFunctionFilePath(projectPath: string, functionName: string): string { + return path.join(getBallerinaPackagePath(projectPath), functionName + '.bal'); +} diff --git a/src/commands/createNewProject/ballerinaSteps/addBallerinaCreateProjectSteps.ts b/src/commands/createNewProject/ballerinaSteps/addBallerinaCreateProjectSteps.ts new file mode 100644 index 000000000..c8ed4aa66 --- /dev/null +++ b/src/commands/createNewProject/ballerinaSteps/addBallerinaCreateProjectSteps.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardExecuteStep, AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { IProjectWizardContext } from "../IProjectWizardContext"; +import { BallerinaProjectCreateStep } from "../ProjectCreateStep/BallerinaProjectCreateSteps"; +import { BallerinaBackendStep } from "./BallerinaBackendStep"; +import { BalPackageNameStep } from "./BallerinaPackageNameStep"; +import { BalPackageOrgStep } from "./BallerinaPackageOrgStep"; +import { BalPackageVersionStep } from "./BallerinaPackageVersionStep"; + +export async function addBallerinaCreateProjectSteps( + promptSteps: AzureWizardPromptStep[], + executeSteps: AzureWizardExecuteStep[]): Promise { + promptSteps.push(new BalPackageNameStep(), new BalPackageOrgStep(), new BalPackageVersionStep(), new BallerinaBackendStep()); + executeSteps.push(new BallerinaProjectCreateStep()); +} diff --git a/src/commands/initProjectForVSCode/InitVSCodeLanguageStep.ts b/src/commands/initProjectForVSCode/InitVSCodeLanguageStep.ts index 96934fae4..1dea869b6 100644 --- a/src/commands/initProjectForVSCode/InitVSCodeLanguageStep.ts +++ b/src/commands/initProjectForVSCode/InitVSCodeLanguageStep.ts @@ -9,6 +9,7 @@ import { previewPythonModel, ProjectLanguage } from '../../constants'; import { pythonNewModelPreview } from '../../constants-nls'; import { localize } from '../../localize'; import { IProjectWizardContext } from '../createNewProject/IProjectWizardContext'; +import { BallerinaInitVSCodeStep } from './InitVSCodeStep/BallerinaInitVSCodeStep'; import { DotnetInitVSCodeStep } from './InitVSCodeStep/DotnetInitVSCodeStep'; import { DotnetScriptInitVSCodeStep } from './InitVSCodeStep/DotnetScriptInitVSCodeStep'; import { JavaScriptInitVSCodeStep } from './InitVSCodeStep/JavaScriptInitVSCodeStep'; @@ -24,6 +25,7 @@ export class InitVSCodeLanguageStep extends AzureWizardPromptStep { // Display all languages, even if we don't have full support for them const languagePicks: IAzureQuickPickItem<{ language: ProjectLanguage, model?: number }>[] = [ + { label: ProjectLanguage.Ballerina, data: { language: ProjectLanguage.Ballerina } }, { label: ProjectLanguage.CSharp, data: { language: ProjectLanguage.CSharp } }, { label: ProjectLanguage.CSharpScript, data: { language: ProjectLanguage.CSharpScript } }, { label: ProjectLanguage.FSharp, data: { language: ProjectLanguage.FSharp } }, @@ -84,6 +86,9 @@ export async function addInitVSCodeSteps( case ProjectLanguage.FSharpScript: executeSteps.push(new DotnetScriptInitVSCodeStep()); break; + case ProjectLanguage.Ballerina: + executeSteps.push(new BallerinaInitVSCodeStep()); + break; default: executeSteps.push(new ScriptInitVSCodeStep()); break; diff --git a/src/commands/initProjectForVSCode/InitVSCodeStep/BallerinaInitVSCodeStep.ts b/src/commands/initProjectForVSCode/InitVSCodeStep/BallerinaInitVSCodeStep.ts new file mode 100644 index 000000000..de21958f4 --- /dev/null +++ b/src/commands/initProjectForVSCode/InitVSCodeStep/BallerinaInitVSCodeStep.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TaskDefinition } from 'vscode'; +import { func, hostStartCommand, hostStartTaskName, ProjectLanguage, projectSubpathSetting } from '../../../constants'; +import { getFuncWatchProblemMatcher } from '../../../vsCodeConfig/settings'; +import { convertToFunctionsTaskLabel } from '../../../vsCodeConfig/tasks'; +import { IJavaProjectWizardContext } from '../../createNewProject/javaSteps/IJavaProjectWizardContext'; +import { isBallerinaProject } from '../detectProjectLanguage'; +import { InitVSCodeStepBase } from './InitVSCodeStepBase'; + +const ballerinaPackageTaskLabel: string = convertToFunctionsTaskLabel('package'); + +export class BallerinaInitVSCodeStep extends InitVSCodeStepBase { + protected preDeployTask: string = ballerinaPackageTaskLabel; + + private _debugSubpath: string = "target/azure_functions"; + + protected async executeCore(context: IJavaProjectWizardContext): Promise { + const isProject: boolean = await isBallerinaProject(context.projectPath); + if (!isProject) { + this._debugSubpath = "azure_functions"; + } + + this.setDeploySubpath(context, this._debugSubpath); + this.settings.push({ + key: projectSubpathSetting, + value: this._debugSubpath + }); + } + + protected getTasks(language: ProjectLanguage): TaskDefinition[] { + return [ + { + type: func, + label: hostStartTaskName, + command: hostStartCommand, + problemMatcher: getFuncWatchProblemMatcher(language), + isBackground: true, + options: { + cwd: this._debugSubpath + }, + dependsOn: ballerinaPackageTaskLabel + }, + { + label: ballerinaPackageTaskLabel, + command: "bal build", + type: 'shell', + group: { + kind: 'build', + isDefault: true + } + } + ]; + } +} + diff --git a/src/commands/initProjectForVSCode/detectProjectLanguage.ts b/src/commands/initProjectForVSCode/detectProjectLanguage.ts index fe1a346b6..1e12e0857 100644 --- a/src/commands/initProjectForVSCode/detectProjectLanguage.ts +++ b/src/commands/initProjectForVSCode/detectProjectLanguage.ts @@ -5,7 +5,7 @@ import { AzExtFsExtra, IActionContext } from '@microsoft/vscode-azext-utils'; import * as path from 'path'; -import { buildGradleFileName, localSettingsFileName, packageJsonFileName, pomXmlFileName, previewPythonModel, ProjectLanguage, pythonFunctionAppFileName, workerRuntimeKey } from '../../constants'; +import { buildGradleFileName, localSettingsFileName, packageJsonFileName, pomXmlFileName, previewPythonModel, ProjectLanguage, pythonFunctionAppFileName, workerRuntimeKey, ballerinaTomlFileName } from '../../constants'; import { getLocalSettingsJson, ILocalSettingsJson } from '../../funcConfig/local.settings'; import { dotnetUtils } from '../../utils/dotnetUtils'; import { hasNodeJsDependency, tryGetPackageJson } from '../../utils/nodeJsUtils'; @@ -39,6 +39,10 @@ export async function detectProjectLanguage(context: IActionContext, projectPath if (await isFSharpProject(context, projectPath)) { detectedLangs.push(ProjectLanguage.FSharp); } + + if (await isBallerinaProject(projectPath)) { + detectedLangs.push(ProjectLanguage.Ballerina); + } await detectLanguageFromLocalSettings(context, detectedLangs, projectPath); @@ -78,6 +82,10 @@ async function isTypeScriptProject(projectPath: string): Promise { return await hasNodeJsDependency(projectPath, 'typescript', true); } +export async function isBallerinaProject(projectPath: string): Promise { + return await AzExtFsExtra.pathExists(path.join(projectPath, ballerinaTomlFileName)); +} + /** * If the user has a "local.settings.json" file, we may be able to infer the langauge from the setting "FUNCTIONS_WORKER_RUNTIME" */ diff --git a/src/constants.ts b/src/constants.ts index a73267922..20579984a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -32,6 +32,7 @@ export enum ProjectLanguage { PowerShell = 'PowerShell', Python = 'Python', TypeScript = 'TypeScript', + Ballerina = 'Ballerina', Custom = 'Custom' } @@ -68,6 +69,14 @@ export enum JavaBuildTool { gradle = 'gradle' } + +export const ballerinaTomlFileName: string = "Ballerina.toml" +export enum BallerinaBackend { + jvm = 'jvm', + native = 'native' +} + + export enum PackageManager { npm = 'npm', brew = 'brew' diff --git a/src/templates/CentralTemplateProvider.ts b/src/templates/CentralTemplateProvider.ts index a57d1bc24..6ba33fe9d 100644 --- a/src/templates/CentralTemplateProvider.ts +++ b/src/templates/CentralTemplateProvider.ts @@ -21,6 +21,7 @@ import { IFunctionTemplate, TemplateCategory } from './IFunctionTemplate'; import { ITemplates } from './ITemplates'; import { getJavaVerifiedTemplateIds } from './java/getJavaVerifiedTemplateIds'; import { JavaTemplateProvider } from './java/JavaTemplateProvider'; +import { BallerinaProvider } from './script/BallerinaProvider'; import { getScriptVerifiedTemplateIds } from './script/getScriptVerifiedTemplateIds'; import { NodeV4Provider } from './script/NodeV4Provider'; import { IScriptFunctionTemplate } from './script/parseScriptTemplates'; @@ -60,6 +61,9 @@ export class CentralTemplateProvider implements Disposable { case ProjectLanguage.Java: providers.push(new JavaTemplateProvider(version, projectPath, language, projectTemplateKey)); break; + case ProjectLanguage.Ballerina: + providers.push(new BallerinaProvider(version, projectPath, language, projectTemplateKey)); + break; default: if (isPythonV2Plus(language, languageModel)) { providers.push(new PysteinTemplateProvider(version, projectPath, language, projectTemplateKey)); @@ -73,6 +77,7 @@ export class CentralTemplateProvider implements Disposable { } break; } + return providers; } diff --git a/src/templates/script/BallerinaProvider.ts b/src/templates/script/BallerinaProvider.ts new file mode 100644 index 000000000..383cf91ea --- /dev/null +++ b/src/templates/script/BallerinaProvider.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzExtFsExtra, IActionContext } from '@microsoft/vscode-azext-utils'; +import * as path from 'path'; +import { IBindingTemplate } from '../IBindingTemplate'; +import { IFunctionTemplate } from '../IFunctionTemplate'; +import { ITemplates } from '../ITemplates'; +import { TemplateProviderBase, TemplateType } from '../TemplateProviderBase'; +import { getScriptResourcesLanguage } from './getScriptResourcesLanguage'; +import { parseScriptTemplates } from './parseScriptTemplates'; + +export class BallerinaProvider extends TemplateProviderBase { + public templateType: TemplateType = TemplateType.Script; + + protected get backupSubpath(): string { + return path.join('ballerina'); + } + + protected _rawResources: object; + protected _rawTemplates: object[]; + protected _rawBindings: object; + + public async getCachedTemplates(): Promise { + return await this.getBackupTemplates(); + } + + public async getLatestTemplateVersion(_context: IActionContext): Promise { + return '1.0'; + } + + public async getLatestTemplates(_context: IActionContext, _latestTemplateVersion: string): Promise { + return await this.getBackupTemplates(); + } + + public async getBackupTemplates(): Promise { + return await this.parseTemplates(this.getBackupPath()); + } + + public async updateBackupTemplates(): Promise { + // NOTE: No-op as the templates are only bundled with this extension. + await Promise.resolve(); + } + + public async cacheTemplates(): Promise { + // NOTE: No-op as the templates are only bundled with this extension. + await Promise.resolve(); + } + + public async clearCachedTemplates(): Promise { + // NOTE: No-op as the templates are only bundled with this extension. + await Promise.resolve(); + } + + public includeTemplate(template: IFunctionTemplate | IBindingTemplate): boolean { + return this.isFunctionTemplate(template) + && template.language === this.language + && !!template.triggerType + } + + protected async parseTemplates(rootPath: string): Promise { + const paths: ITemplatePaths = this.getTemplatePaths(rootPath); + this._rawTemplates = await AzExtFsExtra.readJSON(paths.templates); + this._rawBindings = await AzExtFsExtra.readJSON(paths.bindings); + this._rawResources = await AzExtFsExtra.readJSON(paths.resources); + + return parseScriptTemplates(this._rawResources, this._rawTemplates, this._rawBindings); + } + + protected getResourcesLanguage(): string { + return this.resourcesLanguage || getScriptResourcesLanguage(); + } + + private getTemplatePaths(rootPath: string): ITemplatePaths { + const resources: string = path.join(rootPath, 'resources', `Resources.json`); + const templates: string = path.join(rootPath, 'templates', 'templates.json'); + const bindings: string = path.join(rootPath, 'bindings', 'bindings.json'); + return { resources, templates, bindings }; + } + + private isFunctionTemplate(template: IFunctionTemplate | IBindingTemplate): template is IFunctionTemplate { + return (template as IFunctionTemplate).id !== undefined; + } +} + +interface ITemplatePaths { + resources: string; + templates: string; + bindings: string; +} diff --git a/src/templates/script/getScriptVerifiedTemplateIds.ts b/src/templates/script/getScriptVerifiedTemplateIds.ts index 8581f0fe4..82a8a42f3 100644 --- a/src/templates/script/getScriptVerifiedTemplateIds.ts +++ b/src/templates/script/getScriptVerifiedTemplateIds.ts @@ -39,7 +39,7 @@ export function getScriptVerifiedTemplateIds(version: string): (string | RegExp) // These languages are only supported in v2+ - same functions as JavaScript, with a few minor exceptions that aren't worth distinguishing here // NOTE: The Python Preview IDs are only temporary. // NOTE: The Node Programming Model IDs include -4.x as a suffix - const regExps = verifiedTemplateIds.map(t => new RegExp(`^${t}-(JavaScript(-4.x)?|TypeScript(-4.x)?|Python|PowerShell|Custom|Python-Preview|Python-Preview-Append)$`, 'i')); + const regExps = verifiedTemplateIds.map(t => new RegExp(`^${t}-(JavaScript(-4.x)?|TypeScript(-4.x)?|Python|Ballerina|PowerShell|Custom|Python-Preview|Python-Preview-Append)$`, 'i')); // The Entity templates aren't supported in PowerShell at all, and the DurableFunctionsEntityHttpStart template is not yet supported in Python. // As a result, we need to manually create their respective regular expressions to account for these edge cases diff --git a/src/tree/AzureAccountTreeItemWithProjects.ts b/src/tree/AzureAccountTreeItemWithProjects.ts index ec4e60a33..7cc92eb37 100644 --- a/src/tree/AzureAccountTreeItemWithProjects.ts +++ b/src/tree/AzureAccountTreeItemWithProjects.ts @@ -166,6 +166,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: projectPath, isIsolated: false }; } else { return undefined; } diff --git a/src/utils/ballerinaUtils.ts b/src/utils/ballerinaUtils.ts new file mode 100644 index 000000000..707f93eeb --- /dev/null +++ b/src/utils/ballerinaUtils.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import * as vscode from 'vscode'; +import { localize } from '../localize'; +import { cpUtils } from './cpUtils'; + +export namespace ballerinaUtils { + const ballerinaCommand: string = 'bal'; + + export async function validateBallerinaInstalled(context: IActionContext): Promise { + try { + await cpUtils.executeCommand(undefined, undefined, ballerinaCommand, '--version'); + } catch (error) { + const message: string = localize('ballerinaNotFound', 'Failed to find "bal", please ensure that the bal bin directory is in your system path.'); + + if (!context.errorHandling.suppressDisplay) { + // don't wait + void vscode.window.showErrorMessage(message); + context.errorHandling.suppressDisplay = true; + } + + throw new Error(message); + } + } +} diff --git a/src/vsCodeConfig/settings.ts b/src/vsCodeConfig/settings.ts index bfb00f97a..887e84f44 100644 --- a/src/vsCodeConfig/settings.ts +++ b/src/vsCodeConfig/settings.ts @@ -80,6 +80,7 @@ export function getRootFunctionsWorkerRuntime(language: string | undefined): str case ProjectLanguage.CSharp: case ProjectLanguage.FSharp: return 'dotnet'; + case ProjectLanguage.Ballerina: case ProjectLanguage.Java: return 'java'; case ProjectLanguage.Python: