This commit is contained in:
Bernie White 2022-08-17 23:47:39 +10:00 коммит произвёл GitHub
Родитель 13357d76bf
Коммит b37a09832c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
35 изменённых файлов: 3009 добавлений и 782 удалений

13
.vscode/settings.json поставляемый
Просмотреть файл

@ -58,6 +58,18 @@
],
"files.insertFinalNewline": true
},
"[jsonc]": {
"editor.tabSize": 4,
"editor.tabCompletion": "on",
"editor.quickSuggestions": {
"strings": true
},
"editor.suggest.insertMode": "replace",
"gitlens.codeLens.scopes": [
"document"
],
"files.insertFinalNewline": true
},
"files.associations": {
"**/.azure-pipelines/*.yaml": "azure-pipelines",
"**/.azure-pipelines/jobs/*.yaml": "azure-pipelines",
@ -67,6 +79,7 @@
"APPSERVICEMININSTANCECOUNT",
"cmdlet",
"cmdlets",
"concat",
"datetime",
"deserialize",
"deserialized",

Просмотреть файл

@ -11,8 +11,21 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
[2]: https://microsoft.github.io/PSRule/latest/deprecations/#deprecations-for-v3
**Experimental features**:
- Functions within YAML expressions can be used to perform manipulation prior to testing a condition.
## Unreleased
What's changed since pre-release v2.4.0-B0022:
- New features:
- **Experimental**: Added support for functions within YAML and JSON expressions.
[#1227](https://github.com/microsoft/PSRule/issues/1227)
- Added conversion functions `boolean`, `string`, and `integer`.
- Added lookup functions `configuration`, and `path`.
- Added string functions `concat`, `substring`.
## v2.4.0-B0022 (pre-release)
What's changed since pre-release v2.4.0-B0009:

Просмотреть файл

@ -0,0 +1,127 @@
# Expression functions
!!! Abstract
Functions are an advanced lanaguage feature specific to YAML and JSON resources.
That extend the language to allow for more complex use cases with expressions.
!!! Experimental
Functions are a work in progress and subject to change.
We hope to add more functions, broader support, and more detailed documentation in the future.
[Join or start a disucssion][1] to let us know how we can improve this feature going forward.
[1]: https://github.com/microsoft/PSRule/discussions
## Using functions
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.
- `path` - Get a value from an object path.
## Supported conditions
Currently functions are only supported on a subset of conditions.
The conditions that are supported are:
- `equals`
- `notEquals`
- `count`
- `less`
- `lessOrEquals`
- `greater`
- `greaterOrEquals`
## Examples
```yaml
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example1
spec:
if:
value:
$:
substring:
path: name
length: 7
equals: TestObj
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example2
spec:
if:
value:
$:
configuration: 'ConfigArray'
count: 5
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example3
spec:
if:
value:
$:
boolean: true
equals: true
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example4
spec:
if:
value:
$:
concat:
- path: name
- string: '-'
- path: name
equals: TestObject1-TestObject1
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example5
spec:
if:
value:
$:
integer: 6
greater: 5
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example6
spec:
if:
value: TestObject1-TestObject1
equals:
$:
concat:
- path: name
- string: '-'
- path: name
```

Просмотреть файл

@ -0,0 +1,34 @@
# PSRule function expressions spec (draft)
This is a spec for implementing function expressions in PSRule v2.
## Synopsis
Functions are available to handle complex conditions within YAML and JSON expressions.
## Schema driven
While functions allow handing for complex use cases, they should still remain schema driven.
A schema driven design allows auto-completion and validation during authoring in a broad set of tools.
## Syntax
Functions can be used within YAML and JSON expressions by using the `$` object property.
For example:
```yaml
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example1
spec:
if:
value:
$:
substring:
path: name
length: 3
equals: abc
```

Просмотреть файл

@ -55,6 +55,8 @@ nav:
- Azure resource tagging example: scenarios/azure-tags/azure-tags.md
- Kubernetes resource validation example: scenarios/kubernetes-resources/kubernetes-resources.md
- Concepts:
- Expressions:
- Functions: expressions/functions.md
- Using within continuous integration: scenarios/validation-pipeline/validation-pipeline.md
# - Troubleshooting: troubleshooting.md
- License and contributing: license-contributing.md

Просмотреть файл

@ -1,23 +1,26 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"oneOf": [
{
"$ref": "#/definitions/rule-v1"
},
{
"$ref": "#/definitions/baseline-v1"
},
{
"$ref": "#/definitions/moduleConfig-v1"
},
{
"$ref": "#/definitions/selector-v1"
},
{
"$ref": "#/definitions/suppressionGroup-v1"
}
],
"$ref": "#/definitions/resource-v1",
"definitions": {
"resource-v1": {
"oneOf": [
{
"$ref": "#/definitions/rule-v1"
},
{
"$ref": "#/definitions/baseline-v1"
},
{
"$ref": "#/definitions/moduleConfig-v1"
},
{
"$ref": "#/definitions/selector-v1"
},
{
"$ref": "#/definitions/suppressionGroup-v1"
}
]
},
"resource-metadata": {
"type": "object",
"title": "Metadata",
@ -808,6 +811,9 @@
"oneOf": [
{
"$ref": "#/definitions/selectorPropertyField"
},
{
"$ref": "#/definitions/selectorPropertyValue"
}
]
},
@ -816,6 +822,9 @@
{
"$ref": "#/definitions/selectorPropertyField"
},
{
"$ref": "#/definitions/selectorPropertyValue"
},
{
"$ref": "#/definitions/selectorPropertyType"
},
@ -841,6 +850,16 @@
"field"
]
},
"selectorPropertyValue": {
"properties": {
"value": {
"$ref": "#/definitions/selectorExpressionValue"
}
},
"required": [
"value"
]
},
"selectorPropertyType": {
"properties": {
"type": {
@ -1270,10 +1289,18 @@
"type": "object",
"properties": {
"greater": {
"type": "integer",
"title": "Greater",
"description": "Must be greater then the specified value.",
"markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)"
"markdownDescription": "Must be greater then the specified value. [See help](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Expressions/#greater)",
"oneOf": [
{
"type": "integer"
},
{
"type": "object",
"$ref": "#/definitions/fn"
}
]
},
"convert": {
"type": "boolean",
@ -1936,15 +1963,351 @@
"selectorExpressionValue": {
"oneOf": [
{
"type": "string"
"type": "string",
"title": "Value from string",
"description": "A value to compare."
},
{
"type": "boolean"
"type": "boolean",
"title": "Value from boolean",
"description": "A value to compare."
},
{
"type": "integer"
"type": "integer",
"title": "Value from integer",
"description": "A value to compare."
},
{
"type": "object",
"title": "Value for object",
"description": "A value to compare.",
"not": {
"propertyNames": {
"enum": [
"$"
]
}
}
},
{
"$ref": "#/definitions/fn"
}
]
},
"fn": {
"title": "Value from function",
"description": "A function expression that once evaluated specifies the value.",
"markdownDescription": "A function expression that once evaluated specifies the value.",
"properties": {
"$": {
"type": "object",
"title": "Value from function",
"description": "A function expression that once evaluated specifies the value.",
"markdownDescription": "A function expression that once evaluated specifies the value.",
"$ref": "#/definitions/fn/definitions/function"
}
},
"required": [
"$"
],
"definitions": {
"function": {
"oneOf": [
{
"$ref": "#/definitions/fn/definitions/function/definitions/substring"
},
{
"$ref": "#/definitions/fn/definitions/function/definitions/string"
},
{
"$ref": "#/definitions/fn/definitions/function/definitions/boolean"
},
{
"$ref": "#/definitions/fn/definitions/function/definitions/integer"
},
{
"$ref": "#/definitions/fn/definitions/function/definitions/concat"
},
{
"$ref": "#/definitions/fn/definitions/function/definitions/path"
},
{
"$ref": "#/definitions/fn/definitions/function/definitions/configuration"
}
],
"definitions": {
"equal": {
"type": "object",
"properties": {
"equal": {
"type": "array",
"title": "Equal",
"description": "The equal operator checks for equity between two operands.",
"markdownDescription": "The `equal` operator checks for equity two operands.",
"items": {
"oneOf": [
{
"$ref": "#/definitions/fn/definitions/function"
},
{
"type": "string"
},
{
"type": "integer"
},
{
"type": "boolean"
}
]
},
"additionalItems": false,
"minItems": 2,
"maxItems": 2
}
},
"additionalProperties": false,
"required": [
"equal"
]
},
"substring": {
"type": "object",
"properties": {
"substring": {
"title": "Substring",
"description": "The substring function, copies a number of characters from input starting from a zero based position.",
"markdownDescription": "The `substring` function, copies a number of characters from input starting from a zero based position.",
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"$ref": "#/definitions/fn/definitions/function",
"defaultSnippets": [
{
"label": "From string",
"description": "Set value to a specific value.",
"body": ""
},
{
"label": "From configuration",
"description": "Configure length from configuration.",
"body": {
"configuration": "${1}"
}
}
]
}
]
},
"start": {
"title": "Start",
"description": "The zero based position to start copying characters from.",
"default": 0,
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "object",
"$ref": "#/definitions/fn/definitions/function"
}
],
"defaultSnippets": [
{
"label": "From integer",
"description": "Set start to a specific value.",
"body": 0
},
{
"label": "From configuration",
"description": "Configure start from configuration.",
"body": {
"configuration": "${1}"
}
}
]
},
"length": {
"title": "Length",
"description": "The number of character to copy from the source string.",
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "object",
"$ref": "#/definitions/fn/definitions/function"
}
],
"defaultSnippets": [
{
"label": "From integer",
"description": "Set length to a specific value.",
"body": 0
},
{
"label": "From configuration",
"description": "Configure length from configuration.",
"body": {
"configuration": "${1}"
}
}
]
}
},
"additionalProperties": false,
"required": [
"substring"
],
"defaultSnippets": [
{
"label": "Substring from string",
"description": "Set value to a specific value.",
"body": {
"substring": "${1}",
"length": "${2}"
}
},
{
"label": "Substring from configuration",
"description": "Configure length from configuration.",
"body": {
"substring": {
"configuration": "${1}"
},
"length": "${2}"
}
}
]
},
"string": {
"type": "object",
"properties": {
"string": {
"title": "String",
"oneOf": [
{
"type": "string",
"description": "A literal string value."
},
{
"type": "object",
"description": "Converts the operand in to a string value.",
"$ref": "#/definitions/fn/definitions/function"
}
]
}
},
"additionalProperties": false,
"required": [
"string"
]
},
"integer": {
"type": "object",
"properties": {
"integer": {
"title": "Integer",
"oneOf": [
{
"type": "integer",
"description": "A literal integer value."
},
{
"type": "object",
"description": "Converts the operand in to an integer.",
"$ref": "#/definitions/fn/definitions/function"
}
]
}
},
"additionalProperties": false,
"required": [
"integer"
]
},
"boolean": {
"type": "object",
"properties": {
"boolean": {
"title": "Boolean",
"oneOf": [
{
"type": "boolean",
"description": "A literal boolean value.",
"markdownDescription": "A literal boolean value."
},
{
"type": "object",
"description": "Converts the operand to a boolean value.",
"markdownDescription": "Converts the operand to a boolean value.",
"$ref": "#/definitions/fn/definitions/function"
}
]
}
},
"additionalProperties": false,
"required": [
"boolean"
]
},
"concat": {
"type": "object",
"properties": {
"concat": {
"type": "array",
"description": "The concat function combines two or more operands.",
"markdownDescription": "The `concat` function combines two or more operands.",
"items": {
"$ref": "#/definitions/fn/definitions/function"
},
"additionalItems": false,
"minItems": 2
}
},
"additionalProperties": false,
"required": [
"concat"
]
},
"path": {
"type": "object",
"properties": {
"path": {
"type": "string",
"title": "Path",
"description": "The path function returns a value from an object path.",
"markdownDescription": "The `path` function returns a value from an object path."
}
},
"additionalProperties": false,
"required": [
"path"
]
},
"configuration": {
"type": "object",
"properties": {
"configuration": {
"type": "string",
"title": "Configuration",
"description": "The configuration function returns a value retrieved from configuration by name.",
"markdownDescription": "The `configuration` function returns a value retrieved from configuration by name.",
"minLength": 1
}
},
"additionalProperties": false,
"required": [
"configuration"
]
}
}
}
}
}
}
}
}

