Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SourceGen] Handle default values in binding attributes #1131

Merged
merged 12 commits into from
Nov 9, 2022
1 change: 1 addition & 0 deletions sdk/Sdk.Generators/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal static class Constants
internal const string HttpResponseType = "Microsoft.Azure.Functions.Worker.Http.HttpResponseData";
internal const string EventHubsTriggerType = "Microsoft.Azure.Functions.Worker.EventHubTriggerAttribute";
internal const string BindingPropertyNameAttributeType = "Microsoft.Azure.Functions.Worker.Extensions.Abstractions.BindingPropertyNameAttribute";
internal const string DefaultValueType = "Microsoft.Azure.Functions.Worker.Extensions.Abstractions.DefaultValueAttribute";

// System types
internal const string IEnumerableType = "System.Collections.IEnumerable";
Expand Down
20 changes: 20 additions & 0 deletions sdk/Sdk.Generators/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,25 @@ public static string TrimStringsFromEnd(this string str, IReadOnlyList<string> s

return result;
}

public static string UppercaseFirst(this string str)
{
// Check for empty string.
if (string.IsNullOrEmpty(str))
{
return string.Empty;
}

if (!char.IsUpper(str[0]))
{
// Return char and concat substring.
return char.ToUpper(str[0]) + str.Substring(1);
}
else
{
return str;
}

}
}
}
61 changes: 48 additions & 13 deletions sdk/Sdk.Generators/FunctionMetadataProviderGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,13 +284,13 @@ private bool TryGetParameterInputAndTriggerBindings(MethodDeclarationSyntax meth

if (dataType is not DataType.Undefined)
{
bindingDict!.Add("dataType", FormatObject(Enum.GetName(typeof(DataType), dataType)));
bindingDict!.Add("DataType", FormatObject(Enum.GetName(typeof(DataType), dataType)));
}

// special handling for EventHubsTrigger - we need to define a property called "Cardinality"
if (validEventHubs)
{
if (!bindingDict!.Keys.Contains("Cardinality"))
if (!bindingDict!.ContainsKey("Cardinality"))
{
bindingDict["Cardinality"] = cardinality is Cardinality.Many ? FormatObject("Many") : FormatObject("One");
}
Expand Down Expand Up @@ -433,9 +433,9 @@ private IDictionary<string, string> GetHttpReturnBinding(string returnBindingNam
{
var httpBinding = new Dictionary<string, string>();

httpBinding.Add("name", FormatObject(returnBindingName));
httpBinding.Add("type", FormatObject("http"));
httpBinding.Add("direction", FormatObject("Out"));
httpBinding.Add("Name", FormatObject(returnBindingName));
httpBinding.Add("Type", FormatObject("http"));
httpBinding.Add("Direction", FormatObject("Out"));

return httpBinding;
}
Expand All @@ -460,9 +460,9 @@ private bool TryCreateBindingDict(AttributeData bindingAttrData, string bindingN

var bindingCount = attributeProperties!.Count + 3;
bindings = new Dictionary<string, string>(capacity: bindingCount);
bindings.Add("name", FormatObject(bindingName));
bindings.Add("type", FormatObject(bindingType));
bindings.Add("direction", FormatObject(bindingDirection));
bindings.Add("Name", FormatObject(bindingName));
bindings.Add("Type", FormatObject(bindingType));
bindings.Add("Direction", FormatObject(bindingDirection));

// Add additional bindingInfo to the anonymous type because some functions have more properties than others
foreach (var prop in attributeProperties!) // attributeProperties won't be null here b/c we would've exited this method earlier if it was during TryGetAttributeProperties check
Expand All @@ -472,14 +472,14 @@ private bool TryCreateBindingDict(AttributeData bindingAttrData, string bindingN
if (prop.Value?.GetType().IsArray ?? false)
{
string arr = FormatArray((IEnumerable)prop.Value);
bindings[propertyName] = arr;
bindings[propertyName.UppercaseFirst()] = arr; // Uppercase first to use PascalCase in generated file's anonymous type
}
else
{
bool isEnum = string.Equals(prop.Key, "authLevel", StringComparison.OrdinalIgnoreCase); // prop keys come from Azure Functions defined attributes so we can check directly for authLevel

var propertyValue = FormatObject(prop.Value, isEnum);
bindings[propertyName] = propertyValue;
bindings[propertyName.UppercaseFirst()] = propertyValue;
}
}

Expand All @@ -503,15 +503,38 @@ private bool TryGetAttributeProperties(AttributeData attributeData, Location? at
{
if (namedArgument.Value.Value != null)
{
if (String.Equals(namedArgument.Key, Constants.IsBatchedKey) && !attrProperties.Keys.Contains("Cardinality"))
satvu marked this conversation as resolved.
Show resolved Hide resolved
if (string.Equals(namedArgument.Key, Constants.IsBatchedKey) && !attrProperties.ContainsKey("Cardinality"))
{
var argValue = (bool)namedArgument.Value.Value; // isBatched only takes in booleans and the generator will parse it as a bool so we can type cast this to use in the next line

attrProperties["Cardinality"] = argValue ? "Many" : "One";
}
else
{
attrProperties[namedArgument.Key] = namedArgument.Value.Value;
attrProperties[namedArgument.Key.UppercaseFirst()] = namedArgument.Value.Value;
}
}
}

// some properties have default values, so if these properties were not already defined in constructor or named arguments, we will auto-add them here
foreach (var member in attributeData.AttributeClass!.GetMembers().Where(a => a is IPropertySymbol))
{
var defaultValAttrList = member.GetAttributes().Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, Compilation.GetTypeByMetadataName(Constants.DefaultValueType)));

if (defaultValAttrList.SingleOrDefault() is { } defaultValAttr) // list will only be of size one b/c there cannot be duplicates of an attribute on one piece of syntax
{
var argName = member.Name;
object arg = defaultValAttr.ConstructorArguments.SingleOrDefault().Value!; // only one constructor arg in DefaultValue attribute (the default value)
if (arg is bool b && string.Equals(argName, Constants.IsBatchedKey))
{
if (!attrProperties.Keys.Contains("Cardinality"))
satvu marked this conversation as resolved.
Show resolved Hide resolved
{
attrProperties["Cardinality"] = b ? "Many" : "One";
}
}
else if (!attrProperties.Keys.Any(a => string.Equals(a, argName, StringComparison.OrdinalIgnoreCase))) // check if this property has been assigned a value already in constructor or named args
satvu marked this conversation as resolved.
Show resolved Hide resolved
{
attrProperties[argName.UppercaseFirst()] = arg;
}
}
}
Expand Down Expand Up @@ -590,7 +613,7 @@ private bool IsEventHubsTriggerValid(IParameterSymbol parameterSymbol, TypeSynta
dataType = DataType.Undefined;
cardinality = Cardinality.Undefined;

// Check if IsBatched is false (by default it is true and does not appear in the attribute constructor)
// check if IsBatched is defined in the NamedArguments
foreach (var arg in attribute.NamedArguments)
{
if (String.Equals(arg.Key, Constants.IsBatchedKey) &&
Expand All @@ -607,6 +630,18 @@ private bool IsEventHubsTriggerValid(IParameterSymbol parameterSymbol, TypeSynta
}
}

// Check the default value of IsBatched
var eventHubsAttr = attribute.AttributeClass;
var isBatchedProp = eventHubsAttr!.GetMembers().Where(m => string.Equals(m.Name, Constants.IsBatchedKey, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
AttributeData defaultValAttr = isBatchedProp.GetAttributes().Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, Compilation.GetTypeByMetadataName(Constants.DefaultValueType))).SingleOrDefault();
var defaultVal = defaultValAttr.ConstructorArguments.SingleOrDefault().Value!.ToString(); // there is only one constructor arg, the default value
satvu marked this conversation as resolved.
Show resolved Hide resolved
if (!bool.TryParse(defaultVal, out bool b) || !b)
{
dataType = GetDataType(parameterSymbol.Type);
cardinality = Cardinality.One;
return true;
}

// we check if the param is an array type
// we exclude byte arrays (byte[]) b/c we handle that as cardinality one (we handle this simliar to how a char[] is basically a string)
if (parameterSymbol.Type is IArrayTypeSymbol && !SymbolEqualityComparer.Default.Equals(parameterSymbol.Type, Compilation.GetTypeByMetadataName(Constants.ByteArrayType)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,17 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
var metadataList = new List<IFunctionMetadata>();
var Function0RawBindings = new List<string>();
var Function0binding0 = new {
name = ""input"",
type = ""EventHubTrigger"",
direction = ""In"",
eventHubName = ""test"",
Name = ""input"",
Type = ""EventHubTrigger"",
Direction = ""In"",
EventHubName = ""test"",
Connection = ""EventHubConnectionAppSetting"",
Cardinality = ""One"",");

if(!string.Equals(dataType, ""))
{
expectedOutputBuilder.Append(@"
dataType = """ + dataType + "\",");
DataType = """ + dataType + "\",");
}

expectedOutputBuilder.Append(@"
Expand Down Expand Up @@ -226,21 +226,21 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
var metadataList = new List<IFunctionMetadata>();
var Function0RawBindings = new List<string>();
var Function0binding0 = new {
name = ""input"",
type = ""EventHubTrigger"",
direction = ""In"",
eventHubName = ""test"",
Connection = ""EventHubConnectionAppSetting"","
Name = ""input"",
Type = ""EventHubTrigger"",
Direction = ""In"",
EventHubName = ""test"",
Connection = ""EventHubConnectionAppSetting"",
Cardinality = ""Many"","
);

if (!string.Equals(dataType, ""))
{
expectedOutputBuilder.Append(@"
dataType = """ + dataType + "\",");
DataType = """ + dataType + "\",");
}

expectedOutputBuilder.Append(@"
Cardinality = ""Many"",
};
var Function0binding0JSON = JsonSerializer.Serialize(Function0binding0);
Function0RawBindings.Add(Function0binding0JSON);
Expand Down Expand Up @@ -327,10 +327,10 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
var metadataList = new List<IFunctionMetadata>();
var Function0RawBindings = new List<string>();
var Function0binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
Cardinality = 'Many',
};
Expand Down Expand Up @@ -471,13 +471,13 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
var metadataList = new List<IFunctionMetadata>();
var Function0RawBindings = new List<string>();
var Function0binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
dataType = 'String',
Cardinality = 'Many',
DataType = 'String',
};
var Function0binding0JSON = JsonSerializer.Serialize(Function0binding0);
Function0RawBindings.Add(Function0binding0JSON);
Expand All @@ -492,13 +492,13 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
metadataList.Add(Function0);
var Function1RawBindings = new List<string>();
var Function1binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
dataType = 'String',
Cardinality = 'Many',
DataType = 'String',
};
var Function1binding0JSON = JsonSerializer.Serialize(Function1binding0);
Function1RawBindings.Add(Function1binding0JSON);
Expand All @@ -513,13 +513,13 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
metadataList.Add(Function1);
var Function2RawBindings = new List<string>();
var Function2binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
dataType = 'String',
Cardinality = 'Many',
DataType = 'String',
};
var Function2binding0JSON = JsonSerializer.Serialize(Function2binding0);
Function2RawBindings.Add(Function2binding0JSON);
Expand All @@ -534,13 +534,13 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
metadataList.Add(Function2);
var Function3RawBindings = new List<string>();
var Function3binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
dataType = 'String',
Cardinality = 'Many',
DataType = 'String',
};
var Function3binding0JSON = JsonSerializer.Serialize(Function3binding0);
Function3RawBindings.Add(Function3binding0JSON);
Expand Down Expand Up @@ -652,13 +652,13 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
var metadataList = new List<IFunctionMetadata>();
var Function0RawBindings = new List<string>();
var Function0binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
dataType = 'Binary',
Cardinality = 'Many',
DataType = 'Binary',
};
var Function0binding0JSON = JsonSerializer.Serialize(Function0binding0);
Function0RawBindings.Add(Function0binding0JSON);
Expand All @@ -673,13 +673,13 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
metadataList.Add(Function0);
var Function1RawBindings = new List<string>();
var Function1binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
dataType = 'Binary',
Cardinality = 'Many',
DataType = 'Binary',
};
var Function1binding0JSON = JsonSerializer.Serialize(Function1binding0);
Function1RawBindings.Add(Function1binding0JSON);
Expand Down Expand Up @@ -778,10 +778,10 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
var metadataList = new List<IFunctionMetadata>();
var Function0RawBindings = new List<string>();
var Function0binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
Cardinality = 'Many',
};
Expand All @@ -798,10 +798,10 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
metadataList.Add(Function0);
var Function1RawBindings = new List<string>();
var Function1binding0 = new {
name = 'input',
type = 'EventHubTrigger',
direction = 'In',
eventHubName = 'test',
Name = 'input',
Type = 'EventHubTrigger',
Direction = 'In',
EventHubName = 'test',
Connection = 'EventHubConnectionAppSetting',
Cardinality = 'Many',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ public void TrimStringsFromEndWorks(string input, string expectedOutput)

Assert.Equal(expectedOutput, actual);
}

[Theory]
[InlineData("isBatched", "IsBatched")]
[InlineData("MyProperty", "MyProperty")]
[InlineData("myproperty", "Myproperty")]
public void UpperCaseFirstLetter(string input, string expectedOutput)
{
Assert.Equal(input.UppercaseFirst(), expectedOutput);
}
}
}
}
Loading