Skip to content

Commit

Permalink
Added new function support #1227 (#1368)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Dec 6, 2022
1 parent ea9f21c commit 9b5fe16
Show file tree
Hide file tree
Showing 12 changed files with 955 additions and 126 deletions.
3 changes: 3 additions & 0 deletions docs/CHANGELOG-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 8 additions & 3 deletions docs/expressions/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
168 changes: 168 additions & 0 deletions schemas/PSRule-language.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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"
]
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/PSRule/Common/ArrayExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace PSRule.Common
{
/// <summary>
/// Extension methods for arrays.
/// </summary>
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;
}
}
}
82 changes: 77 additions & 5 deletions src/PSRule/Common/ExpressionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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<object>().ToArray();

else if (o is IEnumerable e)
value = e.OfType<object>().ToArray();

return value != null;
}

internal static bool TryConvertStringArray(object o, out string[] value)
Expand Down
29 changes: 28 additions & 1 deletion src/PSRule/Common/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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();
}
}
}
Loading

0 comments on commit 9b5fe16

Please sign in to comment.