Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new function support #1227 #1368

Merged
merged 1 commit into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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