Просмотреть файл

@ -1,7 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -83,9 +84,24 @@ namespace PSRule
if (!dictionary.TryGetValue(key, out var o))
return false;
if (o is long lvalue || (o is string svalue && long.TryParse(svalue, out lvalue)))
if (ExpressionHelpers.TryLong(o, true, out var i_value))
{
value = lvalue;
value = i_value;
return true;
}
return false;
}
[DebuggerStepThrough]
public static bool TryGetInt(this IDictionary<string, object> dictionary, string key, out int? value)
{
value = null;
if (!dictionary.TryGetValue(key, out var o))
return false;
if (ExpressionHelpers.TryInt(o, true, out var i_value))
{
value = i_value;
return true;
}
return false;
@ -106,6 +122,21 @@ namespace PSRule
return false;
}
[DebuggerStepThrough]
public static bool TryGetEnumerable(this IDictionary<string, object> dictionary, string key, out IEnumerable value)
{
value = null;
if (!dictionary.TryGetValue(key, out var o))
return false;
if (o is IEnumerable evalue)
{
value = evalue;
return true;
}
return false;
}
[DebuggerStepThrough]
public static bool TryGetStringArray(this IDictionary<string, object> dictionary, string key, out string[] value)
{

Просмотреть файл

@ -65,6 +65,16 @@ namespace PSRule
return expectedBase.Equals(actualBase) || expectedValue.Equals(actualValue);
}
internal static int Compare(object left, object right)
{
if (TryString(left, out var stringLeft) && TryString(right, out var stringRight))
return StringComparer.Ordinal.Compare(stringLeft, stringRight);
else if (CompareNumeric(left, right, convert: false, out var compare, out _))
return compare;
return Comparer.Default.Compare(left, right);
}
internal static bool CompareNumeric(object actual, object expected, bool convert, out int compare, out object value)
{
if (TryInt(actual, convert, out var actualInt) && TryInt(expected, convert: true, value: out var expectedInt))
@ -131,9 +141,19 @@ namespace PSRule
value = evalue.ToString();
return true;
}
else if (convert && TryInt(o, false, out var ivalue))
else if (convert && TryLong(o, false, out var l_value))
{
value = ivalue.ToString(Thread.CurrentThread.CurrentCulture);
value = l_value.ToString(Thread.CurrentThread.CurrentCulture);
return true;
}
else if (convert && TryBool(o, false, out var b_value))
{
value = b_value.ToString(Thread.CurrentThread.CurrentCulture);
return true;
}
else if (convert && TryInt(o, false, out var i_value))
{
value = i_value.ToString(Thread.CurrentThread.CurrentCulture);
return true;
}
return false;
@ -215,6 +235,11 @@ namespace PSRule
value = bvalue;
return true;
}
else if (convert && TryLong(o, convert: false, out var lvalue))
{
value = lvalue > 0;
return true;
}
value = default;
return false;
}
@ -525,7 +550,7 @@ namespace PSRule
/// </summary>
internal static object GetBaseObject(object o)
{
return o is PSObject pso && pso.BaseObject != null && !(pso.BaseObject is PSCustomObject) ? pso.BaseObject : o;
return o is PSObject pso && pso.BaseObject != null && pso.BaseObject is not PSCustomObject ? pso.BaseObject : o;
}
private static PSRuleTargetInfo GetTargetInfo(object o)

Просмотреть файл

