Add support for $return in the output binding. Closes #13
This commit is contained in:
Родитель
7aac6244ce
Коммит
d5ce994302
|
@ -1,6 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.NET.Sdk.Functions.MakeFunction;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MakeFunctionJson
|
||||
{
|
||||
|
@ -18,5 +22,235 @@ namespace MakeFunctionJson
|
|||
name = name.Substring(0, name.Length - suffix.Length);
|
||||
return name.ToLowerFirstCharacter();
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> _supportedAttributes = new HashSet<string>
|
||||
{
|
||||
// These 2 attributes are not handled currently.
|
||||
// They can go either on class, method, or parameter.
|
||||
// The code flow now assumes 1:1 mapping of attributes on parameters to function.json binding.
|
||||
// "StorageAccountAttribute",
|
||||
// "ServiceBusAccountAttribute",
|
||||
|
||||
"BlobAttribute",
|
||||
"BlobTriggerAttribute",
|
||||
"QueueAttribute",
|
||||
"QueueTriggerAttribute",
|
||||
"TableAttribute",
|
||||
"EventHubAttribute",
|
||||
"EventHubTriggerAttribute",
|
||||
"TimerTriggerAttribute",
|
||||
"DocumentDBAttribute",
|
||||
"ApiHubTableAttribute",
|
||||
"MobileTableAttribute",
|
||||
"ServiceBusTriggerAttribute",
|
||||
"ServiceBusAttribute",
|
||||
"TwilioSmsAttribute",
|
||||
"NotificationHubAttribute",
|
||||
"HttpTriggerAttribute"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="attribute"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsWebJobsAttribute(this Attribute attribute)
|
||||
{
|
||||
return _supportedAttributes.Contains(attribute.GetType().Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For every binding (which is what the returned JObject represents) there are 3 special keys:
|
||||
/// "name" -> that is the parameter name, not set by this function
|
||||
/// "type" -> that is the binding type. This is derived from the Attribute.Name itself. <see cref="AttributeExtensions.ToAttributeFriendlyName(Attribute)"/>
|
||||
/// "direction" -> default is 'out'
|
||||
/// if the binding is "Trigger", then it's an in
|
||||
/// if the binding Attribute has a FileAccess property on it, then map it to that.
|
||||
/// a side from these 3, all the others are direct serialization of all of the attribute's properties.
|
||||
/// The mapping however isn't 1:1 in terms of the naming. Therefore, <see cref="NormalizePropertyName(string, PropertyInfo)"/>
|
||||
/// </summary>
|
||||
/// <param name="attribute"></param>
|
||||
/// <returns></returns>
|
||||
public static JObject ToJObject(this Attribute attribute)
|
||||
{
|
||||
var obj = new JObject
|
||||
{
|
||||
// the friendly name is basically the name without 'Attribute' suffix and lowerCase first Char.
|
||||
["type"] = attribute.ToAttributeFriendlyName()
|
||||
};
|
||||
|
||||
// Default value is out
|
||||
var direction = Direction.@out;
|
||||
if (obj["type"].ToString().IndexOf("Trigger") > 0)
|
||||
{
|
||||
// if binding.type is trigger, then it's 'in'
|
||||
direction = Direction.@in;
|
||||
}
|
||||
|
||||
foreach (var property in attribute
|
||||
.GetType()
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.PropertyType != typeof(System.Object)))
|
||||
{
|
||||
var propertyValue = property.GetValue(attribute);
|
||||
|
||||
if (propertyValue == null || (propertyValue is int && (int)propertyValue == 0))
|
||||
{
|
||||
// Don't serialize null properties and int properties for some reason.
|
||||
// the int handling logic was copied from Mike's > "Table.Take is not nullable. So 0 means ignore"
|
||||
continue;
|
||||
}
|
||||
|
||||
var propertyType = property.PropertyType;
|
||||
#if NET46
|
||||
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
#else
|
||||
if (propertyType.GetTypeInfo().IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
#endif
|
||||
{
|
||||
// Unwrap nullable types to their underlying type.
|
||||
propertyType = Nullable.GetUnderlyingType(propertyType);
|
||||
}
|
||||
|
||||
// What about other Enums?
|
||||
if (propertyType == typeof(FileAccess))
|
||||
{
|
||||
// FileAccess on the Attribute dictates the "direction" property.
|
||||
Direction convert(FileAccess value)
|
||||
{
|
||||
if (value == FileAccess.Read)
|
||||
{
|
||||
return Direction.@in;
|
||||
}
|
||||
else if (value == FileAccess.Write)
|
||||
{
|
||||
return Direction.@out;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Direction.inout;
|
||||
}
|
||||
}
|
||||
direction = convert((FileAccess)propertyValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if property is supported.
|
||||
CheckIfPropertyIsSupported(attribute.GetType().Name, property);
|
||||
|
||||
// Normalize and store the propertyName
|
||||
var propertyName = NormalizePropertyName(attribute.GetType().Name, property);
|
||||
if (TryGetPropertyValue(property, propertyValue, out string jsonValue))
|
||||
{
|
||||
obj[propertyName] = jsonValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
obj[propertyName] = JToken.FromObject(propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the direction
|
||||
obj["direction"] = direction.ToString();
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static bool TryGetPropertyValue(PropertyInfo property, object propertyValue, out string value)
|
||||
{
|
||||
value = null;
|
||||
if (property.PropertyType.FullName == "Microsoft.ServiceBus.Messaging.AccessRights")
|
||||
{
|
||||
value = Enum.GetName(property.PropertyType, propertyValue).ToLowerFirstCharacter();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void CheckIfPropertyIsSupported(string attributeName, PropertyInfo property)
|
||||
{
|
||||
var propertyName = property.Name;
|
||||
if (attributeName == "TimerTriggerAttribute")
|
||||
{
|
||||
if (propertyName == "ScheduleType")
|
||||
{
|
||||
throw new NotImplementedException($"Property '{propertyName}' on attribute '{attributeName}' is not supported in Azure Functions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// These exceptions are coming from how the script runtime is reading function.json
|
||||
/// See https://github.com/Azure/azure-webjobs-sdk-script/tree/dev/src/WebJobs.Script/Binding
|
||||
/// If there are no exceptions for a given property name on a given attribute, then return it's name with a lowerCase first character.
|
||||
/// </summary>
|
||||
/// <param name="attributeName"></param>
|
||||
/// <param name="property"></param>
|
||||
/// <returns></returns>
|
||||
private static string NormalizePropertyName(string attributeName, PropertyInfo property)
|
||||
{
|
||||
var propertyName = property.Name;
|
||||
|
||||
if ((attributeName == "BlobAttribute") || (attributeName == "BlobTriggerAttribute"))
|
||||
{
|
||||
if (propertyName == "BlobPath")
|
||||
{
|
||||
return "path";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "MobileTableAttribute")
|
||||
{
|
||||
if (propertyName == "MobileAppUriSetting")
|
||||
{
|
||||
return "connection";
|
||||
}
|
||||
else if (propertyName == "ApiKeySetting")
|
||||
{
|
||||
return "apiKey";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "NotificationHubAttribute")
|
||||
{
|
||||
if (propertyName == "ConnectionStringSetting")
|
||||
{
|
||||
return "connection";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "ServiceBusAttribute")
|
||||
{
|
||||
if (propertyName == "QueueOrTopicName")
|
||||
{
|
||||
// The attribute has a QueueOrTopicName while function.json has distinct queue and topic.
|
||||
// I just picked queue.
|
||||
return "queue";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "TwilioSmsAttribute")
|
||||
{
|
||||
if (propertyName == "AccountSidSetting")
|
||||
{
|
||||
return "accountSid";
|
||||
}
|
||||
else if (propertyName == "AuthTokenSetting")
|
||||
{
|
||||
return "authToken";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "TimerTriggerAttribute")
|
||||
{
|
||||
if (propertyName == "ScheduleExpression")
|
||||
{
|
||||
return "schedule";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "EventHubTriggerAttribute")
|
||||
{
|
||||
if (propertyName == "EventHubName")
|
||||
{
|
||||
return "path";
|
||||
}
|
||||
}
|
||||
|
||||
return propertyName.ToLowerFirstCharacter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,11 +25,26 @@ namespace MakeFunctionJson
|
|||
/// <returns><see cref="FunctionJsonSchema"/> object that represents the passed in <paramref name="method"/>.</returns>
|
||||
public static FunctionJsonSchema ToFunctionJson(this MethodInfo method, string assemblyPath)
|
||||
{
|
||||
var bindings = method.GetParameters()
|
||||
.Where(p => p.IsWebJobsSdkParameter())
|
||||
.Select(p => p.ToFunctionJsonBindings())
|
||||
.SelectMany(i => i);
|
||||
var outputBindings = method
|
||||
.ReturnTypeCustomAttributes
|
||||
.GetCustomAttributes(false)
|
||||
.Cast<Attribute>()
|
||||
.Where(a => a.IsWebJobsAttribute())
|
||||
.Select(a => a.ToJObject())
|
||||
.Select(a =>
|
||||
{
|
||||
a["name"] = "$return";
|
||||
return a;
|
||||
});
|
||||
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.GetParameters().Where(p => p.IsWebJobsSdkParameter()).Select(p => p.ToFunctionJsonBindings()).SelectMany(i => i),
|
||||
Bindings = bindings.Concat(outputBindings),
|
||||
// Entry point is the fully qualified name of the function
|
||||
EntryPoint = $"{method.DeclaringType.FullName}.{method.Name}",
|
||||
// scriptFile == assemblyPath.
|
||||
|
|
|
@ -1,41 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.NET.Sdk.Functions.MakeFunction;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MakeFunctionJson
|
||||
{
|
||||
internal static class ParameterInfoExtensions
|
||||
{
|
||||
private static readonly HashSet<string> _supportedAttributes = new HashSet<string>
|
||||
{
|
||||
// These 2 attributes are not handled currently.
|
||||
// They can go either on class, method, or parameter.
|
||||
// The code flow now assumes 1:1 mapping of attributes on parameters to function.json binding.
|
||||
// "StorageAccountAttribute",
|
||||
// "ServiceBusAccountAttribute",
|
||||
|
||||
"BlobAttribute",
|
||||
"BlobTriggerAttribute",
|
||||
"QueueAttribute",
|
||||
"QueueTriggerAttribute",
|
||||
"TableAttribute",
|
||||
"EventHubAttribute",
|
||||
"EventHubTriggerAttribute",
|
||||
"TimerTriggerAttribute",
|
||||
"DocumentDBAttribute",
|
||||
"ApiHubTableAttribute",
|
||||
"MobileTableAttribute",
|
||||
"ServiceBusTriggerAttribute",
|
||||
"ServiceBusAttribute",
|
||||
"TwilioSmsAttribute",
|
||||
"NotificationHubAttribute",
|
||||
"HttpTriggerAttribute"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A parameter is an SDK parameter if it has at lease 1 SDK attribute.
|
||||
/// </summary>
|
||||
|
@ -45,7 +16,7 @@ namespace MakeFunctionJson
|
|||
{
|
||||
return parameterInfo
|
||||
.GetCustomAttributes()
|
||||
.Any(a => _supportedAttributes.Contains(a.GetType().Name));
|
||||
.Any(a => a.IsWebJobsAttribute());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -58,8 +29,8 @@ namespace MakeFunctionJson
|
|||
|
||||
var bindings = parameterInfo
|
||||
.GetCustomAttributes()
|
||||
.Where(a => _supportedAttributes.Contains(a.GetType().Name)) // this has to return at least 1.
|
||||
.Select(AttributeToJObject) // Convert the Attribute into a JObject.
|
||||
.Where(a => a.IsWebJobsAttribute()) // this has to return at least 1.
|
||||
.Select(a => a.ToJObject()) // Convert the Attribute into a JObject.
|
||||
.Select(obj =>
|
||||
{
|
||||
// Add a name property on the JObject that refers to the parameter name.
|
||||
|
@ -78,199 +49,5 @@ namespace MakeFunctionJson
|
|||
return bindings;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For every binding (which is what the returned JObject represents) there are 3 special keys:
|
||||
/// "name" -> that is the parameter name, not set by this function
|
||||
/// "type" -> that is the binding type. This is derived from the Attribute.Name itself. <see cref="AttributeExtensions.ToAttributeFriendlyName(Attribute)"/>
|
||||
/// "direction" -> default is 'out'
|
||||
/// if the binding is "Trigger", then it's an in
|
||||
/// if the binding Attribute has a FileAccess property on it, then map it to that.
|
||||
/// a side from these 3, all the others are direct serialization of all of the attribute's properties.
|
||||
/// The mapping however isn't 1:1 in terms of the naming. Therefore, <see cref="NormalizePropertyName(string, PropertyInfo)"/>
|
||||
/// </summary>
|
||||
/// <param name="attribute"></param>
|
||||
/// <returns></returns>
|
||||
private static JObject AttributeToJObject(Attribute attribute)
|
||||
{
|
||||
var obj = new JObject
|
||||
{
|
||||
// the friendly name is basically the name without 'Attribute' suffix and lowerCase first Char.
|
||||
["type"] = attribute.ToAttributeFriendlyName()
|
||||
};
|
||||
|
||||
// Default value is out
|
||||
var direction = Direction.@out;
|
||||
if (obj["type"].ToString().IndexOf("Trigger") > 0)
|
||||
{
|
||||
// if binding.type is trigger, then it's 'in'
|
||||
direction = Direction.@in;
|
||||
}
|
||||
|
||||
foreach (var property in attribute
|
||||
.GetType()
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.PropertyType != typeof(System.Object)))
|
||||
{
|
||||
var propertyValue = property.GetValue(attribute);
|
||||
|
||||
if (propertyValue == null || (propertyValue is int && (int)propertyValue == 0))
|
||||
{
|
||||
// Don't serialize null properties and int properties for some reason.
|
||||
// the int handling logic was copied from Mike's > "Table.Take is not nullable. So 0 means ignore"
|
||||
continue;
|
||||
}
|
||||
|
||||
var propertyType = property.PropertyType;
|
||||
#if NET46
|
||||
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
#else
|
||||
if (propertyType.GetTypeInfo().IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
#endif
|
||||
{
|
||||
// Unwrap nullable types to their underlying type.
|
||||
propertyType = Nullable.GetUnderlyingType(propertyType);
|
||||
}
|
||||
|
||||
// What about other Enums?
|
||||
if (propertyType == typeof(FileAccess))
|
||||
{
|
||||
// FileAccess on the Attribute dictates the "direction" property.
|
||||
Direction convert(FileAccess value)
|
||||
{
|
||||
if (value == FileAccess.Read)
|
||||
{
|
||||
return Direction.@in;
|
||||
}
|
||||
else if (value == FileAccess.Write)
|
||||
{
|
||||
return Direction.@out;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Direction.inout;
|
||||
}
|
||||
}
|
||||
direction = convert((FileAccess)propertyValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if property is supported.
|
||||
CheckIfPropertyIsSupported(attribute.GetType().Name, property);
|
||||
|
||||
// Normalize and store the propertyName
|
||||
var propertyName = NormalizePropertyName(attribute.GetType().Name, property);
|
||||
if (TryGetPropertyValue(property, propertyValue, out string jsonValue))
|
||||
{
|
||||
obj[propertyName] = jsonValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
obj[propertyName] = JToken.FromObject(propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the direction
|
||||
obj["direction"] = direction.ToString();
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static bool TryGetPropertyValue(PropertyInfo property, object propertyValue, out string value)
|
||||
{
|
||||
value = null;
|
||||
if (property.PropertyType.FullName == "Microsoft.ServiceBus.Messaging.AccessRights")
|
||||
{
|
||||
value = Enum.GetName(property.PropertyType, propertyValue).ToLowerFirstCharacter();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void CheckIfPropertyIsSupported(string attributeName, PropertyInfo property)
|
||||
{
|
||||
var propertyName = property.Name;
|
||||
if (attributeName == "TimerTriggerAttribute")
|
||||
{
|
||||
if (propertyName == "ScheduleType")
|
||||
{
|
||||
throw new NotImplementedException($"Property '{propertyName}' on attribute '{attributeName}' is not supported in Azure Functions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// These exceptions are coming from how the script runtime is reading function.json
|
||||
/// See https://github.com/Azure/azure-webjobs-sdk-script/tree/dev/src/WebJobs.Script/Binding
|
||||
/// If there are no exceptions for a given property name on a given attribute, then return it's name with a lowerCase first character.
|
||||
/// </summary>
|
||||
/// <param name="attributeName"></param>
|
||||
/// <param name="property"></param>
|
||||
/// <returns></returns>
|
||||
private static string NormalizePropertyName(string attributeName, PropertyInfo property)
|
||||
{
|
||||
var propertyName = property.Name;
|
||||
|
||||
if ((attributeName == "BlobAttribute") || (attributeName == "BlobTriggerAttribute"))
|
||||
{
|
||||
if (propertyName == "BlobPath")
|
||||
{
|
||||
return "path";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "MobileTableAttribute")
|
||||
{
|
||||
if (propertyName == "MobileAppUriSetting")
|
||||
{
|
||||
return "connection";
|
||||
}
|
||||
else if (propertyName == "ApiKeySetting")
|
||||
{
|
||||
return "apiKey";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "NotificationHubAttribute")
|
||||
{
|
||||
if (propertyName == "ConnectionStringSetting")
|
||||
{
|
||||
return "connection";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "ServiceBusAttribute")
|
||||
{
|
||||
if (propertyName == "QueueOrTopicName")
|
||||
{
|
||||
// The attribute has a QueueOrTopicName while function.json has distinct queue and topic.
|
||||
// I just picked queue.
|
||||
return "queue";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "TwilioSmsAttribute")
|
||||
{
|
||||
if (propertyName == "AccountSidSetting")
|
||||
{
|
||||
return "accountSid";
|
||||
}
|
||||
else if (propertyName == "AuthTokenSetting")
|
||||
{
|
||||
return "authToken";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "TimerTriggerAttribute")
|
||||
{
|
||||
if (propertyName == "ScheduleExpression")
|
||||
{
|
||||
return "schedule";
|
||||
}
|
||||
}
|
||||
else if (attributeName == "EventHubTriggerAttribute")
|
||||
{
|
||||
if (propertyName == "EventHubName")
|
||||
{
|
||||
return "path";
|
||||
}
|
||||
}
|
||||
|
||||
return propertyName.ToLowerFirstCharacter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче