diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 360e6a11c7..af46ec23fb 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -39,6 +39,9 @@ What's changed since pre-release v2.9.0-B0013: [#1423](https://github.com/microsoft/PSRule/issues/1423) - Quantifiers allow you to specify the number of matches with `count`, `less`, `lessOrEqual`, `greater`, or `greaterOrEqual`. - See [Sub-selectors][4] for more information. + - Added support for new functions by @BernieWhite. + [#1422](https://github.com/microsoft/PSRule/issues/1422) + - Added support for `padLeft`, and `padRight`. ## v2.9.0-B0013 (pre-release) diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md index b3461b8ce7..0ece90cbb8 100644 --- a/docs/expressions/functions.md +++ b/docs/expressions/functions.md @@ -27,6 +27,8 @@ It may be necessary to perform minor transformation before evaluating a conditio - `first` - Return the first element in an array or the first character of a string. - `integer` - Convert a value to an integer. - `last` - Return the last element in an array or the last character of a string. +- `padLeft` - Pad a value with a character on the left to meet the specified length. +- `padRight` - Pad a value with a character on the right to meet the specified length. - `path` - Get a value from an object path. - `replace` - Replace an old string with a new string. - `split` - Split a string into an array by a delimiter. diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index e64eafeac6..314457d697 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -3357,6 +3357,12 @@ }, { "$ref": "#/definitions/fn/definitions/function/definitions/split" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/padLeft" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/padRight" } ], "definitions": { @@ -3781,6 +3787,90 @@ "split", "delimiter" ] + }, + "padLeft": { + "type": "object", + "properties": { + "padLeft": { + "oneOf": [ + { + "type": "string", + "title": "Pad Left", + "description": "The padLeft function returns a string with a minimum of the specified length.", + "markdownDescription": "The `padLeft` function returns a string with a minimum of the specified length." + }, + { + "type": "object", + "title": "Pad Left", + "description": "The padLeft function returns a string with a minimum of the specified length.", + "markdownDescription": "The `padLeft` function returns a string with a minimum of the specified length.", + "$ref": "#/definitions/fn/definitions/function" + } + ], + "default": {} + }, + "totalLength": { + "type": "integer", + "title": "Total length", + "description": "Sets the number of characters to pad the string to. If the string is less then the specified number of characters one or more padding characters will be added until the length is reached.", + "minimum": 1 + }, + "paddingCharacter": { + "type": "string", + "title": "Padding characters", + "description": "Sets the character to use for padding the string to reach the configured total length.", + "minLength": 1, + "maxLength": 1, + "default": " " + } + }, + "additionalProperties": false, + "required": [ + "padLeft", + "totalLength" + ] + }, + "padRight": { + "type": "object", + "properties": { + "padRight": { + "oneOf": [ + { + "type": "string", + "title": "Pad Right", + "description": "The padRight function returns a string with a minimum of the specified length.", + "markdownDescription": "The `padRight` function returns a string with a minimum of the specified length." + }, + { + "type": "object", + "title": "Pad Right", + "description": "The padRight function returns a string with a minimum of the specified length.", + "markdownDescription": "The `padRight` function returns a string with a minimum of the specified length.", + "$ref": "#/definitions/fn/definitions/function" + } + ], + "default": {} + }, + "totalLength": { + "type": "integer", + "title": "Total length", + "description": "Sets the number of characters to pad the string to. If the string is less then the specified number of characters one or more padding characters will be added until the length is reached.", + "minimum": 1 + }, + "paddingCharacter": { + "type": "string", + "title": "Padding characters", + "description": "Sets the character to use for padding the string to reach the configured total length.", + "minLength": 1, + "maxLength": 1, + "default": " " + } + }, + "additionalProperties": false, + "required": [ + "padRight", + "totalLength" + ] } } } diff --git a/src/PSRule/Common/DictionaryExtensions.cs b/src/PSRule/Common/DictionaryExtensions.cs index db7e31fefd..b0f14c7fa8 100644 --- a/src/PSRule/Common/DictionaryExtensions.cs +++ b/src/PSRule/Common/DictionaryExtensions.cs @@ -106,6 +106,26 @@ public static bool TryGetInt(this IDictionary dictionary, string return false; } + [DebuggerStepThrough] + public static bool TryGetChar(this IDictionary dictionary, string key, out char? value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (o is string svalue && svalue.Length == 1) + { + value = svalue[0]; + return true; + } + if (o is char cvalue) + { + value = cvalue; + return true; + } + return false; + } + [DebuggerStepThrough] public static bool TryGetString(this IDictionary dictionary, string key, out string value) { diff --git a/src/PSRule/Definitions/Expressions/Functions.cs b/src/PSRule/Definitions/Expressions/Functions.cs index d260714f61..7c8a8bdc10 100644 --- a/src/PSRule/Definitions/Expressions/Functions.cs +++ b/src/PSRule/Definitions/Expressions/Functions.cs @@ -30,10 +30,16 @@ internal static class Functions private const string FIRST = "first"; private const string LAST = "last"; private const string SPLIT = "split"; + private const string PADLEFT = "padLeft"; + private const string PADRIGHT = "padRight"; private const string DELIMITER = "delimiter"; private const string OLDSTRING = "oldstring"; private const string NEWSTRING = "newstring"; private const string CASESENSITIVE = "casesensitive"; + private const string TOTALLENGTH = "totalLength"; + private const string PADDINGCHARACTER = "paddingCharacter"; + + private const char SPACE = ' '; /// /// The available built-in functions. @@ -52,6 +58,8 @@ internal static class Functions new FunctionDescriptor(FIRST, First), new FunctionDescriptor(LAST, Last), new FunctionDescriptor(SPLIT, Split), + new FunctionDescriptor(PADLEFT, PadLeft), + new FunctionDescriptor(PADRIGHT, PadRight), }; private static ExpressionFnOuter Boolean(IExpressionContext context, PropertyBag properties) @@ -244,6 +252,45 @@ private static ExpressionFnOuter Split(IExpressionContext context, PropertyBag p }; } + private static ExpressionFnOuter PadLeft(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + + !TryProperty(properties, PADLEFT, out ExpressionFnOuter next)) + return null; + + var paddingChar = properties.TryGetChar(PADDINGCHARACTER, out var c) ? c : SPACE; + var totalWidth = properties.TryGetInt(TOTALLENGTH, out var i) ? i : 0; + return (context) => + { + var value = next(context); + if (ExpressionHelpers.TryString(value, convert: true, value: out var s)) + return totalWidth > s.Length ? s.PadLeft(totalWidth.Value, paddingChar.Value) : s; + + return null; + }; + } + + private static ExpressionFnOuter PadRight(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, PADRIGHT, out ExpressionFnOuter next)) + return null; + + var paddingChar = properties.TryGetChar(PADDINGCHARACTER, out var c) ? c : SPACE; + var totalWidth = properties.TryGetInt(TOTALLENGTH, out var i) ? i : 0; + return (context) => + { + var value = next(context); + if (ExpressionHelpers.TryString(value, convert: true, value: out var s)) + return totalWidth > s.Length ? s.PadRight(totalWidth.Value, paddingChar.Value) : s; + + return null; + }; + } + #region Helper functions private static bool TryProperty(PropertyBag properties, string name, out int? value) diff --git a/tests/PSRule.Tests/FunctionTests.cs b/tests/PSRule.Tests/FunctionTests.cs index 036fe41e61..44d0dd87f8 100644 --- a/tests/PSRule.Tests/FunctionTests.cs +++ b/tests/PSRule.Tests/FunctionTests.cs @@ -427,6 +427,174 @@ public void Split() Assert.Null(fn(context, properties)(context)); } + [Fact] + public void PadLeft() + { + var context = GetContext(); + var fn = GetFunction("padLeft"); + + var properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", 5 } + }; + Assert.Equal(" One", fn(context, properties)(context)); + + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", 3 } + }; + Assert.Equal("One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", 5 }, + { "paddingCharacter", '_' } + }; + Assert.Equal("__One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", 5 }, + { "paddingCharacter", "_" } + }; + Assert.Equal("__One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", 5 }, + { "paddingCharacter", "__" } + }; + Assert.Equal(" One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", 3 }, + { "paddingCharacter", "_" } + }; + Assert.Equal("One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", 1 }, + { "paddingCharacter", "_" } + }; + Assert.Equal("One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", -1 }, + { "paddingCharacter", "_" } + }; + Assert.Equal("One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", null }, + { "totalLength", 5 } + }; + Assert.Null(fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padLeft", "One" }, + { "totalLength", null } + }; + Assert.Equal("One", fn(context, properties)(context)); + } + + [Fact] + public void PadRight() + { + var context = GetContext(); + var fn = GetFunction("padRight"); + + var properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", 5 } + }; + Assert.Equal("One ", fn(context, properties)(context)); + + + properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", 3 } + }; + Assert.Equal("One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", 5 }, + { "paddingCharacter", '_' } + }; + Assert.Equal("One__", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", 5 }, + { "paddingCharacter", "_" } + }; + Assert.Equal("One__", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", 5 }, + { "paddingCharacter", "__" } + }; + Assert.Equal("One ", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", 3 }, + { "paddingCharacter", "_" } + }; + Assert.Equal("One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", 1 }, + { "paddingCharacter", "_" } + }; + Assert.Equal("One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", -1 }, + { "paddingCharacter", "_" } + }; + Assert.Equal("One", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padRight", null }, + { "totalLength", 5 } + }; + Assert.Null(fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "padRight", "One" }, + { "totalLength", null } + }; + Assert.Equal("One", fn(context, properties)(context)); + } + #region Helper methods private static ExpressionBuilderFn GetFunction(string name) diff --git a/tests/PSRule.Tests/Functions.Rule.jsonc b/tests/PSRule.Tests/Functions.Rule.jsonc index 452c3ca9f6..a198d2de30 100644 --- a/tests/PSRule.Tests/Functions.Rule.jsonc +++ b/tests/PSRule.Tests/Functions.Rule.jsonc @@ -240,5 +240,49 @@ ] } } + }, + { + // Synopsis: A test for the padLeft function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.PadLeft" + }, + "spec": { + "if": { + "value": { + "$": { + "padLeft": { + "string": "One" + }, + "totalLength": 5, + "paddingCharacter": "_" + } + }, + "equals": "__One" + } + } + }, + { + // Synopsis: A test for the padRight function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.PadRight" + }, + "spec": { + "if": { + "value": { + "$": { + "padRight": { + "string": "One" + }, + "totalLength": 5, + "paddingCharacter": "_" + } + }, + "equals": "One__" + } + } } ] diff --git a/tests/PSRule.Tests/Functions.Rule.yaml b/tests/PSRule.Tests/Functions.Rule.yaml index 5d3b64752e..86d8c14722 100644 --- a/tests/PSRule.Tests/Functions.Rule.yaml +++ b/tests/PSRule.Tests/Functions.Rule.yaml @@ -53,9 +53,9 @@ spec: value: $: concat: - - path: name - - string: '-' - - path: name + - path: name + - string: '-' + - path: name equals: TestObject1-TestObject1 --- @@ -83,9 +83,9 @@ spec: equals: $: concat: - - path: name - - string: '-' - - path: name + - path: name + - string: '-' + - path: name --- # Synopsis: A test for the replace function. @@ -128,8 +128,8 @@ spec: value: $: first: - - string: abc - - string: def + - string: abc + - string: def equals: abc --- @@ -143,8 +143,8 @@ spec: value: $: last: - - string: abc - - string: def + - string: abc + - string: def equals: def --- @@ -162,6 +162,38 @@ spec: delimiter: - ' ' equals: - - One - - Two - - Three + - One + - Two + - Three + +--- +# Synopsis: A test for the padLeft function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.PadLeft +spec: + if: + value: + $: + padLeft: + string: One + paddingCharacter: _ + totalLength: 5 + equals: __One + +--- +# Synopsis: A test for the padRight function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.PadRight +spec: + if: + value: + $: + padRight: + string: One + paddingCharacter: _ + totalLength: 5 + equals: One__ diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index c9be56f7a8..2282da2529 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -1673,7 +1673,7 @@ public void AllOf(string type, string path) GetObject((name: "name", value: "log1")), GetObject((name: "name", value: "log2")) })))); - actual3 = GetObject((name: "Name", value: "TargetObject1"), (name: "properties", value: GetObject((name: "logs", value: new object[] {})))); + actual3 = GetObject((name: "Name", value: "TargetObject1"), (name: "properties", value: GetObject((name: "logs", value: new object[] { })))); Assert.True(allOf.Match(actual1)); Assert.True(allOf.Match(actual2)); @@ -1831,6 +1831,8 @@ public void WithFunctionSpecific(string type, string path) var example3 = GetSelectorVisitor($"{type}.Fn.First", GetSource(path), out _); var example4 = GetSelectorVisitor($"{type}.Fn.Last", GetSource(path), out _); var example5 = GetSelectorVisitor($"{type}.Fn.Split", GetSource(path), out _); + var example6 = GetSelectorVisitor($"{type}.Fn.PadLeft", GetSource(path), out _); + var example7 = GetSelectorVisitor($"{type}.Fn.PadRight", GetSource(path), out _); var actual1 = GetObject( (name: "Name", value: "TestObject1") ); @@ -1840,6 +1842,8 @@ public void WithFunctionSpecific(string type, string path) Assert.True(example3.Match(actual1)); Assert.True(example4.Match(actual1)); Assert.True(example5.Match(actual1)); + Assert.True(example6.Match(actual1)); + Assert.True(example7.Match(actual1)); } #endregion Functions