@ -21,7 +21,7 @@ using PSRule.Runtime;
namespace PSRule
{
/// <summary>
/// A base <c>PSObject</c> converter.
/// A base <seealso cref="PSObject"/> converter.
/// </summary>
internal abstract class PSObjectBaseConverter : JsonConverter
{
@ -345,7 +345,7 @@ namespace PSRule
}
/// <summary>
/// A customer deserializer to convert JSON into ResourceObject
/// A custom deserializer to convert JSON into ResourceObject
/// </summary>
internal sealed class ResourceObjectJsonConverter : JsonConverter
{
@ -385,6 +385,7 @@ namespace PSRule
private IResource MapResource(JsonReader reader, JsonSerializer serializer)
{
reader.GetSourceExtent(RunspaceContext.CurrentThread.Source.File.Path, out var extent);
reader.SkipComments();
if (reader.TokenType != JsonToken.StartObject || !reader.Read())
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
@ -619,23 +620,24 @@ namespace PSRule
}
/// <summary>
/// A JSON converter for deserializing Language Expressions
/// A custom converter for deserializing JSON into a language expression.
/// </summary>
internal sealed class LanguageExpressionJsonConverter : JsonConverter
{
private const string OPERATOR_IF = "if";
public override bool CanRead => true;
public override bool CanWrite => false;
private readonly LanguageExpressionFactory _Factory;
private readonly FunctionBuilder _FunctionBuilder;
public LanguageExpressionJsonConverter()
{
_Factory = new LanguageExpressionFactory();
_FunctionBuilder = new FunctionBuilder();
}
public override bool CanRead => true;
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return typeof(LanguageExpression).IsAssignableFrom(objectType);
@ -653,32 +655,24 @@ namespace PSRule
}
/// <summary>
/// Skip JSON comments.
/// Map an operator.
/// </summary>
private static bool SkipComments(JsonReader reader)
{
var hasComments = false;
while (reader.TokenType == JsonToken.Comment && reader.Read())
hasComments = true;
return hasComments;
}
private LanguageExpression MapOperator(string type, JsonReader reader)
{
if (TryExpression(type, out LanguageOperator result))
{
// If and Not
if (reader.TokenType == JsonToken.StartObject)
{
result.Add(MapExpression(reader));
reader.Read();
}
// AllOf and AnyOf
else if (reader.TokenType == JsonToken.StartArray && reader.Read())
{
while (reader.TokenType != JsonToken.EndArray)
{
if (SkipComments(reader))
if (reader.SkipComments())
continue;
result.Add(MapExpression(reader));
@ -687,7 +681,6 @@ namespace PSRule
reader.Read();
}
}
return result;
}
@ -707,47 +700,96 @@ namespace PSRule
private LanguageExpression MapExpression(JsonReader reader)
{
LanguageExpression result = null;
var properties = new LanguageExpression.PropertyBag();
MapProperty(properties, reader, out var key);
if (key != null && TryCondition(key))
{
result = MapCondition(key, properties, reader);
}
else if (TryOperator(key) &&
(reader.TokenType == JsonToken.StartObject ||
reader.TokenType == JsonToken.StartArray))
else if ((reader.TokenType == JsonToken.StartObject || reader.TokenType == JsonToken.StartArray) &&
TryOperator(key))
{
result = MapOperator(key, reader);
}
return result;
}
private ExpressionFnOuter MapFunction(string type, JsonReader reader)
{
_FunctionBuilder.Push();
while (reader.TokenType != JsonToken.EndObject)
{
var name = reader.Value as string;
if (name != null)
{
reader.Consume(JsonToken.PropertyName);
if (reader.TryConsume(JsonToken.StartObject))
{
var child = MapFunction(name, reader);
_FunctionBuilder.Add(name, child);
reader.Consume(JsonToken.EndObject);
}
else if (reader.TryConsume(JsonToken.StartArray))
{
var sequence = MapSequence(name, reader);
_FunctionBuilder.Add(name, sequence);
reader.Consume(JsonToken.EndArray);
}
else
{
_FunctionBuilder.Add(name, reader.Value);
reader.Read();
}
}
}
var result = _FunctionBuilder.Pop();
return result;
}
private object MapSequence(string name, JsonReader reader)
{
var result = new List<object>();
while (reader.TokenType != JsonToken.EndArray)
{
if (reader.TryConsume(JsonToken.StartObject))
{
var child = MapFunction(name, reader);
result.Add(child);
reader.Consume(JsonToken.EndObject);
}
else
{
result.Add(reader.Value);
reader.Read();
}
}
return result.ToArray();
}
private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader reader, out string name)
{
if (reader.TokenType != JsonToken.StartObject || !reader.Read())
{
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
}
name = null;
while (reader.TokenType != JsonToken.EndObject)
{
var key = reader.Value.ToString();
if (TryCondition(key) || TryOperator(key))
{
name = key;
}
if (reader.Read())
{
if (reader.TokenType == JsonToken.StartObject)
if (TryValue(key, reader, out var value))
{
properties[key] = value;
}
else if (TryCondition(key) && reader.TryConsume(JsonToken.StartObject))
{
if (TryFunction(reader, key, out var fn))
properties.Add(key, fn);
}
else if (reader.TokenType == JsonToken.StartObject)
break;
else if (reader.TokenType == JsonToken.StartArray)
@ -758,12 +800,12 @@ namespace PSRule
var objects = new List<string>();
while (reader.TokenType != JsonToken.EndArray)
{
if (SkipComments(reader))
if (reader.SkipComments())
continue;
var value = reader.ReadAsString();
if (!string.IsNullOrEmpty(value))
objects.Add(value);
var item = reader.ReadAsString();
if (!string.IsNullOrEmpty(item))
objects.Add(item);
}
properties.Add(key, objects.ToArray());
}
@ -773,7 +815,6 @@ namespace PSRule
properties.Add(key, reader.Value);
}
}
reader.Read();
}
}
@ -788,6 +829,44 @@ namespace PSRule
return _Factory.IsCondition(key);
}
private bool TryValue(string key, JsonReader reader, out object value)
{
value = null;
if (key != "value")
return false;
if (reader.TryConsume(JsonToken.StartObject) &&
TryFunction(reader, reader.Value as string, out var fn))
{
value = fn;
return true;
}
return false;
}
private bool TryFunction(JsonReader reader, string key, out ExpressionFnOuter fn)
{
fn = null;
if (!IsFunction(reader))
return false;
reader.Consume(JsonToken.PropertyName);
reader.Consume(JsonToken.StartObject);
fn = MapFunction("$", reader);
if (fn == null)
throw new Exception();
reader.Consume(JsonToken.EndObject);
return true;
}
private static bool IsFunction(JsonReader reader)
{
return reader.TokenType == JsonToken.PropertyName &&
reader.Value is string s &&
s == "$";
}
private bool TryExpression<T>(string type, out T expression) where T : LanguageExpression
{
expression = null;

Просмотреть файл

@ -1,8 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Diagnostics;
using Newtonsoft.Json;
using PSRule.Definitions;
using PSRule.Pipeline;
using PSRule.Resources;
namespace PSRule
{
@ -29,5 +33,37 @@ namespace PSRule
extent = new SourceExtent(file, lineNumber, linePosition);
return true;
}
[DebuggerStepThrough]
public static bool TryConsume(this JsonReader reader, JsonToken token)
{
if (reader.TokenType != token)
return false;
reader.Read();
return true;
}
[DebuggerStepThrough]
public static void Consume(this JsonReader reader, JsonToken token)
{
if (reader.TokenType != token)
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailedExpectedToken, Enum.GetName(typeof(JsonToken), reader.TokenType));
reader.Read();
}
/// <summary>
/// Skip JSON comments.
/// </summary>
[DebuggerStepThrough]
public static bool SkipComments(this JsonReader reader)
{
var hasComments = false;
while (reader.TokenType == JsonToken.Comment && reader.Read())
hasComments = true;
return hasComments;
}
}
}

Просмотреть файл

