Skip to content

Commit 2450f09

Browse files
authored
Added new Bicep language features from v0.27.1 Azure#2859 Azure#2860 Azure#2885 (Azure#2886)
1 parent bbe08c4 commit 2450f09

10 files changed

+692
-40
lines changed

docs/CHANGELOG-v1.md

+10
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
3131

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

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

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

src/PSRule.Rules.Azure/Data/Template/ExpressionHelpers.cs

+29-10
Original file line numberDiff line numberDiff line change
@@ -686,52 +686,71 @@ internal static bool TryJObject(object o, out JObject value)
686686

687687
/// <summary>
688688
/// Union an object by merging in properties.
689+
/// If there are duplicate keys, the last key wins.
689690
/// </summary>
690-
internal static object UnionObject(object[] o)
691+
internal static object UnionObject(object[] o, bool deepMerge)
691692
{
692693
var result = new JObject();
693694
if (o == null || o.Length == 0)
694695
return result;
695696

696-
for (var i = o.Length - 1; i >= 0; i--)
697+
for (var i = 0; i < o.Length; i++)
697698
{
698699
if (o[i] is JObject jObject)
699700
{
700701
foreach (var property in jObject.Properties())
701702
{
702-
if (!result.ContainsKey(property.Name))
703-
result.Add(property.Name, property.Value);
703+
ReplaceOrMergeProperty(result, property.Name, property.Value, deepMerge);
704704
}
705705
}
706706
else if (o[i] is IDictionary<string, string> dss)
707707
{
708708
foreach (var kv in dss)
709709
{
710-
if (!result.ContainsKey(kv.Key))
711-
result.Add(kv.Key, JToken.FromObject(kv.Value));
710+
ReplaceOrMergeProperty(result, kv.Key, JToken.FromObject(kv.Value), deepMerge);
712711
}
713712
}
714713
else if (o[i] is IDictionary<string, object> dso)
715714
{
716715
foreach (var kv in dso)
717716
{
718-
if (!result.ContainsKey(kv.Key))
719-
result.Add(kv.Key, JToken.FromObject(kv.Value));
717+
ReplaceOrMergeProperty(result, kv.Key, JToken.FromObject(kv.Value), deepMerge);
720718
}
721719
}
722720
else if (o[i] is IDictionary d)
723721
{
724722
foreach (DictionaryEntry kv in d)
725723
{
726724
var key = kv.Key.ToString();
727-
if (!result.ContainsKey(key))
728-
result.Add(key, JToken.FromObject(kv.Value));
725+
ReplaceOrMergeProperty(result, key, JToken.FromObject(kv.Value), deepMerge);
729726
}
730727
}
731728
}
732729
return result;
733730
}
734731

732+
private static void ReplaceOrMergeProperty(JObject o, string propertyName, JToken propertyValue, bool deepMerge)
733+
{
734+
if (!o.TryGetProperty(propertyName, out JToken currentValue))
735+
{
736+
o.Add(propertyName, propertyValue);
737+
return;
738+
}
739+
740+
if (deepMerge && currentValue is JObject currentObject && propertyValue is JObject newObject)
741+
{
742+
foreach (var property in newObject.Properties())
743+
{
744+
ReplaceOrMergeProperty(currentObject, property.Name, property.Value, deepMerge);
745+
}
746+
}
747+
else
748+
{
749+
o[propertyName] = propertyValue;
750+
}
751+
752+
}
753+
735754
/// <summary>
736755
/// Try to get DateTime from the existing object.
737756
/// </summary>

src/PSRule.Rules.Azure/Data/Template/Functions.cs

+114-4
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ internal static class Functions
5454
new FunctionDescriptor("length", Length),
5555
new FunctionDescriptor("min", Min),
5656
new FunctionDescriptor("max", Max),
57+
new FunctionDescriptor("objectKeys", ObjectKeys),
5758
new FunctionDescriptor("range", Range),
59+
new FunctionDescriptor("shallowMerge", ShallowMerge),
5860
new FunctionDescriptor("skip", Skip),
5961
new FunctionDescriptor("take", Take),
6062
new FunctionDescriptor("tryGet", TryGet),
@@ -163,6 +165,8 @@ internal static class Functions
163165
new FunctionDescriptor("reduce", Reduce, delayBinding: true),
164166
new FunctionDescriptor("sort", Sort, delayBinding: true),
165167
new FunctionDescriptor("toObject", ToObject, delayBinding: true),
168+
new FunctionDescriptor("mapValues", MapValues, delayBinding: true),
169+
new FunctionDescriptor("groupBy", GroupBy, delayBinding: true),
166170
new FunctionDescriptor("lambda", Lambda, delayBinding: true),
167171
new FunctionDescriptor("lambdaVariables", LambdaVariables, delayBinding: true),
168172

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

749755
// Object
750756
if (ExpressionHelpers.IsObject(args[i]))
751-
return ExpressionHelpers.UnionObject(args);
757+
return ExpressionHelpers.UnionObject(args, deepMerge: true);
752758
}
753759

