From 294873613a72a984e9f0a2557af605e73cdefb1b Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Mon, 6 Jun 2022 12:59:59 -0700 Subject: [PATCH] Add Operations to GetAzureFunctions request (#1527) * Add Operations to GetAzureFunctions request * add comment * comments --- .../AzureFunctions/AzureFunctionsUtils.cs | 64 ++++++++++++++++--- .../Contracts/GetAzureFunctionsRequest.cs | 27 ++++++-- .../GetAzureFunctionsOperation.cs | 5 +- .../AzureFunctionsOperations.cs | 61 ++++++++++++++++++ .../AzureFunctionsRoute.cs | 9 +++ .../AzureFunctionsServiceTests.cs | 45 ++++++++++--- 6 files changed, 188 insertions(+), 23 deletions(-) create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsOperations.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AzureFunctionsUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AzureFunctionsUtils.cs index e0f0b223..3cd81036 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AzureFunctionsUtils.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/AzureFunctionsUtils.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts; using System; using System.Collections.Generic; using System.Linq; @@ -40,11 +41,23 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions } /// - /// Gets the route from an HttpTrigger attribute if specified + /// Gets the info for a HttpTriggerBinding for the specified method (if such a binding exists) /// /// The method - /// The name of the route, or null if no route is specified (or there isn't an HttpTrigger binding) - public static string? GetHttpRoute(this MethodDeclarationSyntax m) + /// The HttpTriggerBinding, or null if none exists + public static HttpTriggerBinding? GetHttpTriggerBinding(this MethodDeclarationSyntax m) + { + var httpTriggerAttribute = m.GetHttpTriggerAttribute(); + return httpTriggerAttribute == null ? null : new HttpTriggerBinding(httpTriggerAttribute.GetHttpRoute(), httpTriggerAttribute.GetHttpOperations()); + } + + /// + /// Gets the HttpTrigger attribute on the parameters for this method if one exists + /// https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger + /// + /// The method + /// The attribute if such a binding exists + public static AttributeSyntax? GetHttpTriggerAttribute(this MethodDeclarationSyntax m) { return m .ParameterList @@ -53,8 +66,18 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions p.AttributeLists // Get a list of all attributes on any of the parameters .SelectMany(al => al.Attributes) ).Where(a => a.Name.ToString().Equals(HTTP_TRIGGER_ATTRIBUTE_NAME) // Find any HttpTrigger attributes - ).FirstOrDefault() // Get the first one available - there should only ever be 0 or 1 - ?.ArgumentList // Get all the arguments for the attribute + ).FirstOrDefault(); // Get the first one available - there should only ever be 0 or 1 + } + + /// + /// Gets the route from the HttpTrigger attribute + /// https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=in-process%2Cfunctionsv2&pivots=programming-language-csharp#attributes + /// + /// The attribute + /// The name of the route, or null if no route is specified (or there isn't an HttpTrigger binding) + public static string? GetHttpRoute(this AttributeSyntax a) + { + return a.ArgumentList ?.Arguments .Where(a => a.ChildNodes().OfType().Where(nes => nes.Name.ToString().Equals(ROUTE_ARGUMENT_NAME)).Any()) // Find the Route argument - it should always be a named argument .FirstOrDefault() @@ -63,8 +86,22 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions .Where(le => le.Kind() != SyntaxKind.NullLiteralExpression) // Skip the null expressions so they aren't ToString()'d into "null" .FirstOrDefault() ?.ToString() - .TrimStart('$') // Remove $ from interpolated string, since this will always be outside the quotes we can just trim - .Trim('\"'); // Trim off the quotes from the string value - additional quotes at the beginning and end aren't valid syntax so it's fine to trim + .TrimStringQuotes(); + } + + /// + /// Get the operations (methods) on an HttpTrigger attribute. These are string params arguments to the attribute + /// https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=in-process%2Cfunctionsv2&pivots=programming-language-csharp#attributes + /// + /// The attribute + /// The operations (methods) specified + public static string[]? GetHttpOperations(this AttributeSyntax a) + { + return a.ArgumentList + ?.Arguments + .Where(a => a.Expression.Kind() == SyntaxKind.StringLiteralExpression && a.NameEquals == null) // Operations are string literals who don't have a name (Route is always a named param) + .Select(a => a.ToString().TrimStringQuotes().ToUpper()) // upper case for consistent naming + .ToArray(); } /// @@ -90,8 +127,19 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions ?.Arguments .FirstOrDefault() // The first argument is the function name ?.ToString() + .TrimStringQuotes() ?? ""; + } + + /// + /// Removes the quotes (and $ for interpolated strings) around a string literal + /// + /// The string + /// The string without quotes + public static string TrimStringQuotes(this string s) + { + return s .TrimStart('$') // Remove $ from interpolated string, since this will always be outside the quotes we can just trim - .Trim('\"') ?? ""; // Trim off the quotes from the string value - additional quotes at the beginning and end aren't valid syntax so it's fine to trim + .Trim('\"'); // Trim off the quotes from the string value - additional quotes at the beginning and end aren't valid syntax so it's fine to trim } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/Contracts/GetAzureFunctionsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/Contracts/GetAzureFunctionsRequest.cs index 839ded38..7883ca23 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/Contracts/GetAzureFunctionsRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/Contracts/GetAzureFunctionsRequest.cs @@ -22,17 +22,36 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts /// /// The name of the function /// - public string Name { get; set; } + public string Name { get; } /// - /// The route of the HttpTrigger binding if one exists on this function + /// The HttpTrigger binding if one is specified /// - public string? Route { get; set; } + public HttpTriggerBinding? HttpTriggerBinding { get; } - public AzureFunction(string name, string? route) + public AzureFunction(string name, HttpTriggerBinding? httpTriggerBinding) { this.Name = name; + this.HttpTriggerBinding = httpTriggerBinding; + } + } + + public class HttpTriggerBinding + { + /// + /// The route specified + /// + public string? Route { get; } + + /// + /// The operations (methods) specified + /// + public string[]? Operations { get; } + + public HttpTriggerBinding(string? route, string[]? operations) + { this.Route = route; + this.Operations = operations; } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/GetAzureFunctionsOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/GetAzureFunctionsOperation.cs index 4cf46835..a1c8d60a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/GetAzureFunctionsOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/AzureFunctions/GetAzureFunctionsOperation.cs @@ -45,7 +45,10 @@ namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions // get all the method declarations with the FunctionName attribute IEnumerable methodsWithFunctionAttributes = AzureFunctionsUtils.GetMethodsWithFunctionAttributes(root); - var azureFunctions = methodsWithFunctionAttributes.Select(m => new AzureFunction(m.GetFunctionName(), m.GetHttpRoute())).ToArray(); + var azureFunctions = methodsWithFunctionAttributes.Select(m => new AzureFunction( + m.GetFunctionName(), + m.GetHttpTriggerBinding())) + .ToArray(); return new GetAzureFunctionsResult(azureFunctions); } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsOperations.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsOperations.cs new file mode 100644 index 00000000..fd57ba48 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsOperations.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Company.Namespace.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; + +namespace Company.Namespace +{ + public class AzureFunctionsRoute + { + /// + /// Tests binding with a single operation and a route specified + /// + [FunctionName("SingleWithRoute")] + public IActionResult SingleWithRoute([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "route")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable result) + { + throw new NotImplementedException(); + } + + /// + /// Tests binding with multiple operations and a route specified + /// + [FunctionName("MultipleWithRoute")] + public IActionResult MultipleWithRoute([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "route")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable result) + { + throw new NotImplementedException(); + } + + /// + /// Tests binding with no operations and a route specified + /// + [FunctionName("NoOperationsWithRoute")] + public IActionResult MultipleWithRoute([HttpTrigger(AuthorizationLevel.Anonymous, Route = "route")] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable result) + { + throw new NotImplementedException(); + } + + /// + /// Tests binding with no operations and no route + /// + [FunctionName("NoOperationsNoRoute")] + public IActionResult MultipleWithRoute([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req, [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable result) + { + throw new NotImplementedException(); + } + + /// + /// Tests binding without an HttpBinding + /// + [FunctionName("NoHttpBinding")] + public IActionResult WithRoute([Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable result) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsRoute.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsRoute.cs index d712dd68..c01a7286 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsRoute.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionTestFiles/AzureFunctionsRoute.cs @@ -85,5 +85,14 @@ namespace Company.Namespace { throw new NotImplementedException(); } + + /// + /// Tests binding without an HttpBinding + /// + [FunctionName("NoHttpBinding")] + public IActionResult WithRoute([Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable result) + { + throw new NotImplementedException(); + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionsServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionsServiceTests.cs index 94b89522..e94e19a2 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionsServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/AzureFunctions/AzureFunctionsServiceTests.cs @@ -228,7 +228,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions } /// - /// Verify getting the routes of Azure Functions in a file + /// Verify getting the routes of a HttpTriggerBinding on Azure Functions in a file /// [Test] public void GetAzureFunctionsRoute() @@ -243,15 +243,40 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.AzureFunctions GetAzureFunctionsOperation operation = new GetAzureFunctionsOperation(parameters); GetAzureFunctionsResult result = operation.GetAzureFunctions(); - Assert.That(result.AzureFunctions.Length, Is.EqualTo(8)); - Assert.That(result.AzureFunctions[0].Route, Is.EqualTo("withRoute")); - Assert.That(result.AzureFunctions[1].Route, Is.EqualTo("{interpolated}String")); - Assert.That(result.AzureFunctions[2].Route, Is.EqualTo("$withDollarSigns$")); - Assert.That(result.AzureFunctions[3].Route, Is.EqualTo("withRouteNoSpaces")); - Assert.That(result.AzureFunctions[4].Route, Is.EqualTo("withRouteExtraSpaces")); - Assert.That(result.AzureFunctions[5].Route, Is.Null, "Route specified as null should be null"); - Assert.That(result.AzureFunctions[6].Route, Is.Null, "No route specified should be null"); - Assert.That(result.AzureFunctions[7].Route, Is.EqualTo("")); + Assert.That(result.AzureFunctions.Length, Is.EqualTo(9)); + Assert.That(result.AzureFunctions[0].HttpTriggerBinding!.Route, Is.EqualTo("withRoute")); + Assert.That(result.AzureFunctions[1].HttpTriggerBinding!.Route, Is.EqualTo("{interpolated}String")); + Assert.That(result.AzureFunctions[2].HttpTriggerBinding!.Route, Is.EqualTo("$withDollarSigns$")); + Assert.That(result.AzureFunctions[3].HttpTriggerBinding!.Route, Is.EqualTo("withRouteNoSpaces")); + Assert.That(result.AzureFunctions[4].HttpTriggerBinding!.Route, Is.EqualTo("withRouteExtraSpaces")); + Assert.That(result.AzureFunctions[5].HttpTriggerBinding!.Route, Is.Null, "Route specified as null should be null"); + Assert.That(result.AzureFunctions[6].HttpTriggerBinding!.Route, Is.Null, "No route specified should be null"); + Assert.That(result.AzureFunctions[7].HttpTriggerBinding!.Route, Is.EqualTo("")); + Assert.That(result.AzureFunctions[8].HttpTriggerBinding, Is.Null, "Should not be an HttpTriggerBinding"); + } + + /// + /// Verify getting the operations of a HttpTriggerBinding on Azure Functions in a file + /// + [Test] + public void GetAzureFunctionsOperations() + { + string testFile = Path.Join(testAzureFunctionsFolder, "AzureFunctionsOperations.cs"); + + GetAzureFunctionsParams parameters = new GetAzureFunctionsParams + { + FilePath = testFile + }; + + GetAzureFunctionsOperation operation = new GetAzureFunctionsOperation(parameters); + GetAzureFunctionsResult result = operation.GetAzureFunctions(); + + Assert.That(result.AzureFunctions.Length, Is.EqualTo(5)); + Assert.That(result.AzureFunctions[0].HttpTriggerBinding!.Operations, Is.EqualTo(new string[] { "GET" })); + Assert.That(result.AzureFunctions[1].HttpTriggerBinding!.Operations, Is.EqualTo(new string[] { "GET", "POST" })); + Assert.That(result.AzureFunctions[2].HttpTriggerBinding!.Operations, Is.EqualTo(Array.Empty())); + Assert.That(result.AzureFunctions[3].HttpTriggerBinding!.Operations, Is.EqualTo(Array.Empty())); + Assert.That(result.AzureFunctions[4].HttpTriggerBinding, Is.Null, "Should not be an HttpTriggerBinding"); } } }