@ -115,7 +115,7 @@ namespace PSRule
emitter.Emit(new MappingStart());
emitter.Emit(new MappingEnd());
}
if (!(value is FieldMap map))
if (value is not FieldMap map)
return;
emitter.Emit(new MappingStart());
@ -179,7 +179,7 @@ namespace PSRule
if (parser.TryConsume<SequenceStart>(out _))
{
var values = new List<PSObject>();
while (!(parser.Current is SequenceEnd))
while (parser.Current is not SequenceEnd)
{
if (parser.Current is MappingStart)
{
@ -308,16 +308,16 @@ namespace PSRule
/// </summary>
internal sealed class OrderedPropertiesTypeInspector : TypeInspectorSkeleton
{
private readonly ITypeInspector _innerTypeDescriptor;
private readonly ITypeInspector _InnerTypeDescriptor;
public OrderedPropertiesTypeInspector(ITypeInspector innerTypeDescriptor)
{
_innerTypeDescriptor = innerTypeDescriptor;
_InnerTypeDescriptor = innerTypeDescriptor;
}
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
{
return _innerTypeDescriptor
return _InnerTypeDescriptor
.GetProperties(type, container)
.OrderBy(prop => prop.Name);
}
@ -598,17 +598,22 @@ namespace PSRule
}
}
/// <summary>
/// A custom deserializer to convert YAML into a language expression.
/// </summary>
internal sealed class LanguageExpressionDeserializer : INodeDeserializer
{
private const string OPERATOR_IF = "if";
private readonly INodeDeserializer _Next;
private readonly LanguageExpressionFactory _Factory;
private readonly FunctionBuilder _FunctionBuilder;
public LanguageExpressionDeserializer(INodeDeserializer next)
{
_Next = next;
_Factory = new LanguageExpressionFactory();
_FunctionBuilder = new FunctionBuilder();
}
bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object> nestedObjectDeserializer, out object value)
@ -625,6 +630,9 @@ namespace PSRule
}
}
/// <summary>
/// Map an operator.
/// </summary>
private LanguageExpression MapOperator(string type, IParser reader, Func<IParser, Type, object> nestedObjectDeserializer)
{
if (TryExpression(reader, type, nestedObjectDeserializer, out LanguageOperator result))
@ -674,17 +682,78 @@ namespace PSRule
{
result = MapCondition(key, properties, reader, nestedObjectDeserializer);
}
else if (TryOperator(key) && reader.Accept(out MappingStart mapping))
else if (TryOperator(key) && reader.Accept<MappingStart>(out _))
{
result = MapOperator(key, reader, nestedObjectDeserializer);
}
else if (TryOperator(key) && reader.Accept(out SequenceStart sequence))
else if (TryOperator(key) && reader.Accept<SequenceStart>(out _))
{
result = MapOperator(key, reader, nestedObjectDeserializer);
}
return result;
}
private ExpressionFnOuter MapFunction(string type, IParser reader, Func<IParser, Type, object> nestedObjectDeserializer)
{
_FunctionBuilder.Push();
string name = null;
while (!(reader.Accept<MappingEnd>(out _) || reader.Accept<SequenceEnd>(out _)))
{
if (reader.TryConsume<Scalar>(out var s))
{
if (name != null)
{
_FunctionBuilder.Add(name, s.Value);
name = null;
}
else
{
name = s.Value;
}
}
else if (reader.TryConsume<MappingStart>(out _))
{
var child = MapFunction(name, reader, nestedObjectDeserializer);
if (name != null)
{
_FunctionBuilder.Add(name, child);
name = null;
}
reader.Consume<MappingEnd>();
}
else if (reader.TryConsume<SequenceStart>(out _))
{
var sequence = MapSequence(name, reader, nestedObjectDeserializer);
if (name != null)
{
_FunctionBuilder.Add(name, sequence);
name = null;
}
reader.Consume<SequenceEnd>();
}
}
var result = _FunctionBuilder.Pop();
return result;
}
private object MapSequence(string name, IParser reader, Func<IParser, Type, object> nestedObjectDeserializer)
{
var result = new List<object>();
while (!reader.Accept<SequenceEnd>(out _))
{
if (reader.TryConsume<Scalar>(out var s))
result.Add(s.Value);
else if (reader.TryConsume<MappingStart>(out _))
{
var child = MapFunction(name, reader, nestedObjectDeserializer);
result.Add(child);
reader.Consume<MappingEnd>();
}
}
return result.ToArray();
}
private void MapProperty(LanguageExpression.PropertyBag properties, IParser reader, Func<IParser, Type, object> nestedObjectDeserializer, out string name)
{
name = null;
@ -698,6 +767,15 @@ namespace PSRule
{
properties[key] = scalar.Value;
}
else if (TryValue(key, reader, nestedObjectDeserializer, out var value))
{
properties[key] = value;
}
else if (TryCondition(key) && reader.TryConsume<MappingStart>(out _))
{
if (TryFunction(reader, nestedObjectDeserializer, out var fn))
properties[key] = fn;
}
else if (TryCondition(key) && reader.TryConsume<SequenceStart>(out _))
{
var objects = new List<string>();
@ -723,6 +801,40 @@ namespace PSRule
return _Factory.IsCondition(key);
}
private bool TryValue(string key, IParser reader, Func<IParser, Type, object> nestedObjectDeserializer, out object value)
{
value = null;
if (key != "value")
return false;
if (reader.TryConsume<MappingStart>(out _) && TryFunction(reader, nestedObjectDeserializer, out var fn))
{
value = fn;
return true;
}
reader.SkipThisAndNestedEvents();
return false;
}
private bool TryFunction(IParser reader, Func<IParser, Type, object> nestedObjectDeserializer, out ExpressionFnOuter fn)
{
fn = null;
if (!IsFunction(reader))
return false;
reader.Consume<Scalar>();
reader.Consume<MappingStart>();
fn = MapFunction("$", reader, nestedObjectDeserializer);
reader.Consume<MappingEnd>();
reader.Consume<MappingEnd>();
return true;
}
private static bool IsFunction(IParser reader)
{
return reader.Accept<Scalar>(out var scalar) || scalar.Value == "$";
}
private bool TryExpression<T>(IParser reader, string type, Func<IParser, Type, object> nestedObjectDeserializer, out T expression) where T : LanguageExpression
{
expression = null;

Просмотреть файл

@ -0,0 +1,169 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Runtime.Serialization;
using System.Security.Permissions;
using PSRule.Pipeline;
namespace PSRule.Definitions.Expressions
{
/// <summary>
/// A base class for runtime exceptions.
/// </summary>
public abstract class SelectorException : PipelineException
{
protected SelectorException()
: base() { }
protected SelectorException(string message)
: base(message) { }
protected SelectorException(string message, Exception innerException)
: base(message, innerException) { }
protected SelectorException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
[Serializable]
public sealed class ExpressionParseException : SelectorException
{
public ExpressionParseException()
{
}
public ExpressionParseException(string message)
: base(message) { }
public ExpressionParseException(string message, Exception innerException)
: base(message, innerException) { }
internal ExpressionParseException(string expression, string message)
: base(message)
{
Expression = expression;
}
internal ExpressionParseException(string expression, string message, Exception innerException)
: base(message, innerException)
{
Expression = expression;
}
private ExpressionParseException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
public string Expression { get; }
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null) throw new ArgumentNullException(nameof(info));
base.GetObjectData(info, context);
}
}
public abstract class ExpressionException : SelectorException
{
protected ExpressionException()
{
}
protected ExpressionException(string message)
: base(message) { }
protected ExpressionException(string message, Exception innerException)
: base(message, innerException) { }
protected ExpressionException(string expression, string message)
: base(message)
{
Expression = expression;
}
protected ExpressionException(string expression, string message, Exception innerException)
: base(message, innerException)
{
Expression = expression;
}
protected ExpressionException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
public string Expression { get; }
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null) throw new ArgumentNullException(nameof(info));
base.GetObjectData(info, context);
}
}
[Serializable]
public sealed class ExpressionReferenceException : SelectorException
{
public ExpressionReferenceException()
{
}
public ExpressionReferenceException(string message)
: base(message) { }
public ExpressionReferenceException(string message, Exception innerException)
: base(message, innerException) { }
internal ExpressionReferenceException(string expression, string message)
: base(message)
{
Expression = expression;
}
internal ExpressionReferenceException(string expression, string message, Exception innerException)
: base(message, innerException)
{
Expression = expression;
}
private ExpressionReferenceException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
public string Expression { get; }
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null) throw new ArgumentNullException(nameof(info));
base.GetObjectData(info, context);
}
}
[Serializable]
public sealed class ExpressionArgumentException : ExpressionException
{
public ExpressionArgumentException()
{
}
public ExpressionArgumentException(string message)
: base(message) { }
public ExpressionArgumentException(string message, Exception innerException)
: base(message, innerException) { }
internal ExpressionArgumentException(string expression, string message)
: base(expression, message) { }
private ExpressionArgumentException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null) throw new ArgumentNullException(nameof(info));
base.GetObjectData(info, context);
}
}
}

Просмотреть файл