754760
// Handle mocks as objects if no other object or array is found.
755-
return hasMocks ? ExpressionHelpers.UnionObject(args) : null;
761+
return hasMocks ? ExpressionHelpers.UnionObject(args, deepMerge: true) : null;
762+
}
763+
764+
/// <summary>
765+
/// shallowMerge(inputArray)
766+
/// </summary>
767+
/// <remarks>
768+
/// Combines an array of objects, where only the top-level objects are merged.
769+
/// This means that if the objects being merged contain nested objects, those nested object aren't deeply merged.
770+
/// Instead, they're replaced entirely by the corresponding property from the merging object.
771+
/// Returns a single object with all elements from the parameters.
772+
/// If there are duplicate keys, the last key wins.
773+
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/templates/template-functions-object#shallowmerge"/>.
774+
/// </remarks>
775+
/// <returns>Returns an object.</returns>
776+
internal static object ShallowMerge(ITemplateContext context, object[] args)
777+
{
778+
if (CountArgs(args) != 1)
779+
throw ArgumentsOutOfRange(nameof(ShallowMerge), args);
780+
781+
if (!ExpressionHelpers.TryArray(args[0], out var entries))
782+
throw ArgumentFormatInvalid(nameof(ShallowMerge));
783+
784+
return ExpressionHelpers.UnionObject(entries.OfType<object>().ToArray(), deepMerge: false);
785+
}
786+
787+
/// <summary>
788+
/// objectKeys(object)
789+
/// </summary>
790+
/// <remarks>
791+
/// Returns the keys from an object, where an object is a collection of key-value pairs.
792+
/// Elements are consistently ordered alphabetically but case-insensitive see <see href="https://github.com/Azure/bicep/issues/14057"/>.
793+
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/templates/template-functions-object#objectkeys"/>.
794+
/// </remarks>
795+
/// <returns>An array of keys.</returns>
796+
internal static object ObjectKeys(ITemplateContext context, object[] args)
797+
{
798+
if (CountArgs(args) != 1)
799+
throw ArgumentsOutOfRange(nameof(ObjectKeys), args);
800+
801+
802+
if (!ExpressionHelpers.TryJObject(args[0], out var jObject))
803+
throw ArgumentFormatInvalid(nameof(ObjectKeys));
804+
805+
var result = new JArray();
806+
807+
// Sorting of properties is case-insensitive.
808+
foreach (var item in jObject.Properties().OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase))
809+
result.Add(item.Name);
810+
811+
return result;
756812
}
757813

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

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

2097+
/// <summary>
2098+
/// groupBy(inputArray, lambda expression)
2099+
/// </summary>
2100+
/// <remarks>
2101+
/// Creates an object with array values from an array, using a grouping condition.
2102+
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/bicep/bicep-functions-lambda#groupby"/>.
2103+
/// </remarks>
2104+
internal static object GroupBy(ITemplateContext context, object[] args)
2105+
{
2106+
if (CountArgs(args) != 2)
2107+
throw ArgumentsOutOfRange(nameof(GroupBy), args);
2108+
2109+
args[0] = GetExpression(context, args[0]);
2110+
if (!ExpressionHelpers.TryArray(args[0], out var inputArray))
2111+
throw ArgumentFormatInvalid(nameof(GroupBy));
2112+
2113+
args[1] = GetExpression(context, args[1]);
2114+
if (args[1] is not LambdaExpressionFn lambda)
2115+
throw ArgumentFormatInvalid(nameof(GroupBy));
2116+
2117+
return lambda.GroupBy(context, inputArray.OfType<object>().ToArray());
2118+
}
2119+
2120+
/// <summary>
2121+
/// mapValues(inputObject, lambda expression)
2122+
/// </summary>
2123+
/// <remarks>
2124+
/// Creates an object from an input object, using a lambda expression to map values.
2125+
/// See <seealso href="https://learn.microsoft.com/azure/azure-resource-manager/bicep/bicep-functions-lambda#mapvalues"/>.
2126+
/// </remarks>
2127+
internal static object MapValues(ITemplateContext context, object[] args)
2128+
{
2129+
if (CountArgs(args) != 2)
2130+
throw ArgumentsOutOfRange(nameof(MapValues), args);
2131+
2132+
args[0] = GetExpression(context, args[0]);
2133+
if (!ExpressionHelpers.TryJObject(args[0], out var inputObject))
2134+
throw ArgumentFormatInvalid(nameof(MapValues));
2135+
2136+
args[1] = GetExpression(context, args[1]);
2137+
if (args[1] is not LambdaExpressionFn lambda)
2138+
throw ArgumentFormatInvalid(nameof(MapValues));
2139+
2140+
return lambda.MapValues(context, inputObject);
2141+
}
2142+
20332143
/// <summary>
20342144
/// Evaluate a lambda expression.
20352145
/// </summary>
20362146
internal static object Lambda(ITemplateContext context, object[] args)
20372147
{
20382148
var count = CountArgs(args);
2039-
if (count is < 2 or > 3)
2149+
if (count is < 2 or > 4)
20402150
throw ArgumentsOutOfRange(nameof(Lambda), args);
20412151

20422152
return new LambdaExpressionFn(args);

0 commit comments

Comments
 (0)