Skip to content

Commit

Permalink
Added new Bicep language features from v0.27.1 Azure#2859 Azure#2860 A…
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored May 23, 2024
1 parent bbe08c4 commit 2450f09
Show file tree
Hide file tree
Showing 10 changed files with 692 additions and 40 deletions.
10 changes: 10 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

What's changed since pre-release v1.37.0-B0009:

- New features:
- Added support for new Bicep language features introduced in v0.27.1 by @BernieWhite.
[#2860](https://github.com/Azure/PSRule.Rules.Azure/issues/2860)
[#2859](https://github.com/Azure/PSRule.Rules.Azure/issues/2859)
- Added support for `shallowMerge`, `groupBy`, `objectKeys`, and `mapValues`.
- Updated syntax for Bicep lambda usage of `map`, `reduce`, and `filter` which now support indices.
- Added support for spread operator.
- New rules:
- Application Gateway:
- Check that WAF v2 doesn't use legacy WAF configuration by @BenjaminEngeset.
Expand All @@ -56,6 +63,9 @@ What's changed since pre-release v1.37.0-B0009:
- General improvements:
- Updated resource providers and policy aliases.
[#2880](https://github.com/Azure/PSRule.Rules.Azure/pull/2880)
- Bug fixed:
- Fixed `union` does not perform deep merge or keep property order by @BernieWhite.
[#2885](https://github.com/Azure/PSRule.Rules.Azure/issues/2885)

## v1.37.0-B0009 (pre-release)

Expand Down
39 changes: 29 additions & 10 deletions src/PSRule.Rules.Azure/Data/Template/ExpressionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -686,52 +686,71 @@ internal static bool TryJObject(object o, out JObject value)

/// <summary>
/// Union an object by merging in properties.
/// If there are duplicate keys, the last key wins.
/// </summary>
internal static object UnionObject(object[] o)
internal static object UnionObject(object[] o, bool deepMerge)
{
var result = new JObject();
if (o == null || o.Length == 0)
return result;

for (var i = o.Length - 1; i >= 0; i--)
for (var i = 0; i < o.Length; i++)
{
if (o[i] is JObject jObject)
{
foreach (var property in jObject.Properties())
{
if (!result.ContainsKey(property.Name))
result.Add(property.Name, property.Value);
ReplaceOrMergeProperty(result, property.Name, property.Value, deepMerge);
}
}
else if (o[i] is IDictionary<string, string> dss)
{
foreach (var kv in dss)
{
if (!result.ContainsKey(kv.Key))
result.Add(kv.Key, JToken.FromObject(kv.Value));
ReplaceOrMergeProperty(result, kv.Key, JToken.FromObject(kv.Value), deepMerge);
}
}
else if (o[i] is IDictionary<string, object> dso)
{
foreach (var kv in dso)
{
if (!result.ContainsKey(kv.Key))
result.Add(kv.Key, JToken.FromObject(kv.Value));
ReplaceOrMergeProperty(result, kv.Key, JToken.FromObject(kv.Value), deepMerge);
}
}
else if (o[i] is IDictionary d)
{
foreach (DictionaryEntry kv in d)
{
var key = kv.Key.ToString();
if (!result.ContainsKey(key))
result.Add(key, JToken.FromObject(kv.Value));
ReplaceOrMergeProperty(result, key, JToken.FromObject(kv.Value), deepMerge);
}
}
}
return result;
}

private static void ReplaceOrMergeProperty(JObject o, string propertyName, JToken propertyValue, bool deepMerge)
{
if (!o.TryGetProperty(propertyName, out JToken currentValue))
{
o.Add(propertyName, propertyValue);
return;
}

if (deepMerge && currentValue is JObject currentObject && propertyValue is JObject newObject)
{
foreach (var property in newObject.Properties())
{
ReplaceOrMergeProperty(currentObject, property.Name, property.Value, deepMerge);
}
}
else
{
o[propertyName] = propertyValue;
}

}

/// <summary>
/// Try to get DateTime from the existing object.
/// </summary>
Expand Down
118 changes: 114 additions & 4 deletions src/PSRule.Rules.Azure/Data/Template/Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ internal static class Functions
new FunctionDescriptor("length", Length),
new FunctionDescriptor("min", Min),
new FunctionDescriptor("max", Max),
new FunctionDescriptor("objectKeys", ObjectKeys),
new FunctionDescriptor("range", Range),
new FunctionDescriptor("shallowMerge", ShallowMerge),
new FunctionDescriptor("skip", Skip),
new FunctionDescriptor("take", Take),
new FunctionDescriptor("tryGet", TryGet),
Expand Down Expand Up @@ -163,6 +165,8 @@ internal static class Functions
new FunctionDescriptor("reduce", Reduce, delayBinding: true),
new FunctionDescriptor("sort", Sort, delayBinding: true),
new FunctionDescriptor("toObject", ToObject, delayBinding: true),
new FunctionDescriptor("mapValues", MapValues, delayBinding: true),
new FunctionDescriptor("groupBy", GroupBy, delayBinding: true),
new FunctionDescriptor("lambda", Lambda, delayBinding: true),
new FunctionDescriptor("lambdaVariables", LambdaVariables, delayBinding: true),

Expand Down Expand Up @@ -722,8 +726,10 @@ internal static object TryGet(ITemplateContext context, object[] args)
/// union(arg1, arg2, arg3, ...)
/// </summary>
/// <remarks>
/// Returns a single array or object with all elements from the parameters. For arrays, duplicate values are included once.
/// Returns a single array or object with all elements from the parameters.
/// For arrays, duplicate values are included once.
/// For objects, duplicate property names are only included once.
/// If there are duplicate keys, the last key wins.
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/templates/template-functions-object#union"/>.
/// </remarks>
internal static object Union(ITemplateContext context, object[] args)
Expand All @@ -748,11 +754,61 @@ internal static object Union(ITemplateContext context, object[] args)

// Object
if (ExpressionHelpers.IsObject(args[i]))
return ExpressionHelpers.UnionObject(args);
return ExpressionHelpers.UnionObject(args, deepMerge: true);
}

// Handle mocks as objects if no other object or array is found.
return hasMocks ? ExpressionHelpers.UnionObject(args) : null;
return hasMocks ? ExpressionHelpers.UnionObject(args, deepMerge: true) : null;
}

/// <summary>
/// shallowMerge(inputArray)
/// </summary>
/// <remarks>
/// Combines an array of objects, where only the top-level objects are merged.
/// This means that if the objects being merged contain nested objects, those nested object aren't deeply merged.
/// Instead, they're replaced entirely by the corresponding property from the merging object.
/// Returns a single object with all elements from the parameters.
/// If there are duplicate keys, the last key wins.
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/templates/template-functions-object#shallowmerge"/>.
/// </remarks>
/// <returns>Returns an object.</returns>
internal static object ShallowMerge(ITemplateContext context, object[] args)
{
if (CountArgs(args) != 1)
throw ArgumentsOutOfRange(nameof(ShallowMerge), args);

if (!ExpressionHelpers.TryArray(args[0], out var entries))
throw ArgumentFormatInvalid(nameof(ShallowMerge));

return ExpressionHelpers.UnionObject(entries.OfType<object>().ToArray(), deepMerge: false);
}

/// <summary>
/// objectKeys(object)
/// </summary>
/// <remarks>
/// Returns the keys from an object, where an object is a collection of key-value pairs.
/// Elements are consistently ordered alphabetically but case-insensitive see <see href="https://github.com/Azure/bicep/issues/14057"/>.
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/templates/template-functions-object#objectkeys"/>.
/// </remarks>
/// <returns>An array of keys.</returns>
internal static object ObjectKeys(ITemplateContext context, object[] args)
{
if (CountArgs(args) != 1)
throw ArgumentsOutOfRange(nameof(ObjectKeys), args);


if (!ExpressionHelpers.TryJObject(args[0], out var jObject))
throw ArgumentFormatInvalid(nameof(ObjectKeys));

var result = new JArray();

// Sorting of properties is case-insensitive.
foreach (var item in jObject.Properties().OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase))
result.Add(item.Name);

return result;
}

#endregion Array and object
Expand Down Expand Up @@ -1986,6 +2042,7 @@ internal static object Reduce(ITemplateContext context, object[] args)
/// sort(inputArray, lambda expression)
/// </summary>
/// <remarks>
/// Sorts an array with a custom sort function.
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/bicep/bicep-functions-lambda#sort"/>.
/// </remarks>
internal static object Sort(ITemplateContext context, object[] args)
Expand All @@ -2004,6 +2061,13 @@ internal static object Sort(ITemplateContext context, object[] args)
return lambda.Sort(context, inputArray.OfType<object>().ToArray());
}