@ -16,6 +16,8 @@ namespace PSRule.Definitions.Expressions
void Reason(IOperand operand, string text, params object[] args);
object Current { get; }
RunspaceContext GetContext();
}
@ -25,12 +27,13 @@ namespace PSRule.Definitions.Expressions
private List<ResultReason> _Reason;
internal ExpressionContext(SourceFile source, ResourceKind kind)
internal ExpressionContext(SourceFile source, ResourceKind kind, object current)
{
Source = source;
LanguageScope = source.Module;
Kind = kind;
_NameTokenCache = new Dictionary<string, PathExpression>();
Current = current;
}
public SourceFile Source { get; }
@ -39,6 +42,8 @@ namespace PSRule.Definitions.Expressions
public ResourceKind Kind { get; }
public object Current { get; }
[DebuggerStepThrough]
void IBindingContext.CachePathExpression(string path, PathExpression expression)
{

Просмотреть файл

@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace PSRule.Definitions.Expressions
{
internal delegate object ExpressionFnOuter(IExpressionContext context);
internal delegate object ExpressionFn(IExpressionContext context, object[] args);
internal delegate ExpressionFnOuter ExpressionBuilderFn(IExpressionContext context, LanguageExpression.PropertyBag properties);
internal abstract class FunctionReader
{
public abstract bool TryProperty(out string propertyName);
}
internal sealed class FunctionBuilder
{
private readonly Stack<LanguageExpression.PropertyBag> _Stack;
private readonly FunctionFactory _Functions;
private LanguageExpression.PropertyBag _Current;
internal FunctionBuilder() : this(new FunctionFactory()) { }
internal FunctionBuilder(FunctionFactory expressionFactory)
{
_Functions = expressionFactory;
_Stack = new Stack<LanguageExpression.PropertyBag>();
}
public void Push()
{
_Current = new LanguageExpression.PropertyBag();
_Stack.Push(_Current);
}
internal void Add(string name, object value)
{
_Current.Add(name, value);
}
public ExpressionFnOuter Pop()
{
var properties = _Stack.Pop();
_Current = _Stack.Count > 0 ? _Stack.Peek() : null;
return TryFunction(properties, out var descriptor) ? descriptor.Fn(null, properties) : null;
}
private bool TryFunction(LanguageExpression.PropertyBag properties, out IFunctionDescriptor descriptor)
{
descriptor = null;
foreach (var property in properties)
{
if (_Functions.TryDescriptor(property.Key, out descriptor))
return true;
}
return false;
}
}
internal sealed class FunctionFactory
{
private readonly Dictionary<string, IFunctionDescriptor> _Descriptors;
public FunctionFactory()
{
_Descriptors = new Dictionary<string, IFunctionDescriptor>(StringComparer.OrdinalIgnoreCase);
foreach (var d in Functions.Builtin)
With(d);
}
public bool TryDescriptor(string name, out IFunctionDescriptor descriptor)
{
return _Descriptors.TryGetValue(name, out descriptor);
}
public void With(IFunctionDescriptor descriptor)
{
_Descriptors.Add(descriptor.Name, descriptor);
}
}
/// <summary>
/// A structure describing a specific function.
/// </summary>
[DebuggerDisplay("Function: {Name}")]
internal sealed class FunctionDescriptor : IFunctionDescriptor
{
public FunctionDescriptor(string name, ExpressionBuilderFn fn)
{
Name = name;
Fn = fn;
}
/// <inheritdoc/>
public string Name { get; }
/// <inheritdoc/>
public ExpressionBuilderFn Fn { get; }
}
/// <summary>
/// A structure describing a specific function.
/// </summary>
internal interface IFunctionDescriptor
{
/// <summary>
/// The name of the function.
/// </summary>
string Name { get; }
/// <summary>
/// The function delegate.
/// </summary>
ExpressionBuilderFn Fn { get; }
}
}

Просмотреть файл

@ -0,0 +1,227 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Text;
using System.Threading;
using PSRule.Resources;
using PSRule.Runtime;
using static PSRule.Definitions.Expressions.LanguageExpression;
namespace PSRule.Definitions.Expressions
{
/// <summary>
/// Implementation of Azure Resource Manager template functions as ExpressionFn.
/// </summary>
internal static class Functions
{
private const string BOOLEAN = "boolean";
private const string STRING = "string";
private const string INTEGER = "integer";
private const string CONCAT = "concat";
private const string SUBSTRING = "substring";
private const string CONFIGURATION = "configuration";
private const string PATH = "path";
private const string LENGTH = "length";
/// <summary>
/// The available built-in functions.
/// </summary>
internal readonly static IFunctionDescriptor[] Builtin = new IFunctionDescriptor[]
{
new FunctionDescriptor(CONFIGURATION, Configuration),
new FunctionDescriptor(PATH, Path),
new FunctionDescriptor(BOOLEAN, Boolean),
new FunctionDescriptor(STRING, String),
new FunctionDescriptor(INTEGER, Integer),
new FunctionDescriptor(CONCAT, Concat),
new FunctionDescriptor(SUBSTRING, Substring),
};
private static ExpressionFnOuter Boolean(IExpressionContext context, PropertyBag properties)
{
if (properties == null ||
properties.Count == 0 ||
!TryProperty(properties, BOOLEAN, out ExpressionFnOuter next))
return null;
return (context) =>
{
var value = next(context);
ExpressionHelpers.TryBool(value, true, out var b);
return b;
};
}
private static ExpressionFnOuter String(IExpressionContext context, PropertyBag properties)
{
if (properties == null ||
properties.Count == 0 ||
!TryProperty(properties, STRING, out ExpressionFnOuter next))
return null;
return (context) =>
{
var value = next(context);
ExpressionHelpers.TryString(value, true, out var s);
return s;
};
}
private static ExpressionFnOuter Integer(IExpressionContext context, PropertyBag properties)
{
if (properties == null ||
properties.Count == 0 ||
!TryProperty(properties, INTEGER, out ExpressionFnOuter next))
return null;
return (context) =>
{
var value = next(context);
ExpressionHelpers.TryInt(value, true, out var i);
return i;
};
}
private static ExpressionFnOuter Configuration(IExpressionContext context, PropertyBag properties)
{
if (properties == null || properties.Count == 0 ||
!properties.TryGetString(CONFIGURATION, out var name))
return null;
// Lookup a configuration value.
return (context) =>
{
return context.GetContext().TryGetConfigurationValue(name, out var value) ? value : null;
};
}
private static ExpressionFnOuter Path(IExpressionContext context, PropertyBag properties)
{
if (properties == null || properties.Count == 0 ||
!properties.TryGetString(PATH, out var path))
return null;
return (context) =>
{
return ObjectHelper.GetPath(context, context.Current, path, false, out object value) ? value : null;
};
}
private static ExpressionFnOuter Concat(IExpressionContext context, PropertyBag properties)
{
if (properties == null ||
properties.Count == 0 ||
!properties.TryGetEnumerable(CONCAT, out var values))
return null;
return (context) =>
{
var sb = new StringBuilder();
foreach (var value in values)
sb.Append(Value(context, value));
return sb.ToString();
};
}
private static ExpressionFnOuter Substring(IExpressionContext context, PropertyBag properties)
{
if (properties == null ||
properties.Count == 0 ||
!TryProperty(properties, LENGTH, out int? length) ||
!TryProperty(properties, SUBSTRING, out ExpressionFnOuter next))
return null;
return (context) =>
{
var value = next(context);
if (value is string s)
{
length = s.Length < length ? s.Length : length;
return s.Substring(0, length.Value);
}
return null;
};
}
#region Helper functions
private static bool TryProperty(PropertyBag properties, string name, out int? value)
{
return properties.TryGetInt(name, out value);
}
private static bool TryProperty(PropertyBag properties, string name, out ExpressionFnOuter value)
{
value = null;
if (properties.TryGetValue(name, out var v) && v is ExpressionFnOuter fn)
value = fn;
else if (properties.TryGetBool(name, out var b_value))
value = (context) => b_value;
else if (properties.TryGetLong(name, out var l_value))
value = (context) => l_value;
else if (properties.TryGetInt(name, out var i_value))
value = (context) => i_value;
else if (properties.TryGetValue(name, out var o_value))
value = (context) => o_value;
return value != null;
}
private static object Value(IExpressionContext context, object value)
{
return value is ExpressionFnOuter fn ? fn(context) : value;
}
#endregion Helper functions
#region Exceptions
private static ExpressionArgumentException ArgumentsOutOfRange(string expression, object[] args)
{
var length = args == null ? 0 : args.Length;
return new ExpressionArgumentException(
expression,
string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentsOutOfRange, expression, length)
);
}
private static ExpressionArgumentException ArgumentFormatInvalid(string expression)
{
return new ExpressionArgumentException(
expression,
string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentFormatInvalid, expression)
);
}
private static ExpressionArgumentException ArgumentInvalidInteger(string expression, string operand)
{
return new ExpressionArgumentException(
expression,
string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentInvalidInteger, operand, expression)
);
}
private static ExpressionArgumentException ArgumentInvalidBoolean(string expression, string operand)
{
return new ExpressionArgumentException(
expression,
string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentInvalidBoolean, operand, expression)
);
}
private static ExpressionArgumentException ArgumentInvalidString(string expression, string operand)
{
return new ExpressionArgumentException(
expression,
string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.ArgumentInvalidString, operand, expression)
);
}
#endregion Exceptions
}
}

Просмотреть файл

