зеркало из https://github.com/microsoft/PSRule.git
Родитель
13357d76bf
Коммит
b37a09832c
|
@ -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)
|
||||
{
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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>
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче