diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 5ac2ad1a52..4f83e19a7f 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -35,6 +35,9 @@ What's changed since pre-release v2.7.0-B0016: - New features: - Added API version date comparison assertion method and expression by @BernieWhite. [#1356](https://github.com/microsoft/PSRule/issues/1356) + - Added support for new functions by @BernieWhite. + [#1227](https://github.com/microsoft/PSRule/issues/1227) + - Added support for `trim`, `replace`, `split`, `first`, and `last`. - Bug fixes: - Fixes CLI failed to load required assemblies by @BernieWhite. [#1361](https://github.com/microsoft/PSRule/issues/1361) diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md index 312dc77a06..7269c74910 100644 --- a/docs/expressions/functions.md +++ b/docs/expressions/functions.md @@ -22,12 +22,17 @@ Functions cover two (2) main scenarios: It may be necessary to perform minor transformation before evaluating a condition. - `boolean` - Convert a value to a boolean. -- `string` - Convert a value to a string. -- `integer` - Convert a value to an integer. - `concat` - Concatenate multiple values. -- `substring` - Extract a substring from a string. - `configuration` - Get a configuration value. +- `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. - `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. +- `string` - Convert a value to a string. +- `substring` - Extract a substring from a string. +- `trim` - Remove whitespace from the start and end of a string. ## Supported conditions diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index b30580c5c6..d34c8c3597 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -3034,6 +3034,21 @@ }, { "$ref": "#/definitions/fn/definitions/function/definitions/configuration" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/replace" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/trim" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/first" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/last" + }, + { + "$ref": "#/definitions/fn/definitions/function/definitions/split" } ], "definitions": { @@ -3305,6 +3320,159 @@ "required": [ "configuration" ] + }, + "replace": { + "type": "object", + "properties": { + "replace": { + "type": "object", + "title": "Trim", + "description": "The replace function searches for a string and replaces them with a new string.", + "markdownDescription": "The `replace` function searches for a string and replaces them with a new string.", + "$ref": "#/definitions/fn/definitions/function" + }, + "oldString": { + "type": "string", + "description": "The string to replace.", + "minLength": 1 + }, + "newString": { + "type": "string", + "description": "The new string to replace oldString." + }, + "caseSensitive": { + "type": "boolean", + "title": "Case-sensitive", + "description": "Determines if the replace is case-sensitive. By default, a case-insensitive comparison is performed.", + "default": false + } + }, + "additionalProperties": false, + "required": [ + "replace", + "oldString", + "newString" + ] + }, + "trim": { + "type": "object", + "properties": { + "trim": { + "type": "object", + "title": "Trim", + "description": "Trim a string value by removing leading and trailings whitespace.", + "markdownDescription": "Trim a string value by removing leading and trailings whitespace.", + "$ref": "#/definitions/fn/definitions/function" + } + }, + "additionalProperties": false, + "required": [ + "trim" + ] + }, + "first": { + "type": "object", + "properties": { + "first": { + "oneOf": [ + { + "type": "array", + "description": "The first function returns the first element in the array.", + "markdownDescription": "The `first` function returns the first element in the array.", + "items": { + "$ref": "#/definitions/fn/definitions/function" + }, + "additionalItems": false, + "minItems": 2 + }, + { + "type": "object", + "description": "The first function returns the first element of arrays or the first character of a string.", + "markdownDescription": "The `first` function returns the first element of arrays or the first character of a string.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "first" + ] + }, + "last": { + "type": "object", + "properties": { + "last": { + "oneOf": [ + { + "type": "array", + "description": "The last function returns the last element in the array.", + "markdownDescription": "The `last` function returns the last element in the array.", + "items": { + "$ref": "#/definitions/fn/definitions/function" + }, + "additionalItems": false, + "minItems": 2 + }, + { + "type": "object", + "description": "The last function returns the last element of arrays or the last character of a string.", + "markdownDescription": "The `last` function returns the last element of arrays or the last character of a string.", + "$ref": "#/definitions/fn/definitions/function" + } + ] + } + }, + "additionalProperties": false, + "required": [ + "last" + ] + }, + "split": { + "type": "object", + "properties": { + "split": { + "oneOf": [ + { + "type": "string", + "description": "The split function returns converts a string into an array by spliting based on a delimiter.", + "markdownDescription": "The `split` function returns converts a string into an array by spliting based on a delimiter." + }, + { + "type": "object", + "description": "The split function returns converts a string into an array by spliting based on a delimiter.", + "markdownDescription": "The `split` function returns converts a string into an array by spliting based on a delimiter.", + "$ref": "#/definitions/fn/definitions/function" + } + ], + "default": {} + }, + "delimiter": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "additionalItems": false + } + ], + "default": [ + "" + ] + } + }, + "additionalProperties": false, + "required": [ + "split", + "delimiter" + ] } } } diff --git a/src/PSRule/Common/ArrayExtensions.cs b/src/PSRule/Common/ArrayExtensions.cs new file mode 100644 index 0000000000..2caf42f5eb --- /dev/null +++ b/src/PSRule/Common/ArrayExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace PSRule.Common +{ + /// + /// Extension methods for arrays. + /// + internal static class ArrayExtensions + { + internal static object Last(this Array array) + { + return array.Length > 0 ? array.GetValue(array.Length - 1) : null; + } + + internal static object First(this Array array) + { + return array.Length > 0 ? array.GetValue(0) : null; + } + } +} diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs index 3f6e51f8c4..8cd3e47a69 100644 --- a/src/PSRule/Common/ExpressionHelpers.cs +++ b/src/PSRule/Common/ExpressionHelpers.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Management.Automation; +using System.Reflection; using System.Text.RegularExpressions; using System.Threading; using Newtonsoft.Json.Linq; @@ -57,6 +58,9 @@ internal static bool Equal(object expectedValue, object actualValue, bool caseSe if (TryInt(expectedValue, convertExpected, out var i1) && TryInt(actualValue, convertActual, out var i2)) return i1 == i2; + if (TryArray(expectedValue, out var a1) && TryArray(actualValue, out var a2)) + return SequenceEqual(a1, a2); + var expectedBase = GetBaseObject(expectedValue); var actualBase = GetBaseObject(actualValue); if (expectedBase == null || actualBase == null) @@ -65,6 +69,69 @@ internal static bool Equal(object expectedValue, object actualValue, bool caseSe return expectedBase.Equals(actualBase) || expectedValue.Equals(actualValue); } + internal static bool SequenceEqual(Array array1, Array array2) + { + if (array1.Length != array2.Length) + return false; + + for (var i = 0; i < array1.Length; i++) + { + if (!Equal(array1.GetValue(i), array2.GetValue(i))) + return false; + } + return true; + } + + internal static bool Equal(object o1, object o2) + { + // One null + if (o1 == null || o2 == null) + return o1 == o2; + + // Arrays + if (o1 is Array array1 && o2 is Array array2) + return SequenceEqual(array1, array2); + else if (o1 is Array || o2 is Array) + return false; + + // String and int + if (TryString(o1, out var s1) && TryString(o2, out var s2)) + return s1 == s2; + else if (TryString(o1, out _) || TryString(o2, out _)) + return false; + else if (TryLong(o1, false, out var i1) && TryLong(o2, false, out var i2)) + return i1 == i2; + else if (TryLong(o1, false, out var _) || TryLong(o2, false, out var _)) + return false; + + // JTokens + if (o1 is JToken t1 && o2 is JToken t2) + return JTokenEquals(t1, t2); + + // Objects + return ObjectEquals(o1, o2); + } + + private static bool JTokenEquals(JToken t1, JToken t2) + { + return JToken.DeepEquals(t1, t2); + } + + internal static bool ObjectEquals(object o1, object o2) + { + var objectType = o1.GetType(); + if (objectType != o2.GetType()) + return false; + + var props = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty); + for (var i = 0; i < props.Length; i++) + { + if (!object.Equals(props[i].GetValue(o1), props[i].GetValue(o2))) + return false; + } + return true; + } + internal static int Compare(object left, object right) { if (TryString(left, out var stringLeft) && TryString(right, out var stringRight)) @@ -162,13 +229,18 @@ internal static bool TryString(object o, bool convert, out string value) internal static bool TryArray(object o, out Array value) { o = GetBaseObject(o); + value = null; + if (o is string) return false; if (o is Array a) - { value = a; - return true; - } - value = null; - return false; + + else if (o is JArray jArray) + value = jArray.Values().ToArray(); + + else if (o is IEnumerable e) + value = e.OfType().ToArray(); + + return value != null; } internal static bool TryConvertStringArray(object o, out string[] value) diff --git a/src/PSRule/Common/StringExtensions.cs b/src/PSRule/Common/StringExtensions.cs index 4e4b02c694..d2a4af20fc 100644 --- a/src/PSRule/Common/StringExtensions.cs +++ b/src/PSRule/Common/StringExtensions.cs @@ -1,8 +1,9 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; +using System.Text; namespace PSRule { @@ -78,5 +79,31 @@ public static bool Contains(this string source, string value, StringComparison c { return source?.IndexOf(value, comparison) >= 0; } + + public static string Replace(this string s, string oldString, string newString, bool caseSensitive) + { + if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(oldString) || s.Length < oldString.Length) + return s; + + if (caseSensitive) + return s.Replace(oldString, newString); + + var sb = new StringBuilder(s.Length); + var pos = 0; + var replaceWithEmpty = string.IsNullOrEmpty(newString); + int indexAt; + while ((indexAt = s.IndexOf(oldString, pos, StringComparison.OrdinalIgnoreCase)) != -1) + { + sb.Append(s, pos, indexAt - pos); + if (!replaceWithEmpty) + sb.Append(newString); + + pos = indexAt + oldString.Length; + } + if (pos < s.Length) + sb.Append(s, pos, s.Length - pos); + + return sb.ToString(); + } } } diff --git a/src/PSRule/Definitions/Expressions/Functions.cs b/src/PSRule/Definitions/Expressions/Functions.cs index 6262145d1b..d260714f61 100644 --- a/src/PSRule/Definitions/Expressions/Functions.cs +++ b/src/PSRule/Definitions/Expressions/Functions.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Linq; using System.Text; using System.Threading; +using PSRule.Common; using PSRule.Resources; using PSRule.Runtime; using static PSRule.Definitions.Expressions.LanguageExpression; @@ -22,6 +25,15 @@ internal static class Functions private const string CONFIGURATION = "configuration"; private const string PATH = "path"; private const string LENGTH = "length"; + private const string REPLACE = "replace"; + private const string TRIM = "trim"; + private const string FIRST = "first"; + private const string LAST = "last"; + private const string SPLIT = "split"; + private const string DELIMITER = "delimiter"; + private const string OLDSTRING = "oldstring"; + private const string NEWSTRING = "newstring"; + private const string CASESENSITIVE = "casesensitive"; /// /// The available built-in functions. @@ -35,6 +47,11 @@ internal static class Functions new FunctionDescriptor(INTEGER, Integer), new FunctionDescriptor(CONCAT, Concat), new FunctionDescriptor(SUBSTRING, Substring), + new FunctionDescriptor(REPLACE, Replace), + new FunctionDescriptor(TRIM, Trim), + new FunctionDescriptor(FIRST, First), + new FunctionDescriptor(LAST, Last), + new FunctionDescriptor(SPLIT, Split), }; private static ExpressionFnOuter Boolean(IExpressionContext context, PropertyBag properties) @@ -144,6 +161,89 @@ private static ExpressionFnOuter Substring(IExpressionContext context, PropertyB }; } + private static ExpressionFnOuter Replace(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !properties.TryGetString(OLDSTRING, out var oldString) || + !properties.TryGetString(NEWSTRING, out var newString) || + !TryProperty(properties, REPLACE, out ExpressionFnOuter next)) + return null; + + var caseSensitive = properties.TryGetBool(CASESENSITIVE, out var cs) && cs.HasValue && cs.Value; + return (context) => + { + var value = next(context); + if (ExpressionHelpers.TryString(value, out var originalString)) + return originalString.Length > 0 && oldString.Length > 0 ? originalString.Replace(oldString, newString, caseSensitive) : originalString; + + return null; + }; + } + + private static ExpressionFnOuter Trim(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, TRIM, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + return ExpressionHelpers.TryString(value, out var s) ? s.Trim() : null; + }; + } + + private static ExpressionFnOuter First(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, FIRST, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + if (ExpressionHelpers.TryString(value, out var s)) + return s.Length > 0 ? new string(s[0], 1) : null; + + return ExpressionHelpers.TryArray(value, out var array) ? Value(context, array.First()) : null; + }; + } + + private static ExpressionFnOuter Last(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !TryProperty(properties, LAST, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + if (ExpressionHelpers.TryString(value, out var s)) + return s.Length > 0 ? new string(s[s.Length - 1], 1) : null; + + return ExpressionHelpers.TryArray(value, out var array) ? Value(context, array.Last()) : null; + }; + } + + private static ExpressionFnOuter Split(IExpressionContext context, PropertyBag properties) + { + if (properties == null || + properties.Count == 0 || + !properties.TryGetStringArray(DELIMITER, out var delimiter) || + !TryProperty(properties, SPLIT, out ExpressionFnOuter next)) + return null; + + return (context) => + { + var value = next(context); + return ExpressionHelpers.TryString(value, out var s) ? s.Split(delimiter, options: StringSplitOptions.None) : 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 87772ce402..036fe41e61 100644 --- a/tests/PSRule.Tests/FunctionTests.cs +++ b/tests/PSRule.Tests/FunctionTests.cs @@ -237,6 +237,196 @@ public void Path() Assert.Null(fn(context, properties)(context)); } + [Fact] + public void Replace() + { + var context = GetContext(); + var fn = GetFunction("replace"); + + var properties = new LanguageExpression.PropertyBag + { + { "oldString", "12" }, + { "newString", "" }, + { "replace", "Test123" } + }; + Assert.Equal("Test3", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "oldString", "456" }, + { "newString", "" }, + { "replace", "Test123" } + }; + Assert.Equal("Test123", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "oldString", "456" }, + { "newString", "" }, + { "replace", "" } + }; + Assert.Equal("", fn(context, properties)(context)); + } + + [Fact] + public void Trim() + { + var context = GetContext(); + var fn = GetFunction("trim"); + + var properties = new LanguageExpression.PropertyBag + { + { "trim", " test " } + }; + Assert.Equal("test", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "trim", "test" } + }; + Assert.Equal("test", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "trim", "\r\ntest\r\n" } + }; + Assert.Equal("test", fn(context, properties)(context)); + } + + [Fact] + public void First() + { + var context = GetContext(); + var fn = GetFunction("first"); + + // String + var properties = new LanguageExpression.PropertyBag + { + { "first", "Test123" } + }; + Assert.Equal("T", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "first", "" } + }; + Assert.Null(fn(context, properties)(context)); + + // Array + properties = new LanguageExpression.PropertyBag + { + { "first", new string[] { "one", "two", "three" } } + }; + Assert.Equal("one", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "first", new int[] { 1, 2, 3 } } + }; + Assert.Equal(1, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "first", Array.Empty() } + }; + Assert.Null(fn(context, properties)(context)); + } + + [Fact] + public void Last() + { + var context = GetContext(); + var fn = GetFunction("last"); + + // String + var properties = new LanguageExpression.PropertyBag + { + { "last", "Test123" } + }; + Assert.Equal("3", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "last", "" } + }; + Assert.Null(fn(context, properties)(context)); + + // Array + properties = new LanguageExpression.PropertyBag + { + { "last", new string[] { "one", "two", "three" } } + }; + Assert.Equal("three", fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "last", new int[] { 1, 2, 3 } } + }; + Assert.Equal(3, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "last", Array.Empty() } + }; + Assert.Null(fn(context, properties)(context)); + } + + [Fact] + public void Split() + { + var context = GetContext(); + var fn = GetFunction("split"); + + var properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", " " } + }; + Assert.Equal(new string[] { "One", "Two", "Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", " Two " } + }; + Assert.Equal(new string[] { "One", "Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", "/" } + }; + Assert.Equal(new string[] { "One Two Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "" }, + { "delimiter", "/" } + }; + Assert.Equal(new string[] { "" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", new string[] { " Two " } } + }; + Assert.Equal(new string[] { "One", "Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", "One Two Three" }, + { "delimiter", new string[] { " ", "Two" } } + }; + Assert.Equal(new string[] { "One", "", "", "Three" }, fn(context, properties)(context)); + + properties = new LanguageExpression.PropertyBag + { + { "split", null }, + { "delimiter", new string[] { " ", "Two" } } + }; + Assert.Null(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 4caba42731..452c3ca9f6 100644 --- a/tests/PSRule.Tests/Functions.Rule.jsonc +++ b/tests/PSRule.Tests/Functions.Rule.jsonc @@ -1,133 +1,244 @@ [ - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example1" + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example1" + }, + "spec": { + "if": { + "value": { + "$": { + "substring": { + "path": "name" + }, + "length": 7 + } }, - "spec": { - "if": { - "value": { - "$": { - "substring": { - "path": "name" - }, - "length": 7 - } - }, - "equals": "TestObj" - } - } + "equals": "TestObj" + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example2" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example2" + "spec": { + "if": { + "value": { + "$": { + "configuration": "ConfigArray" + } }, - "spec": { - "if": { - "value": { - "$": { - "configuration": "ConfigArray" - } - }, - "count": 5 - } - } + "count": 5 + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example3" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example3" + "spec": { + "if": { + "value": { + "$": { + "boolean": true + } }, - "spec": { - "if": { - "value": { - "$": { - "boolean": true - } - }, - "equals": true - } - } + "equals": true + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example4" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example4" + "spec": { + "if": { + "value": { + "$": { + "concat": [ + { + "path": "name" + }, + { + "string": "-" + }, + { + "path": "name" + } + ] + } }, - "spec": { - "if": { - "value": { - "$": { - "concat": [ - { - "path": "name" - }, - { - "string": "-" - }, - { - "path": "name" - } - ] - } - }, - "equals": "TestObject1-TestObject1" - } - } + "equals": "TestObject1-TestObject1" + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example5" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example5" + "spec": { + "if": { + "value": { + "$": { + "integer": 6 + } }, - "spec": { - "if": { - "value": { - "$": { - "integer": 6 - } - }, - "greater": 5 - } + "greater": 5 + } + } + }, + { + // Synopsis: An expression function example. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Example6" + }, + "spec": { + "if": { + "value": "TestObject1-TestObject1", + "equals": { + "$": { + "concat": [ + { + "path": "name" + }, + { + "string": "-" + }, + { + "path": "name" + } + ] + } } + } + } + }, + { + // Synopsis: A test for the replace function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Replace" }, - { - // Synopsis: An expression function example. - "apiVersion": "github.com/microsoft/PSRule/v1", - "kind": "Selector", - "metadata": { - "name": "Json.Fn.Example6" + "spec": { + "if": { + "value": { + "$": { + "replace": { + "string": " test one " + }, + "oldString": " ", + "newString": "" + } }, - "spec": { - "if": { - "value": "TestObject1-TestObject1", - "equals": { - "$": { - "concat": [ - { - "path": "name" - }, - { - "string": "-" - }, - { - "path": "name" - } - ] - } - } + "equals": "testone" + } + } + }, + { + // Synopsis: A test for the trim function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Trim" + }, + "spec": { + "if": { + "value": { + "$": { + "trim": { + "string": " test " } - } + } + }, + "equals": "test" + } + } + }, + { + // Synopsis: A test for the first function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.First" + }, + "spec": { + "if": { + "value": { + "$": { + "first": [ + "abc", + "def" + ] + } + }, + "equals": "abc" + } + } + }, + { + // Synopsis: A test for the last function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Last" + }, + "spec": { + "if": { + "value": { + "$": { + "last": [ + "abc", + "def" + ] + } + }, + "equals": "def" + } + } + }, + { + // Synopsis: A test for the split function. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "Json.Fn.Split" + }, + "spec": { + "if": { + "value": { + "$": { + "split": { + "string": "One Two Three" + }, + "delimiter": [ + " " + ] + } + }, + "equals": [ + "One", + "Two", + "Three" + ] + } } + } ] diff --git a/tests/PSRule.Tests/Functions.Rule.yaml b/tests/PSRule.Tests/Functions.Rule.yaml index ba3f42f0eb..5d3b64752e 100644 --- a/tests/PSRule.Tests/Functions.Rule.yaml +++ b/tests/PSRule.Tests/Functions.Rule.yaml @@ -86,3 +86,82 @@ spec: - path: name - string: '-' - path: name + +--- +# Synopsis: A test for the replace function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Replace +spec: + if: + value: + $: + replace: + string: ' test one ' + oldString: ' ' + newString: '' + equals: testone + +--- +# Synopsis: A test for the trim function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Trim +spec: + if: + value: + $: + trim: + string: ' test ' + equals: test + +--- +# Synopsis: A test for the first function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.First +spec: + if: + value: + $: + first: + - string: abc + - string: def + equals: abc + +--- +# Synopsis: A test for the last function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Last +spec: + if: + value: + $: + last: + - string: abc + - string: def + equals: def + +--- +# Synopsis: A test for the split function. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: Yaml.Fn.Split +spec: + if: + value: + $: + split: + string: One Two Three + delimiter: + - ' ' + equals: + - One + - Two + - Three diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index a5d143ba67..b7f64822b7 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -1762,6 +1762,27 @@ public void WithFunction(string type, string path) Assert.True(example6.Match(actual1)); } + [Theory] + [InlineData("Yaml", FunctionsYamlFileName)] + [InlineData("Json", FunctionsJsonFileName)] + public void WithFunctionSpecific(string type, string path) + { + var example1 = GetSelectorVisitor($"{type}.Fn.Replace", GetSource(path), out _); + var example2 = GetSelectorVisitor($"{type}.Fn.Trim", GetSource(path), out _); + 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 actual1 = GetObject( + (name: "Name", value: "TestObject1") + ); + + Assert.True(example1.Match(actual1)); + Assert.True(example2.Match(actual1)); + Assert.True(example3.Match(actual1)); + Assert.True(example4.Match(actual1)); + Assert.True(example5.Match(actual1)); + } + #endregion Functions #region Helper methods diff --git a/tests/PSRule.Tests/StringExtensionsTests.cs b/tests/PSRule.Tests/StringExtensionsTests.cs new file mode 100644 index 0000000000..b3bc833010 --- /dev/null +++ b/tests/PSRule.Tests/StringExtensionsTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit; + +namespace PSRule +{ + public sealed class StringExtensionsTests + { + [Fact] + public void Replace() + { + var actual = "One Two Three"; + Assert.Equal("OneTwoThree", actual.Replace(" ", "", caseSensitive: true)); + Assert.Equal("OneTwoThree", actual.Replace(" ", "", caseSensitive: false)); + Assert.Equal("One Two ", actual.Replace("Three", "", caseSensitive: true)); + Assert.Equal("One Two ", actual.Replace("Three", "", caseSensitive: false)); + Assert.Equal(" Two Three", actual.Replace("One", "", caseSensitive: true)); + Assert.Equal(" Two Three", actual.Replace("One", "", caseSensitive: false)); + Assert.Equal("One 2 Three", actual.Replace("Two", "2", caseSensitive: true)); + Assert.Equal("One 2 Three", actual.Replace("Two", "2", caseSensitive: false)); + Assert.Equal("One Two Three", actual.Replace("two", "2", caseSensitive: true)); + Assert.Equal("One 2 Three", actual.Replace("two", "2", caseSensitive: false)); + Assert.Equal("One Two Three", actual.Replace("three", "3", caseSensitive: true)); + Assert.Equal("One Two 3", actual.Replace("three", "3", caseSensitive: false)); + Assert.Equal("One Two Three", actual.Replace("one", "1", caseSensitive: true)); + Assert.Equal("1 Two Three", actual.Replace("one", "1", caseSensitive: false)); + } + } +}