@ -21,7 +21,9 @@ namespace PSRule.Definitions.Expressions
{
Operator = 1,
Condition = 2
Condition = 2,
Function = 3
}
internal sealed class ExpressionInfo
@ -47,7 +49,9 @@ namespace PSRule.Definitions.Expressions
public bool TryDescriptor(string name, out ILanguageExpresssionDescriptor descriptor)
{
return _Descriptors.TryGetValue(name, out descriptor);
descriptor = null;
return !string.IsNullOrEmpty(name) &&
_Descriptors.TryGetValue(name, out descriptor);
}
public bool IsOperator(string name)
@ -60,6 +64,12 @@ namespace PSRule.Definitions.Expressions
return TryDescriptor(name, out var d) && d != null && d.Type == LanguageExpressionType.Condition;
}
public bool IsFunction(string name)
{
return TryDescriptor(name, out var d) &&
d != null && d.Type == LanguageExpressionType.Function;
}
private void With(ILanguageExpresssionDescriptor descriptor)
{
_Descriptors.Add(descriptor.Name, descriptor);
@ -350,6 +360,7 @@ namespace PSRule.Definitions.Expressions
private const string INCLUDEPRERELEASE = "includePrerelease";
private const string PROPERTY_SCHEMA = "$schema";
private const string SOURCE = "source";
private const string VALUE = "value";
// Comparisons
private const string LESS_THAN = "<";
@ -476,7 +487,7 @@ namespace PSRule.Definitions.Expressions
return Condition(
context,
operand,
ExpressionHelpers.Equal(propertyValue, operand.Value, caseSensitive, convertExpected: true, convertActual: convert),
ExpressionHelpers.Equal(Value(context, propertyValue), Value(context, operand), caseSensitive, convertExpected: true, convertActual: convert),
ReasonStrings.Assert_IsSetTo,
operand.Value
);
@ -500,7 +511,7 @@ namespace PSRule.Definitions.Expressions
return Condition(
context,
operand,
!ExpressionHelpers.Equal(propertyValue, operand.Value, caseSensitive, convertExpected: true, convertActual: convert),
!ExpressionHelpers.Equal(Value(context, propertyValue), Value(context, operand), caseSensitive, convertExpected: true, convertActual: convert),
ReasonStrings.Assert_IsSetTo,
operand.Value
);
@ -684,59 +695,50 @@ namespace PSRule.Definitions.Expressions
internal static bool Count(ExpressionContext context, ExpressionInfo info, object[] args, object o)
{
var properties = GetProperties(args);
if (TryPropertyLong(properties, COUNT, out var expectedValue) && TryField(properties, out var field))
{
context.ExpressionTrace(COUNT, field, expectedValue);
if (!ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object value))
return NotHasField(context, field);
if (!TryPropertyLong(context, properties, COUNT, out var expectedValue) ||
!TryOperand(context, COUNT, o, properties, out var operand))
return Invalid(context, COUNT);
if (value == null)
return Fail(context, field, ReasonStrings.Null, field);
var operandValue = Value(context, operand);
if (operandValue == null)
return Fail(context, operand, ReasonStrings.Assert_IsNull);
if (ExpressionHelpers.TryEnumerableLength(value, value: out var actualValue))
return Condition(
context,
field,
actualValue == expectedValue,
ReasonStrings.Count,
field,
actualValue,
expectedValue
);
}
return Invalid(context, COUNT);
// int, string, bool
return Condition(
context,
operand,
ExpressionHelpers.TryEnumerableLength(operandValue, value: out var actualValue) && actualValue == expectedValue,
ReasonStrings.Assert_Count,
actualValue,
expectedValue
);
}
internal static bool NotCount(ExpressionContext context, ExpressionInfo info, object[] args, object o)
{
var properties = GetProperties(args);
if (TryPropertyLong(properties, NOTCOUNT, out var expectedValue) && TryField(properties, out var field))
{
context.ExpressionTrace(NOTCOUNT, field, expectedValue);
if (!ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object value))
return NotHasField(context, field);
if (!TryPropertyLong(context, properties, NOTCOUNT, out var expectedValue) ||
!TryOperand(context, NOTCOUNT, o, properties, out var operand))
return Invalid(context, NOTCOUNT);
if (value == null)
return Fail(context, field, ReasonStrings.Null, field);
var operandValue = Value(context, operand);
if (operandValue == null)
return Fail(context, operand, ReasonStrings.Assert_IsNull);
if (ExpressionHelpers.TryEnumerableLength(value, value: out var actualValue))
return Condition(
context,
field,
actualValue != expectedValue,
ReasonStrings.NotCount,
field,
actualValue,
expectedValue
);
}
return Invalid(context, NOTCOUNT);
// int, string, bool
return Condition(
context,
operand,
ExpressionHelpers.TryEnumerableLength(operandValue, value: out var actualValue) && actualValue != expectedValue,
ReasonStrings.Assert_NotCount,
actualValue
);
}
internal static bool Less(ExpressionContext context, ExpressionInfo info, object[] args, object o)
{
var properties = GetProperties(args);
if (!TryPropertyLong(properties, LESS, out var propertyValue) ||
if (!TryPropertyLong(context, properties, LESS, out var propertyValue) ||
!TryOperand(context, LESS, o, properties, out var operand) ||
!GetConvert(properties, out var convert))
return Invalid(context, LESS);
@ -749,8 +751,9 @@ namespace PSRule.Definitions.Expressions
ReasonStrings.Assert_IsNullOrEmpty
);
var operandValue = Value(context, operand);
if (!ExpressionHelpers.CompareNumeric(
operand.Value,
operandValue,
propertyValue,
convert,
compare: out var compare,
@ -763,7 +766,7 @@ namespace PSRule.Definitions.Expressions
operand,
compare < 0,
ReasonStrings.Assert_NotComparedTo,
operand.Value,
operandValue,
LESS_THAN,
propertyValue
);
@ -772,7 +775,7 @@ namespace PSRule.Definitions.Expressions
internal static bool LessOrEquals(ExpressionContext context, ExpressionInfo info, object[] args, object o)
{
var properties = GetProperties(args);
if (!TryPropertyLong(properties, LESSOREQUALS, out var propertyValue) ||
if (!TryPropertyLong(context, properties, LESSOREQUALS, out var propertyValue) ||
!TryOperand(context, LESSOREQUALS, o, properties, out var operand) ||
!GetConvert(properties, out var convert))
return Invalid(context, LESSOREQUALS);
@ -785,8 +788,9 @@ namespace PSRule.Definitions.Expressions
ReasonStrings.Assert_IsNullOrEmpty
);
var operandValue = Value(context, operand);
if (!ExpressionHelpers.CompareNumeric(
operand.Value,
operandValue,
propertyValue,
convert,
compare: out var compare,
@ -799,7 +803,7 @@ namespace PSRule.Definitions.Expressions
operand,
compare <= 0,
ReasonStrings.Assert_NotComparedTo,
operand.Value,
operandValue,
LESS_THAN_EQUALS,
propertyValue
);
@ -808,7 +812,7 @@ namespace PSRule.Definitions.Expressions
internal static bool Greater(ExpressionContext context, ExpressionInfo info, object[] args, object o)
{
var properties = GetProperties(args);
if (!TryPropertyLong(properties, GREATER, out var propertyValue) ||
if (!TryPropertyLong(context, properties, GREATER, out var propertyValue) ||
!TryOperand(context, GREATER, o, properties, out var operand) ||
!GetConvert(properties, out var convert))
return Invalid(context, GREATER);
@ -821,8 +825,9 @@ namespace PSRule.Definitions.Expressions
ReasonStrings.Assert_IsNullOrEmpty
);
var operandValue = Value(context, operand);
if (!ExpressionHelpers.CompareNumeric(
operand.Value,
operandValue,
propertyValue,
convert,
compare: out var compare,
@ -835,7 +840,7 @@ namespace PSRule.Definitions.Expressions
operand,
compare > 0,
ReasonStrings.Assert_NotComparedTo,
operand.Value,
operandValue,
GREATER_THAN,
propertyValue
);
@ -844,7 +849,7 @@ namespace PSRule.Definitions.Expressions
internal static bool GreaterOrEquals(ExpressionContext context, ExpressionInfo info, object[] args, object o)
{
var properties = GetProperties(args);
if (!TryPropertyLong(properties, GREATEROREQUALS, out var propertyValue) ||
if (!TryPropertyLong(context, properties, GREATEROREQUALS, out var propertyValue) ||
!TryOperand(context, GREATEROREQUALS, o, properties, out var operand) ||
!GetConvert(properties, out var convert))
return Invalid(context, GREATEROREQUALS);
@ -857,8 +862,9 @@ namespace PSRule.Definitions.Expressions
ReasonStrings.Assert_IsNullOrEmpty
);
var operandValue = Value(context, operand);
if (!ExpressionHelpers.CompareNumeric(
operand.Value,
operandValue,
propertyValue,
convert,
compare: out var compare,
@ -871,7 +877,7 @@ namespace PSRule.Definitions.Expressions
operand,
compare >= 0,
ReasonStrings.Assert_NotComparedTo,
operand.Value,
operandValue,
GREATER_THAN_EQUALS,
propertyValue
);
@ -1484,9 +1490,17 @@ namespace PSRule.Definitions.Expressions
return true;
}
private static bool TryPropertyLong(LanguageExpression.PropertyBag properties, string propertyName, out long? propertyValue)
private static bool TryPropertyLong(ExpressionContext context, LanguageExpression.PropertyBag properties, string propertyName, out long? propertyValue)
{
return properties.TryGetLong(propertyName, out propertyValue);
propertyValue = null;
if (!properties.TryGetValue(propertyName, out var value))
return false;
if (!ExpressionHelpers.TryLong(Value(context, value), true, out var l_value))
return false;
propertyValue = l_value;
return true;
}
private static bool TryField(LanguageExpression.PropertyBag properties, out string field)
@ -1537,7 +1551,6 @@ namespace PSRule.Definitions.Expressions
if (string.IsNullOrEmpty(type))
return Invalid(context, svalue);
operand = Operand.FromType(type, binding.TargetTypePath);
}
return operand != null;
@ -1557,6 +1570,36 @@ namespace PSRule.Definitions.Expressions
return operand != null;
}
private static bool TryValue(IExpressionContext context, LanguageExpression.PropertyBag properties, out IOperand operand)
{
operand = null;
if (properties.TryGetValue(VALUE, out var value))
{
// TODO: Propogate path
operand = Operand.FromValue(value);
}
return operand != null;
}
/// <summary>
/// Unwrap a function delegate or a literal value.
/// </summary>
private static object Value(IExpressionContext context, IOperand operand)
{
if (operand == null)
return null;
return operand.Value is ExpressionFnOuter fn ? fn(context) : operand.Value;
}
/// <summary>
/// Unwrap a function delegate or a literal value.
/// </summary>
private static object Value(IExpressionContext context, object value)
{
return value is ExpressionFnOuter fn ? fn(context) : value;
}
private static bool GetCaseSensitive(LanguageExpression.PropertyBag properties, out bool caseSensitive, bool defaultValue = false)
{
return TryPropertyBoolOrDefault(properties, CASESENSITIVE, out caseSensitive, defaultValue);
@ -1587,6 +1630,7 @@ namespace PSRule.Definitions.Expressions
TryType(context, properties, out operand) ||
TryName(context, properties, out operand) ||
TrySource(context, properties, out operand) ||
TryValue(context, properties, out operand) ||
Invalid(context, name);
}

Просмотреть файл

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
@ -39,7 +39,7 @@ namespace PSRule.Definitions.Expressions
if (Type == LanguageExpressionType.Condition)
return new LanguageCondition(this, properties);
return null;
return Type == LanguageExpressionType.Function ? new LanguageFunction(this) : null;
}
}
@ -104,4 +104,14 @@ namespace PSRule.Definitions.Expressions
Property.AddUnique(properties);
}
}
[DebuggerDisplay("Selector {Descriptor.Name}")]
internal sealed class LanguageFunction : LanguageExpression
{
internal LanguageFunction(LanguageExpresssionDescriptor descriptor)
: base(descriptor)
{
}
}
}