/// <summary>
/// toObject(inputArray, lambda expression, [lambda expression])
/// </summary>
/// <remarks>
/// Converts an array to an object with a custom key function and optional custom value function.
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/bicep/bicep-functions-lambda#toobject"/>.
/// </remarks>
internal static object ToObject(ITemplateContext context, object[] args)
{
var count = CountArgs(args);
Expand All @@ -2030,13 +2094,59 @@ internal static object ToObject(ITemplateContext context, object[] args)
return lambdaKeys.ToObject(context, inputArray.OfType<object>().ToArray(), lambdaValues);
}

/// <summary>
/// groupBy(inputArray, lambda expression)
/// </summary>
/// <remarks>
/// Creates an object with array values from an array, using a grouping condition.
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/bicep/bicep-functions-lambda#groupby"/>.
/// </remarks>
internal static object GroupBy(ITemplateContext context, object[] args)
{
if (CountArgs(args) != 2)
throw ArgumentsOutOfRange(nameof(GroupBy), args);

args[0] = GetExpression(context, args[0]);
if (!ExpressionHelpers.TryArray(args[0], out var inputArray))
throw ArgumentFormatInvalid(nameof(GroupBy));

args[1] = GetExpression(context, args[1]);
if (args[1] is not LambdaExpressionFn lambda)
throw ArgumentFormatInvalid(nameof(GroupBy));

return lambda.GroupBy(context, inputArray.OfType<object>().ToArray());
}

/// <summary>
/// mapValues(inputObject, lambda expression)
/// </summary>
/// <remarks>
/// Creates an object from an input object, using a lambda expression to map values.
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/bicep/bicep-functions-lambda#mapvalues"/>.
/// </remarks>
internal static object MapValues(ITemplateContext context, object[] args)
{
if (CountArgs(args) != 2)
throw ArgumentsOutOfRange(nameof(MapValues), args);

args[0] = GetExpression(context, args[0]);
if (!ExpressionHelpers.TryJObject(args[0], out var inputObject))
throw ArgumentFormatInvalid(nameof(MapValues));

args[1] = GetExpression(context, args[1]);
if (args[1] is not LambdaExpressionFn lambda)
throw ArgumentFormatInvalid(nameof(MapValues));

return lambda.MapValues(context, inputObject);
}

/// <summary>
/// Evaluate a lambda expression.
/// </summary>
internal static object Lambda(ITemplateContext context, object[] args)
{
var count = CountArgs(args);
if (count is < 2 or > 3)
if (count is < 2 or > 4)
throw ArgumentsOutOfRange(nameof(Lambda), args);

return new LambdaExpressionFn(args);
Expand Down
Loading

0 comments on commit 2450f09

Please sign in to comment.