Skip to content

Commit

Permalink
Include all bindings when generating function metadata (#542)
Browse files Browse the repository at this point in the history
* Adding all bindings to function.json

* Fixed casing of direction attribute

* Update assembly version

* Removed Array allocation

* sFixed to address PR comments

* Bumped minor version of package.
  • Loading branch information
kshyju committed Feb 9, 2022
1 parent eedc6cc commit 2a5e618
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 23 deletions.
4 changes: 2 additions & 2 deletions common.props
Expand Up @@ -3,8 +3,8 @@
<ContinuousIntegrationBuild Condition="'$(TF_BUILD)' == 'true'">true</ContinuousIntegrationBuild>

<MajorProductVersion>4</MajorProductVersion>
<MinorProductVersion>0</MinorProductVersion>
<PatchProductVersion>1</PatchProductVersion>
<MinorProductVersion>1</MinorProductVersion>
<PatchProductVersion>0</PatchProductVersion>

<!-- Clear this value for non-preview releases -->
<PreviewProductVersion></PreviewProductVersion>
Expand Down
50 changes: 43 additions & 7 deletions src/Microsoft.NET.Sdk.Functions.Generator/MethodInfoExtensions.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
Expand Down Expand Up @@ -59,15 +60,20 @@ public static JObject ManualTriggerBinding(this MethodDefinition method)
/// <returns><see cref="FunctionJsonSchema"/> object that represents the passed in <paramref name="method"/>.</returns>
public static FunctionJsonSchema ToFunctionJson(this MethodDefinition method, string assemblyPath)
{
// For every SDK parameter, convert it to a FunctionJson bindings.
// Every parameter can potentially contain more than 1 attribute that will be converted into a binding object.
var bindingsFromParameters = method.HasNoAutomaticTriggerAttribute() ? new[] { method.ManualTriggerBinding() } : method.Parameters
.Select(p => p.ToFunctionJsonBindings())
.SelectMany(i => i);

// Get binding if a return attribute is used.
// Ex: [return: Queue("myqueue-items-a", Connection = "MyStorageConnStr")]
var returnBindings = GetOutputBindingsFromReturnAttribute(method);
var allBindings = bindingsFromParameters.Concat(returnBindings).ToArray();

return new FunctionJsonSchema
{
// For every SDK parameter, convert it to a FunctionJson bindings.
// Every parameter can potentially contain more than 1 attribute that will be converted into a binding object.
Bindings = method.HasNoAutomaticTriggerAttribute() ? new[] { method.ManualTriggerBinding() } : method.Parameters
.Where(p => p.IsWebJobSdkTriggerParameter())
.Select(p => p.ToFunctionJsonBindings())
.SelectMany(i => i)
.ToArray(),
Bindings = allBindings,
// Entry point is the fully qualified name of the function
EntryPoint = $"{method.DeclaringType.FullName}.{method.Name}",
ScriptFile = assemblyPath,
Expand All @@ -77,6 +83,36 @@ public static FunctionJsonSchema ToFunctionJson(this MethodDefinition method, st
};
}

/// <summary>
/// Gets bindings from return expression used with a binding expression.
/// Ex:
/// [FunctionName("HttpTriggerWriteToQueue1")]
/// [return: Queue("myqueue-items-a", Connection = "MyStorageConnStra")]
/// public static string Run([HttpTrigger] HttpRequestMessage request) => "foo";
/// </summary>
private static JObject[] GetOutputBindingsFromReturnAttribute(MethodDefinition method)
{
if (method.MethodReturnType == null)
{
return Array.Empty<JObject>();
}

var outputBindings = new List<JObject>();
foreach (var attribute in method.MethodReturnType.CustomAttributes.Where(a=>a.IsWebJobsAttribute()))
{
var bindingJObject = attribute.ToReflection().ToJObject();

// return binding must have the direction attribute set to out.
// https://github.com/Azure/azure-functions-host/blob/dev/src/WebJobs.Script/Utility.cs#L561
bindingJObject["name"] = "$return";
bindingJObject["direction"] = "out";

outputBindings.Add(bindingJObject);
}

return outputBindings.ToArray();
}

/// <summary>
/// Gets a function name from a <paramref name="method"/>
/// </summary>
Expand Down
@@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand All @@ -8,6 +10,7 @@
using Microsoft.Azure.EventHubs;
using Microsoft.Azure.WebJobs;
using Microsoft.WindowsAzure.Storage.Queue;
using Newtonsoft.Json.Linq;
using Xunit;

namespace Microsoft.NET.Sdk.Functions.Test
Expand All @@ -24,6 +27,26 @@ public class FunctionsClass
[FunctionName("MyHttpTrigger")]
public static void Run1([HttpTrigger] HttpRequestMessage request) { }

[FunctionName("HttpTriggerQueueReturn")]
[return: Queue("myqueue-items-a", Connection = "MyStorageConnStrA")]
public static string HttpTriggerQueueReturn([HttpTrigger] HttpRequestMessage request) => "foo";

[FunctionName("HttpTriggerQueueOutParam")]
public static void HttpTriggerQueueOutParam([HttpTrigger] HttpRequestMessage request,
[Queue("myqueue-items-b", Connection = "MyStorageConnStrB")] out string msg)
{
msg = "foo";
}

[FunctionName("HttpTriggerMultipleOutputs")]
public static void HttpTriggerMultipleOutputs([HttpTrigger] HttpRequestMessage request,
[Blob("binding-metric-test/sample-text.txt", Connection = "MyStorageConnStrC")] out string myBlob,
[Queue("myqueue-items-c", Connection = "MyStorageConnStrC")] IAsyncCollector<string> qCollector)
{
myBlob = "foo-blob";
qCollector.AddAsync("foo-queue");
}

[FunctionName("MyBlobTrigger")]
public static void Run2([BlobTrigger("blob.txt")] string blobContent) { }

Expand All @@ -46,24 +69,237 @@ public class FunctionsClass
public static void Run8() { }
}

public class BindingAssertionItem
{
public string FunctionName { set; get; }

public Dictionary<string, string>[] Bindings { set; get; }
}

public class BindingTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyHttpTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "httpTrigger" },
{"name" , "request"},
{"authLevel" , "function"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="HttpTriggerQueueReturn",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "httpTrigger" },
{"name" , "request"},
{"authLevel" , "function"}
},
new Dictionary<string, string>
{
{"type", "queue" },
{"name" , "$return"},
{"connection" , "MyStorageConnStrA"},
{"queueName","myqueue-items-a" }
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="HttpTriggerQueueOutParam",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "httpTrigger" },
{"name" , "request"},
{"authLevel" , "function"}
},
new Dictionary<string, string>
{
{"type", "queue" },
{"name" , "msg"},
{"connection" , "MyStorageConnStrB"},
{"queueName","myqueue-items-b" }
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="HttpTriggerMultipleOutputs",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "httpTrigger" },
{"name" , "request"},
{"authLevel" , "function"}
},
new Dictionary<string, string>
{
{"type", "queue" },
{"name" , "qCollector"},
{"connection" , "MyStorageConnStrC"},
{"queueName","myqueue-items-c" }
},
new Dictionary<string, string>
{
{"type", "blob" },
{"name" , "myBlob"},
{"blobPath", "binding-metric-test/sample-text.txt" },
{"connection" , "MyStorageConnStrC"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyBlobTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "blobTrigger" },
{"name" , "blobContent"},
{"path" , "blob.txt"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyEventHubTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "eventHubTrigger" },
{"name" , "message"},
{"eventHubName" , "hub"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyTimerTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "timerTrigger" },
{"name" , "timer"},
{"schedule" , "00:30:00"},
{"useMonitor" , "True"},
{"runOnStartup" , "False"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyServiceBusTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "serviceBusTrigger" },
{"name" , "message"},
{"queueName" , "queue"},
{"isSessionsEnabled" , "False"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyManualTrigger",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "manualTrigger" },
{"name" , "input"}
}
}
}
};