Просмотреть файл

@ -50,7 +50,7 @@ namespace PSRule.Definitions.Rules
public IConditionResult If()
{
var context = new ExpressionContext(Source, ResourceKind.Rule);
var context = new ExpressionContext(Source, ResourceKind.Rule, RunspaceContext.CurrentThread.TargetObject.Value);
context.Debug(PSRuleResources.RuleMatchTrace, Id);
context.PushScope(RunspaceScope.Rule);
try

Просмотреть файл

@ -42,7 +42,7 @@ namespace PSRule.Definitions.Selectors
public bool Match(object o)
{
var context = new ExpressionContext(Source, ResourceKind.Selector);
var context = new ExpressionContext(Source, ResourceKind.Selector, o);
context.Debug(PSRuleResources.SelectorMatchTrace, Id);
return _Fn(context, o).GetValueOrDefault(false);
}

Просмотреть файл

@ -77,7 +77,7 @@ namespace PSRule.Definitions.SuppressionGroups
public bool TryMatch(object o, out ISuppressionInfo suppression)
{
suppression = null;
var context = new ExpressionContext(Source, ResourceKind.SuppressionGroup);
var context = new ExpressionContext(Source, ResourceKind.SuppressionGroup, o);
context.Debug(PSRuleResources.SelectorMatchTrace, Id);
if (_Fn(context, o).GetValueOrDefault(false))
{

Просмотреть файл

@ -260,7 +260,7 @@ namespace PSRule.Host
context.EnterSourceScope(source: file);
using var reader = new StreamReader(file.Path);
var parser = new YamlDotNet.Core.Parser(reader);
var parser = new Parser(reader);
parser.TryConsume<StreamStart>(out _);
while (parser.Current is DocumentStart)
{

753
src/PSRule/Resources/PSRuleResources.Designer.cs сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -121,6 +121,25 @@
<value>The {0} '{1}' is obsolete. Consider switching to an alternative {0}.</value>
<comment>Occurs when a resource is used that has been flagged as obsolete.</comment>
</data>
<data name="ArgumentFormatInvalid" xml:space="preserve">
<value>The arguments for '{0}' are not in the expected format or type.</value>
</data>
<data name="ArgumentInvalidBoolean" xml:space="preserve">
<value>The argument '{0}' for '{1}' is not a valid boolean.</value>
</data>
<data name="ArgumentInvalidInteger" xml:space="preserve">
<value>The argument '{0}' for '{1}' is not a valid integer.</value>
</data>
<data name="ArgumentInvalidString" xml:space="preserve">
<value>The argument '{0}' for '{1}' is not a valid string.</value>
</data>
<data name="ArgumentsOutOfRange" xml:space="preserve">
<value>The number of arguments '{1}' is not within the allowed range for '{0}'.</value>
</data>
<data name="BaselineObsolete" xml:space="preserve">
<value>The baseline '{0}' is obsolete. Consider switching to an alternative baseline.</value>
<comment>Occurs when a baseline is used that has been flagged as obsolete.</comment>
</data>
<data name="ConstrainedTargetBinding" xml:space="preserve">
<value>Binding functions are not supported in this language mode.</value>
</data>
@ -164,9 +183,18 @@
<data name="FileSourceType" xml:space="preserve">
<value>File</value>
</data>
<data name="ExpressionInvalid" xml:space="preserve">
<value>Failed to parse expression. The expression may not be valid. Expression: "{0}"</value>
</data>
<data name="FoundModules" xml:space="preserve">
<value>[PSRule][D] -- Found {0} PSRule module(s)</value>
</data>
<data name="FunctionNotFound" xml:space="preserve">
<value>The function "{0}" was not found.</value>
</data>
<data name="IndexInvalid" xml:space="preserve">
<value>The language expression index '{0}' is not valid for the object.</value>
</data>
<data name="InvalidErrorAction" xml:space="preserve">
<value>An invalid ErrorAction ({0}) was specified for rule at {1}. Ignore | Stop are supported.</value>
</data>
@ -229,6 +257,9 @@
<data name="OutcomeUnknown" xml:space="preserve">
<value>Unknown</value>
</data>
<data name="PropertyNotFound" xml:space="preserve">
<value>The language expression property '{0}' doesn't exist.</value>
</data>
<data name="PropertyObsolete" xml:space="preserve">
<value>The property '${0}.{1}' is obsolete and will be removed in the next major version.</value>
</data>

636
src/PSRule/Resources/ReasonStrings.Designer.cs сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -345,4 +345,13 @@
<data name="Assert_NotExists" xml:space="preserve">
<value>Exists.</value>
</data>
<data name="Assert_Count" xml:space="preserve">
<value>The number of items is '{0}' instead of '{1}'.</value>
</data>
<data name="Assert_IsNull" xml:space="preserve">
<value>The value is null.</value>
</data>
<data name="Assert_NotCount" xml:space="preserve">
<value>The number of items is '{0}'.</value>
</data>
</root>

Просмотреть файл

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
@ -69,23 +69,7 @@ namespace PSRule.Runtime
private bool TryGetValue(string name, out object value)
{
value = null;
if (_Context == null)
return false;
// Get from baseline configuration
if (_Context.LanguageScope.TryConfigurationValue(name, out var result))
{
value = result;
return true;
}
// Check if value exists in Rule definition defaults
if (_Context.RuleBlock == null || _Context.RuleBlock.Configuration == null || !_Context.RuleBlock.Configuration.ContainsKey(name))
return false;
// Get from rule default
value = _Context.RuleBlock.Configuration[name];
return true;
return _Context != null && _Context.TryGetConfigurationValue(name, out value);
}
private static bool TryBool(object o, out bool value)

Просмотреть файл

@ -38,7 +38,12 @@ namespace PSRule.Runtime
/// <summary>
/// The target object itself.
/// </summary>
Target = 5
Target = 5,
/// <summary>
/// A literal value or function.
/// </summary>
Value = 6
}
/// <summary>
@ -117,6 +122,11 @@ namespace PSRule.Runtime
return new Operand(OperandKind.Target, null, null);
}
internal static IOperand FromValue(object value)
{
return new Operand(OperandKind.Value, null, value);
}
internal static string JoinPath(string p1, string p2)
{
if (IsEmptyPath(p1))

Просмотреть файл

@ -136,7 +136,7 @@ namespace PSRule.Runtime
/// <summary>
/// The current target object.
/// </summary>
public PSObject TargetObject => GetContext().RuleRecord.TargetObject;
public PSObject TargetObject => GetContext().TargetObject.Value;
/// <summary>
/// The bound name of the target object.

Просмотреть файл

@ -771,7 +771,6 @@ namespace PSRule.Runtime
{
InitLanguageScopes(source);
Host.HostHelper.ImportResource(source, this);
foreach (var languageScope in _LanguageScopes.Get())
Pipeline.Baseline.BuildScope(languageScope);
}
@ -841,6 +840,32 @@ namespace PSRule.Runtime
return true;
}
#region Configuration
internal bool TryGetConfigurationValue(string name, out object value)
{
value = null;
if (string.IsNullOrEmpty(name))
return false;
// Get from baseline configuration
if (LanguageScope.TryConfigurationValue(name, out var result))
{
value = result;
return true;
}
// Check if value exists in Rule definition defaults
if (RuleBlock == null || RuleBlock.Configuration == null || !RuleBlock.Configuration.ContainsKey(name))
return false;
// Get from rule default
value = RuleBlock.Configuration[name];
return true;
}
#endregion Configuration
#region IDisposable
public void Dispose()

Просмотреть файл

@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Linq;
using PSRule.Configuration;
using PSRule.Definitions.Selectors;
using PSRule.Host;
using PSRule.Pipeline;
using PSRule.Runtime;
using Xunit;
using Assert = Xunit.Assert;
namespace PSRule
{
public sealed class FunctionBuilderTests
{
private const string FunctionYamlFileName = "Functions.Rule.yaml";
private const string FunctionJsonFileName = "Functions.Rule.jsonc";
[Theory]
[InlineData("Yaml", FunctionYamlFileName)]
[InlineData("Json", FunctionJsonFileName)]
public void Build(string type, string path)
{
Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example1", GetSource(path), out _));
Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example2", GetSource(path), out _));
Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example3", GetSource(path), out _));
Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example4", GetSource(path), out _));
Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example5", GetSource(path), out _));
Assert.NotNull(GetSelectorVisitor($"{type}.Fn.Example6", GetSource(path), out _));
}
#region Helper methods
private static PSRuleOption GetOption()
{
return new PSRuleOption();
}
private static Source[] GetSource(string path)
{
var builder = new SourcePipelineBuilder(null, null);
builder.Directory(GetSourcePath(path));
return builder.Build();
}
private static SelectorVisitor GetSelectorVisitor(string name, Source[] source, out RunspaceContext context)
{
context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContext(), null), null);
context.Init(source);
context.Begin();
var selector = HostHelper.GetSelector(source, context).ToArray().FirstOrDefault(s => s.Name == name);
return new SelectorVisitor(selector.Id, selector.Source, selector.Spec.If);
}
private static string GetSourcePath(string fileName)
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
}
#endregion Helper methods
}
}

Просмотреть файл

@ -0,0 +1,282 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Linq;
using System.Management.Automation;
using PSRule.Configuration;
using PSRule.Definitions.Expressions;
using PSRule.Pipeline;
using Xunit;
namespace PSRule
{
/// <summary>
/// Define tests for expression functions are working correctly.
/// </summary>
public sealed class FunctionTests
{
[Fact]
public void Concat()
{
var context = GetContext();
var fn = GetFunction("concat");
var properties = new LanguageExpression.PropertyBag
{
{ "concat", new object[] { "1", "2", "3" } }
};
Assert.Equal("123", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "concat", new object[] { 1, 2, 3 } }
};
Assert.Equal("123", fn(context, properties)(context));
}
[Fact]
public void Configuration()
{
var context = GetContext();
var fn = GetFunction("configuration");
var properties = new LanguageExpression.PropertyBag
{
{ "configuration", "config1" }
};
Assert.Equal("123", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "configuration", "notconfig" }
};
Assert.Null(fn(context, properties)(context));
}
[Fact]
public void Boolean()
{
var context = GetContext();
var fn = GetFunction("boolean");
var properties = new LanguageExpression.PropertyBag
{
{ "boolean", true }
};
Assert.Equal(true, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "boolean", false }
};
Assert.Equal(false, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "boolean", "true" }
};
Assert.Equal(true, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "boolean", "false" }
};
Assert.Equal(false, fn(context, properties)(context));
}
[Fact]
public void Integer()
{
var context = GetContext();
var fn = GetFunction("integer");
var properties = new LanguageExpression.PropertyBag
{
{ "integer", 1 }
};
Assert.Equal(1, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "integer", -1 }
};
Assert.Equal(-1, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "integer", 0 }
};
Assert.Equal(0, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "integer", "1" }
};
Assert.Equal(1, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "integer", "-1" }
};
Assert.Equal(-1, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "integer", "0" }
};
Assert.Equal(0, fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "integer", "not" }
};
Assert.Equal(0, fn(context, properties)(context));
}
[Fact]
public void String()
{
var context = GetContext();
var fn = GetFunction("string");
var properties = new LanguageExpression.PropertyBag
{
{ "string", 1 }
};
Assert.Equal("1", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "string", -1 }
};
Assert.Equal("-1", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "string", "1" }
};
Assert.Equal("1", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "string", "-1" }
};
Assert.Equal("-1", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "string", "0" }
};
Assert.Equal("0", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "string", "abc" }
};
Assert.Equal("abc", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "string", true }
};
Assert.Equal("True", fn(context, properties)(context));
}
[Fact]
public void Substring()
{
var context = GetContext();
var fn = GetFunction("substring");
var properties = new LanguageExpression.PropertyBag
{
{ "substring", "TestObject" },
{ "length", 7 }
};
Assert.Equal("TestObj", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "substring", "TestObject" },
{ "length", "7" }
};
Assert.Equal("TestObj", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "substring", 10000 },
{ "length", 2 }
};
Assert.Null(fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "substring", "Test" },
{ "length", 100 }
};
Assert.Equal("Test", fn(context, properties)(context));
}
[Fact]
public void Path()
{
var context = GetContext();
var fn = GetFunction("path");
var properties = new LanguageExpression.PropertyBag
{
{ "path", "name" }
};
Assert.Equal("TestObject1", fn(context, properties)(context));
properties = new LanguageExpression.PropertyBag
{
{ "path", "notProperty" }
};
Assert.Null(fn(context, properties)(context));
}
#region Helper methods
private static ExpressionBuilderFn GetFunction(string name)
{
return Functions.Builtin.Single(f => f.Name == name).Fn;
}
private static PSRuleOption GetOption()
{
var option = new PSRuleOption();
option.Configuration["config1"] = "123";
return option;
}
private static Source[] GetSource()
{
var builder = new SourcePipelineBuilder(null, null);
builder.Directory(GetSourcePath("Selectors.Rule.yaml"));
return builder.Build();
}
private static ExpressionContext GetContext()
{
var targetObject = new PSObject();
targetObject.Properties.Add(new PSNoteProperty("name", "TestObject1"));
var context = new Runtime.RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContextBuilder(GetOption(), null, null, null).Build(), null), null);
var s = GetSource();
var result = new ExpressionContext(s[0].File[0], Definitions.ResourceKind.Rule, targetObject);
context.Init(s);
context.Begin();
context.PushScope(Runtime.RunspaceScope.Precondition);
context.EnterTargetObject(new TargetObject(targetObject));
return result;
}
private static string GetSourcePath(string fileName)
{
return System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
}
#endregion Helper methods
}
}

Просмотреть файл

@ -0,0 +1,133 @@
[
{
// 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
}
},
"equals": "TestObj"
}
}
},
{
// Synopsis: An expression function example.
"apiVersion": "github.com/microsoft/PSRule/v1",
"kind": "Selector",
"metadata": {
"name": "Json.Fn.Example2"
},
"spec": {
"if": {
"value": {
"$": {
"configuration": "ConfigArray"
}
},
"count": 5
}
}
},
{
// Synopsis: An expression function example.
"apiVersion": "github.com/microsoft/PSRule/v1",
"kind": "Selector",
"metadata": {
"name": "Json.Fn.Example3"
},
"spec": {
"if": {
"value": {
"$": {
"boolean": true
}
},
"equals": true
}
}
},
{
// 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"
}
]
}
},
"equals": "TestObject1-TestObject1"
}
}
},
{
// Synopsis: An expression function example.
"apiVersion": "github.com/microsoft/PSRule/v1",
"kind": "Selector",
"metadata": {
"name": "Json.Fn.Example5"
},
"spec": {
"if": {
"value": {
"$": {
"integer": 6
}
},
"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"
}
]
}
}
}
}
}
]

Просмотреть файл

@ -0,0 +1,88 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example1
spec:
if:
value:
$:
substring:
path: name
length: 7
equals: TestObj
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example2
spec:
if:
value:
$:
configuration: 'ConfigArray'
count: 5
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example3
spec:
if:
value:
$:
boolean: true
equals: true
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example4
spec:
if:
value:
$:
concat:
- path: name
- string: '-'
- path: name
equals: TestObject1-TestObject1
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example5
spec:
if:
value:
$:
integer: 6
greater: 5
---
# Synopsis: An expression function example.
apiVersion: github.com/microsoft/PSRule/v1
kind: Selector
metadata:
name: Yaml.Fn.Example6
spec:
if:
value: TestObject1-TestObject1
equals:
$:
concat:
- path: name
- string: '-'
- path: name

Просмотреть файл

@ -70,6 +70,12 @@
<None Update="FromFileName.Rule.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Functions.Rule.jsonc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Functions.Rule.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ModuleConfig.Rule.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

Просмотреть файл

@ -27,6 +27,8 @@ namespace PSRule
{
private const string SelectorYamlFileName = "Selectors.Rule.yaml";
private const string SelectorJsonFileName = "Selectors.Rule.jsonc";
private const string FunctionsYamlFileName = "Functions.Rule.yaml";
private const string FunctionsJsonFileName = "Functions.Rule.jsonc";
[Theory]
[InlineData("Yaml", SelectorYamlFileName)]
@ -1651,11 +1653,40 @@ namespace PSRule
#endregion Properties
#region Functions
[Theory]
[InlineData("Yaml", FunctionsYamlFileName)]
[InlineData("Json", FunctionsJsonFileName)]
public void WithFunction(string type, string path)
{
var example1 = GetSelectorVisitor($"{type}.Fn.Example1", GetSource(path), out _);
var example2 = GetSelectorVisitor($"{type}.Fn.Example2", GetSource(path), out _);
var example3 = GetSelectorVisitor($"{type}.Fn.Example3", GetSource(path), out _);
var example4 = GetSelectorVisitor($"{type}.Fn.Example4", GetSource(path), out _);
var example5 = GetSelectorVisitor($"{type}.Fn.Example5", GetSource(path), out _);
var example6 = GetSelectorVisitor($"{type}.Fn.Example6", 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));
Assert.True(example6.Match(actual1));
}
#endregion Functions
#region Helper methods
private static PSRuleOption GetOption()
{
return new PSRuleOption();
var option = new PSRuleOption();
option.Configuration["ConfigArray"] = new string[] { "1", "2", "3", "4", "5" };
return option;
}
private static Source[] GetSource(string path)
@ -1676,7 +1707,8 @@ namespace PSRule
private static SelectorVisitor GetSelectorVisitor(string name, Source[] source, out RunspaceContext context)
{
context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContext(), null), null);
var builder = new OptionContextBuilder(GetOption(), null, null, null);
context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, builder.Build(), null), null);
context.Init(source);
context.Begin();
var selector = HostHelper.GetSelector(source, context).ToArray().FirstOrDefault(s => s.Name == name);