yield return new object[] {
new BindingAssertionItem
{
FunctionName="MyManualTriggerWithoutParameters",
Bindings = new Dictionary<string, string>[]
{
new Dictionary<string, string>
{
{"type", "manualTrigger" }
}
}
}
};
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

[Theory]
[InlineData("MyHttpTrigger", "httpTrigger", "request")]
[InlineData("MyBlobTrigger", "blobTrigger", "blobContent")]
[InlineData("MyQueueTrigger", "queueTrigger", "queue")]
[InlineData("MyEventHubTrigger", "eventHubTrigger", "message")]
[InlineData("MyTimerTrigger", "timerTrigger", "timer")]
[InlineData("MyServiceBusTrigger", "serviceBusTrigger", "message")]
[InlineData("MyManualTrigger", "manualTrigger", "input")]
[InlineData("MyManualTriggerWithoutParameters", "manualTrigger", null)]
public void FunctionMethodsAreExported(string functionName, string type, string parameterName)
[ClassData(typeof(BindingTestData))]
public void FunctionMethodsAreExported(BindingAssertionItem item)
{
var logger = new RecorderLogger();
var converter = new FunctionJsonConverter(logger, ".", ".", functionsInDependencies: false);
var functions = converter.GenerateFunctions(new[] { TestUtility.GetTypeDefinition(typeof(FunctionsClass)) });
var schema = functions.Single(e => Path.GetFileName(e.Value.outputFile.DirectoryName) == functionName).Value.schema;
var binding = schema.Bindings.Single();
binding.Value<string>("type").Should().Be(type);
binding.Value<string>("name").Should().Be(parameterName);
var functions = converter.GenerateFunctions(new[] { TestUtility.GetTypeDefinition(typeof(FunctionsClass)) }).ToArray();
var schemaActual = functions.Single(e => Path.GetFileName(e.Value.outputFile.DirectoryName) == item.FunctionName).Value.schema;

foreach (var expectedBindingItem in item.Bindings)
{
var expectedBindingType = expectedBindingItem.FirstOrDefault(a => a.Key == "type");

// query binding entry from actual using the type.
var matchingBindingFromActual = schemaActual.Bindings
.First(a => a.Properties().Any(g => g.Name == "type"
&& g.Value.ToString()== expectedBindingType.Value));

// compare all props of binding entry from expected entry with actual.
foreach (var prop in expectedBindingItem)
{
// make sure the prop exist in the binding.
matchingBindingFromActual.ContainsKey(prop.Key).Should().BeTrue();

// Verify the prop values matches between expected and actual.
expectedBindingItem[prop.Key].Should().Be(matchingBindingFromActual[prop.Key].ToString());
}
}

logger.Errors.Should().BeEmpty();
logger.Warnings.Should().BeEmpty();
}
Expand Down

0 comments on commit 2a5e618

Please sign in to comment.