diff --git a/.vscode/settings.json b/.vscode/settings.json index e4c4f5964b..75e83e463a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,6 +58,18 @@ ], "files.insertFinalNewline": true }, + "[jsonc]": { + "editor.tabSize": 4, + "editor.tabCompletion": "on", + "editor.quickSuggestions": { + "strings": true + }, + "editor.suggest.insertMode": "replace", + "gitlens.codeLens.scopes": [ + "document" + ], + "files.insertFinalNewline": true + }, "files.associations": { "**/.azure-pipelines/*.yaml": "azure-pipelines", "**/.azure-pipelines/jobs/*.yaml": "azure-pipelines", @@ -67,6 +79,7 @@ "APPSERVICEMININSTANCECOUNT", "cmdlet", "cmdlets", + "concat", "datetime", "deserialize", "deserialized", diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md index 3188dac922..7c42ed3360 100644 --- a/docs/CHANGELOG-v2.md +++ b/docs/CHANGELOG-v2.md @@ -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: diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md new file mode 100644 index 0000000000..124ed9f1d1 --- /dev/null +++ b/docs/expressions/functions.md @@ -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 +``` diff --git a/docs/specs/function-spec.md b/docs/specs/function-spec.md new file mode 100644 index 0000000000..8378dcdb40 --- /dev/null +++ b/docs/specs/function-spec.md @@ -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 +``` diff --git a/mkdocs.yml b/mkdocs.yml index b7bbc8a882..240ab34e5f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json index 69db8259d2..e4346d5dac 100644 --- a/schemas/PSRule-language.schema.json +++ b/schemas/PSRule-language.schema.json @@ -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", + "title": "Value from boolean", + "description": "A value to compare." + }, + { + "type": "integer", + "title": "Value from integer", + "description": "A value to compare." }, { - "type": "boolean" + "type": "object", + "title": "Value for object", + "description": "A value to compare.", + "not": { + "propertyNames": { + "enum": [ + "$" + ] + } + } }, { - "type": "integer" + "$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" + ] + } + } + } + } } } -} \ No newline at end of file +} diff --git a/src/PSRule/Common/DictionaryExtensions.cs b/src/PSRule/Common/DictionaryExtensions.cs index 42a8276d50..fa65fdffca 100644 --- a/src/PSRule/Common/DictionaryExtensions.cs +++ b/src/PSRule/Common/DictionaryExtensions.cs @@ -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 @@ public static bool TryGetLong(this IDictionary dictionary, strin 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 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 @@ public static bool TryGetString(this IDictionary dictionary, str return false; } + [DebuggerStepThrough] + public static bool TryGetEnumerable(this IDictionary 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 dictionary, string key, out string[] value) { diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs index 8d5e9aabfa..8249200744 100644 --- a/src/PSRule/Common/ExpressionHelpers.cs +++ b/src/PSRule/Common/ExpressionHelpers.cs @@ -65,6 +65,16 @@ internal static bool Equal(object expectedValue, object actualValue, bool caseSe 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 @@ internal static bool TryString(object o, bool convert, out string value) 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 = l_value.ToString(Thread.CurrentThread.CurrentCulture); + return true; + } + else if (convert && TryBool(o, false, out var b_value)) { - value = ivalue.ToString(Thread.CurrentThread.CurrentCulture); + 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 @@ internal static bool TryBool(object o, bool convert, out bool value) 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 @@ private static void SetPipelineCache(string prefix, string key, T value) /// 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) diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index cc3c8b5411..e4481f8c04 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -21,7 +21,7 @@ namespace PSRule { /// - /// A base PSObject converter. + /// A base converter. /// internal abstract class PSObjectBaseConverter : JsonConverter { @@ -345,7 +345,7 @@ protected override IList CreateProperties(Type type, MemberSeriali } /// - /// A customer deserializer to convert JSON into ResourceObject + /// A custom deserializer to convert JSON into ResourceObject /// internal sealed class ResourceObjectJsonConverter : JsonConverter { @@ -385,6 +385,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s 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 @@ private static bool SkipComments(JsonReader reader) } /// - /// A JSON converter for deserializing Language Expressions + /// A custom converter for deserializing JSON into a language expression. /// 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 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } /// - /// Skip JSON comments. + /// Map an operator. /// - 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 @@ private LanguageExpression MapOperator(string type, JsonReader reader) reader.Read(); } } - return result; } @@ -707,47 +700,96 @@ private LanguageExpression MapCondition(string type, LanguageExpression.Property 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(); + 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 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r var objects = new List(); 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 @@ private void MapProperty(LanguageExpression.PropertyBag properties, JsonReader r properties.Add(key, reader.Value); } } - reader.Read(); } } @@ -788,6 +829,44 @@ private bool TryCondition(string key) 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(string type, out T expression) where T : LanguageExpression { expression = null; diff --git a/src/PSRule/Common/JsonReaderExtensions.cs b/src/PSRule/Common/JsonReaderExtensions.cs index 9bb5c0c123..6285368c9d 100644 --- a/src/PSRule/Common/JsonReaderExtensions.cs +++ b/src/PSRule/Common/JsonReaderExtensions.cs @@ -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 @@ public static bool GetSourceExtent(this JsonReader reader, string file, out ISou 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(); + } + + /// + /// Skip JSON comments. + /// + [DebuggerStepThrough] + public static bool SkipComments(this JsonReader reader) + { + var hasComments = false; + while (reader.TokenType == JsonToken.Comment && reader.Read()) + hasComments = true; + + return hasComments; + } } } diff --git a/src/PSRule/Common/YamlConverters.cs b/src/PSRule/Common/YamlConverters.cs index 6b5344fa2b..1027fc7d9b 100644 --- a/src/PSRule/Common/YamlConverters.cs +++ b/src/PSRule/Common/YamlConverters.cs @@ -115,7 +115,7 @@ public void WriteYaml(IEmitter emitter, object value, Type type) 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 @@ private PSNoteProperty ReadNoteProperty(IParser parser, string name) if (parser.TryConsume(out _)) { var values = new List(); - while (!(parser.Current is SequenceEnd)) + while (parser.Current is not SequenceEnd) { if (parser.Current is MappingStart) { @@ -308,16 +308,16 @@ public bool Resolve(NodeEvent nodeEvent, ref Type currentType) /// internal sealed class OrderedPropertiesTypeInspector : TypeInspectorSkeleton { - private readonly ITypeInspector _innerTypeDescriptor; + private readonly ITypeInspector _InnerTypeDescriptor; public OrderedPropertiesTypeInspector(ITypeInspector innerTypeDescriptor) { - _innerTypeDescriptor = innerTypeDescriptor; + _InnerTypeDescriptor = innerTypeDescriptor; } public override IEnumerable GetProperties(Type type, object container) { - return _innerTypeDescriptor + return _InnerTypeDescriptor .GetProperties(type, container) .OrderBy(prop => prop.Name); } @@ -598,17 +598,22 @@ private bool TryResource(IParser reader, string apiVersion, string kind, Func + /// A custom deserializer to convert YAML into a language expression. + /// 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 nestedObjectDeserializer, out object value) @@ -625,6 +630,9 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func + /// Map an operator. + /// private LanguageExpression MapOperator(string type, IParser reader, Func nestedObjectDeserializer) { if (TryExpression(reader, type, nestedObjectDeserializer, out LanguageOperator result)) @@ -674,17 +682,78 @@ private LanguageExpression MapExpression(IParser reader, Func(out _)) { result = MapOperator(key, reader, nestedObjectDeserializer); } - else if (TryOperator(key) && reader.Accept(out SequenceStart sequence)) + else if (TryOperator(key) && reader.Accept(out _)) { result = MapOperator(key, reader, nestedObjectDeserializer); } return result; } + private ExpressionFnOuter MapFunction(string type, IParser reader, Func nestedObjectDeserializer) + { + _FunctionBuilder.Push(); + string name = null; + while (!(reader.Accept(out _) || reader.Accept(out _))) + { + if (reader.TryConsume(out var s)) + { + if (name != null) + { + _FunctionBuilder.Add(name, s.Value); + name = null; + } + else + { + name = s.Value; + } + } + else if (reader.TryConsume(out _)) + { + var child = MapFunction(name, reader, nestedObjectDeserializer); + if (name != null) + { + _FunctionBuilder.Add(name, child); + name = null; + } + reader.Consume(); + } + else if (reader.TryConsume(out _)) + { + var sequence = MapSequence(name, reader, nestedObjectDeserializer); + if (name != null) + { + _FunctionBuilder.Add(name, sequence); + name = null; + } + reader.Consume(); + } + } + var result = _FunctionBuilder.Pop(); + return result; + } + + private object MapSequence(string name, IParser reader, Func nestedObjectDeserializer) + { + var result = new List(); + while (!reader.Accept(out _)) + { + if (reader.TryConsume(out var s)) + result.Add(s.Value); + + else if (reader.TryConsume(out _)) + { + var child = MapFunction(name, reader, nestedObjectDeserializer); + result.Add(child); + reader.Consume(); + } + } + return result.ToArray(); + } + private void MapProperty(LanguageExpression.PropertyBag properties, IParser reader, Func nestedObjectDeserializer, out string name) { name = null; @@ -698,6 +767,15 @@ private void MapProperty(LanguageExpression.PropertyBag properties, IParser read { properties[key] = scalar.Value; } + else if (TryValue(key, reader, nestedObjectDeserializer, out var value)) + { + properties[key] = value; + } + else if (TryCondition(key) && reader.TryConsume(out _)) + { + if (TryFunction(reader, nestedObjectDeserializer, out var fn)) + properties[key] = fn; + } else if (TryCondition(key) && reader.TryConsume(out _)) { var objects = new List(); @@ -723,6 +801,40 @@ private bool TryCondition(string key) return _Factory.IsCondition(key); } + private bool TryValue(string key, IParser reader, Func nestedObjectDeserializer, out object value) + { + value = null; + if (key != "value") + return false; + + if (reader.TryConsume(out _) && TryFunction(reader, nestedObjectDeserializer, out var fn)) + { + value = fn; + return true; + } + reader.SkipThisAndNestedEvents(); + return false; + } + + private bool TryFunction(IParser reader, Func nestedObjectDeserializer, out ExpressionFnOuter fn) + { + fn = null; + if (!IsFunction(reader)) + return false; + + reader.Consume(); + reader.Consume(); + fn = MapFunction("$", reader, nestedObjectDeserializer); + reader.Consume(); + reader.Consume(); + return true; + } + + private static bool IsFunction(IParser reader) + { + return reader.Accept(out var scalar) || scalar.Value == "$"; + } + private bool TryExpression(IParser reader, string type, Func nestedObjectDeserializer, out T expression) where T : LanguageExpression { expression = null; diff --git a/src/PSRule/Definitions/Expressions/Exceptions.cs b/src/PSRule/Definitions/Expressions/Exceptions.cs new file mode 100644 index 0000000000..eda4c4e5c1 --- /dev/null +++ b/src/PSRule/Definitions/Expressions/Exceptions.cs @@ -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 +{ + /// + /// A base class for runtime exceptions. + /// + 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); + } + } +} diff --git a/src/PSRule/Definitions/Expressions/ExpressionContext.cs b/src/PSRule/Definitions/Expressions/ExpressionContext.cs index e4e24687dd..1cc576be6f 100644 --- a/src/PSRule/Definitions/Expressions/ExpressionContext.cs +++ b/src/PSRule/Definitions/Expressions/ExpressionContext.cs @@ -16,6 +16,8 @@ internal interface IExpressionContext : IBindingContext void Reason(IOperand operand, string text, params object[] args); + object Current { get; } + RunspaceContext GetContext(); } @@ -25,12 +27,13 @@ internal sealed class ExpressionContext : IExpressionContext, IBindingContext private List _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(); + Current = current; } public SourceFile Source { get; } @@ -39,6 +42,8 @@ internal ExpressionContext(SourceFile source, ResourceKind kind) public ResourceKind Kind { get; } + public object Current { get; } + [DebuggerStepThrough] void IBindingContext.CachePathExpression(string path, PathExpression expression) { diff --git a/src/PSRule/Definitions/Expressions/FunctionBuilder.cs b/src/PSRule/Definitions/Expressions/FunctionBuilder.cs new file mode 100644 index 0000000000..2f1e2aac11 --- /dev/null +++ b/src/PSRule/Definitions/Expressions/FunctionBuilder.cs @@ -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 _Stack; + private readonly FunctionFactory _Functions; + + private LanguageExpression.PropertyBag _Current; + + internal FunctionBuilder() : this(new FunctionFactory()) { } + + internal FunctionBuilder(FunctionFactory expressionFactory) + { + _Functions = expressionFactory; + _Stack = new Stack(); + } + + 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 _Descriptors; + + public FunctionFactory() + { + _Descriptors = new Dictionary(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); + } + } + + /// + /// A structure describing a specific function. + /// + [DebuggerDisplay("Function: {Name}")] + internal sealed class FunctionDescriptor : IFunctionDescriptor + { + public FunctionDescriptor(string name, ExpressionBuilderFn fn) + { + Name = name; + Fn = fn; + } + + /// + public string Name { get; } + + /// + public ExpressionBuilderFn Fn { get; } + } + + /// + /// A structure describing a specific function. + /// + internal interface IFunctionDescriptor + { + /// + /// The name of the function. + /// + string Name { get; } + + /// + /// The function delegate. + /// + ExpressionBuilderFn Fn { get; } + } +} diff --git a/src/PSRule/Definitions/Expressions/Functions.cs b/src/PSRule/Definitions/Expressions/Functions.cs new file mode 100644 index 0000000000..b369310a77 --- /dev/null +++ b/src/PSRule/Definitions/Expressions/Functions.cs @@ -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 +{ + /// + /// Implementation of Azure Resource Manager template functions as ExpressionFn. + /// + 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"; + + /// + /// The available built-in functions. + /// + 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 + } +} diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 72f1c78667..9a7dd18e13 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -21,7 +21,9 @@ internal enum LanguageExpressionType { Operator = 1, - Condition = 2 + Condition = 2, + + Function = 3 } internal sealed class ExpressionInfo @@ -47,7 +49,9 @@ public LanguageExpressionFactory() 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 @@ public bool IsCondition(string name) 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 @@ internal sealed class LanguageExpressions 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 @@ internal static bool Equals(ExpressionContext context, ExpressionInfo info, obje 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 @@ internal static bool NotEquals(ExpressionContext context, ExpressionInfo info, o 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 @@ internal static bool Subset(ExpressionContext context, ExpressionInfo info, obje 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 @@ internal static bool Less(ExpressionContext context, ExpressionInfo info, object ReasonStrings.Assert_IsNullOrEmpty ); + var operandValue = Value(context, operand); if (!ExpressionHelpers.CompareNumeric( - operand.Value, + operandValue, propertyValue, convert, compare: out var compare, @@ -763,7 +766,7 @@ internal static bool Less(ExpressionContext context, ExpressionInfo info, object operand, compare < 0, ReasonStrings.Assert_NotComparedTo, - operand.Value, + operandValue, LESS_THAN, propertyValue ); @@ -772,7 +775,7 @@ internal static bool Less(ExpressionContext context, ExpressionInfo info, object 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 @@ internal static bool LessOrEquals(ExpressionContext context, ExpressionInfo info ReasonStrings.Assert_IsNullOrEmpty ); + var operandValue = Value(context, operand); if (!ExpressionHelpers.CompareNumeric( - operand.Value, + operandValue, propertyValue, convert, compare: out var compare, @@ -799,7 +803,7 @@ internal static bool LessOrEquals(ExpressionContext context, ExpressionInfo info operand, compare <= 0, ReasonStrings.Assert_NotComparedTo, - operand.Value, + operandValue, LESS_THAN_EQUALS, propertyValue ); @@ -808,7 +812,7 @@ internal static bool LessOrEquals(ExpressionContext context, ExpressionInfo info 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 @@ internal static bool Greater(ExpressionContext context, ExpressionInfo info, obj ReasonStrings.Assert_IsNullOrEmpty ); + var operandValue = Value(context, operand); if (!ExpressionHelpers.CompareNumeric( - operand.Value, + operandValue, propertyValue, convert, compare: out var compare, @@ -835,7 +840,7 @@ internal static bool Greater(ExpressionContext context, ExpressionInfo info, obj operand, compare > 0, ReasonStrings.Assert_NotComparedTo, - operand.Value, + operandValue, GREATER_THAN, propertyValue ); @@ -844,7 +849,7 @@ internal static bool Greater(ExpressionContext context, ExpressionInfo info, obj 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 @@ internal static bool GreaterOrEquals(ExpressionContext context, ExpressionInfo i ReasonStrings.Assert_IsNullOrEmpty ); + var operandValue = Value(context, operand); if (!ExpressionHelpers.CompareNumeric( - operand.Value, + operandValue, propertyValue, convert, compare: out var compare, @@ -871,7 +877,7 @@ internal static bool GreaterOrEquals(ExpressionContext context, ExpressionInfo i operand, compare >= 0, ReasonStrings.Assert_NotComparedTo, - operand.Value, + operandValue, GREATER_THAN_EQUALS, propertyValue ); @@ -1484,9 +1490,17 @@ private static bool TryPropertyBoolOrDefault(LanguageExpression.PropertyBag prop 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 @@ private static bool TryType(IExpressionContext context, LanguageExpression.Prope if (string.IsNullOrEmpty(type)) return Invalid(context, svalue); - operand = Operand.FromType(type, binding.TargetTypePath); } return operand != null; @@ -1557,6 +1570,36 @@ private static bool TrySource(IExpressionContext context, LanguageExpression.Pro 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; + } + + /// + /// Unwrap a function delegate or a literal value. + /// + private static object Value(IExpressionContext context, IOperand operand) + { + if (operand == null) + return null; + + return operand.Value is ExpressionFnOuter fn ? fn(context) : operand.Value; + } + + /// + /// Unwrap a function delegate or a literal value. + /// + 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 @@ private static bool TryOperand(ExpressionContext context, string name, object o, TryType(context, properties, out operand) || TryName(context, properties, out operand) || TrySource(context, properties, out operand) || + TryValue(context, properties, out operand) || Invalid(context, name); } diff --git a/src/PSRule/Definitions/Expressions/Primitives.cs b/src/PSRule/Definitions/Expressions/Primitives.cs index dccd59300a..0f71649944 100644 --- a/src/PSRule/Definitions/Expressions/Primitives.cs +++ b/src/PSRule/Definitions/Expressions/Primitives.cs @@ -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 @@ public LanguageExpression CreateInstance(SourceFile source, LanguageExpression.P if (Type == LanguageExpressionType.Condition) return new LanguageCondition(this, properties); - return null; + return Type == LanguageExpressionType.Function ? new LanguageFunction(this) : null; } } @@ -104,4 +104,14 @@ internal void Add(PropertyBag properties) Property.AddUnique(properties); } } + + [DebuggerDisplay("Selector {Descriptor.Name}")] + internal sealed class LanguageFunction : LanguageExpression + { + internal LanguageFunction(LanguageExpresssionDescriptor descriptor) + : base(descriptor) + { + + } + } } diff --git a/src/PSRule/Definitions/Rules/RuleVisitor.cs b/src/PSRule/Definitions/Rules/RuleVisitor.cs index 0a886245f5..608d5ae9f9 100644 --- a/src/PSRule/Definitions/Rules/RuleVisitor.cs +++ b/src/PSRule/Definitions/Rules/RuleVisitor.cs @@ -50,7 +50,7 @@ public void Dispose() 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 diff --git a/src/PSRule/Definitions/Selectors/SelectorVisitor.cs b/src/PSRule/Definitions/Selectors/SelectorVisitor.cs index 81bfcd7f9a..d7ab303dbf 100644 --- a/src/PSRule/Definitions/Selectors/SelectorVisitor.cs +++ b/src/PSRule/Definitions/Selectors/SelectorVisitor.cs @@ -42,7 +42,7 @@ public SelectorVisitor(ResourceId id, SourceFile source, LanguageIf expression) 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); } diff --git a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs index f8b1d2a914..9d8ace4efb 100644 --- a/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs +++ b/src/PSRule/Definitions/SuppressionGroups/SuppressionGroupVisitor.cs @@ -77,7 +77,7 @@ internal void Hit() 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)) { diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index c163edfa07..0b6fb4c4fc 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -260,7 +260,7 @@ private static ILanguageBlock[] GetYamlLanguageBlocks(Source[] sources, Runspace 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(out _); while (parser.Current is DocumentStart) { diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index 12e9d59e5f..881e558509 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -8,10 +8,11 @@ // //------------------------------------------------------------------------------ -namespace PSRule.Resources { +namespace PSRule.Resources +{ using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -22,706 +23,980 @@ namespace PSRule.Resources { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class PSRuleResources { - + internal class PSRuleResources + { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal PSRuleResources() { + internal PSRuleResources() + { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Resources.PSRuleResources", typeof(PSRuleResources).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { + internal static global::System.Globalization.CultureInfo Culture + { + get + { return resourceCulture; } - set { + set + { resourceCulture = value; } } - + + /// + /// Looks up a localized string similar to The arguments for '{0}' are not in the expected format or type.. + /// + internal static string ArgumentFormatInvalid + { + get + { + return ResourceManager.GetString("ArgumentFormatInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid boolean.. + /// + internal static string ArgumentInvalidBoolean + { + get + { + return ResourceManager.GetString("ArgumentInvalidBoolean", resourceCulture); + } + } + /// - /// Looks up a localized string similar to The {0} resource '{1}' is currently referencing '{2}' using the alias '{3}'. Consider updating the reference to use name or id directly.. + /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid integer.. /// - internal static string AliasReference { - get { + internal static string ArgumentInvalidInteger + { + get + { + return ResourceManager.GetString("ArgumentInvalidInteger", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument '{0}' for '{1}' is not a valid string.. + /// + internal static string ArgumentInvalidString + { + get + { + return ResourceManager.GetString("ArgumentInvalidString", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The number of arguments '{1}' is not within the allowed range for '{0}'.. + /// + internal static string ArgumentsOutOfRange + { + get + { + return ResourceManager.GetString("ArgumentsOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The baseline '{0}' is obsolete. Consider switching to an alternative baseline.. + /// + internal static string AliasReference + { + get + { return ResourceManager.GetString("AliasReference", resourceCulture); } } - + /// /// Looks up a localized string similar to Suppression for the rule '{0}' was configured using the alias '{1}'. Consider updating the suppression to use the name or id directly.. /// - internal static string AliasSuppression { - get { + internal static string AliasSuppression + { + get + { return ResourceManager.GetString("AliasSuppression", resourceCulture); } } - + /// /// Looks up a localized string similar to Binding functions are not supported in this language mode.. /// - internal static string ConstrainedTargetBinding { - get { + internal static string ConstrainedTargetBinding + { + get + { return ResourceManager.GetString("ConstrainedTargetBinding", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}: The property '${1}.{2}' is obsolete and will be removed in the next major version.. /// - internal static string DebugPropertyObsolete { - get { + internal static string DebugPropertyObsolete + { + get + { return ResourceManager.GetString("DebugPropertyObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to Target failed If precondition. /// - internal static string DebugTargetIfMismatch { - get { + internal static string DebugTargetIfMismatch + { + get + { return ResourceManager.GetString("DebugTargetIfMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to Target failed Rule precondition. /// - internal static string DebugTargetRuleMismatch { - get { + internal static string DebugTargetRuleMismatch + { + get + { return ResourceManager.GetString("DebugTargetRuleMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to Target failed Type precondition. /// - internal static string DebugTargetTypeMismatch { - get { + internal static string DebugTargetTypeMismatch + { + get + { return ResourceManager.GetString("DebugTargetTypeMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to A circular rule dependency was detected. The rule '{0}' depends on '{1}' which also depend on '{0}'.. /// - internal static string DependencyCircularReference { - get { + internal static string DependencyCircularReference + { + get + { return ResourceManager.GetString("DependencyCircularReference", resourceCulture); } } - + /// /// Looks up a localized string similar to The dependency '{0}' for '{1}' could not be found. Check that the rule is defined in a .Rule.ps1 file within the search path.. /// - internal static string DependencyNotFound { - get { + internal static string DependencyNotFound + { + get + { return ResourceManager.GetString("DependencyNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to A rule with the same id '{0}' already exists.. /// - internal static string DuplicateRuleId { - get { + internal static string DuplicateRuleId + { + get + { return ResourceManager.GetString("DuplicateRuleId", resourceCulture); } } - + /// /// Looks up a localized string similar to A rule with the same name '{0}' already exists.. /// - internal static string DuplicateRuleName { - get { + internal static string DuplicateRuleName + { + get + { return ResourceManager.GetString("DuplicateRuleName", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} : Reported '{1}'. At {2}:{3} char:{4}. /// - internal static string ErrorDetailMessage { - get { + internal static string ErrorDetailMessage + { + get + { return ResourceManager.GetString("ErrorDetailMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more errors occured.. /// - internal static string ErrorPipelineException { - get { + internal static string ErrorPipelineException + { + get + { return ResourceManager.GetString("ErrorPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to Exists: {0}. /// - internal static string ExistsTrue { - get { + internal static string ExistsTrue + { + get + { return ResourceManager.GetString("ExistsTrue", resourceCulture); } } - + /// /// Looks up a localized string similar to File. /// - internal static string FileSourceType { - get { + internal static string FileSourceType + { + get + { return ResourceManager.GetString("FileSourceType", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Failed to parse expression. The expression may not be valid. Expression: "{0}". + /// + internal static string ExpressionInvalid + { + get + { + return ResourceManager.GetString("ExpressionInvalid", resourceCulture); + } + } + /// /// Looks up a localized string similar to [PSRule][D] -- Found {0} PSRule module(s). /// - internal static string FoundModules { - get { + internal static string FoundModules + { + get + { return ResourceManager.GetString("FoundModules", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The function "{0}" was not found.. + /// + internal static string FunctionNotFound + { + get + { + return ResourceManager.GetString("FunctionNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The language expression index '{0}' is not valid for the object.. + /// + internal static string IndexInvalid + { + get + { + return ResourceManager.GetString("IndexInvalid", resourceCulture); + } + } + /// /// Looks up a localized string similar to Output written to the following file: '{0}'. /// - internal static string InfoOutputPath { - get { + internal static string InfoOutputPath + { + get + { return ResourceManager.GetString("InfoOutputPath", resourceCulture); } } - + /// /// Looks up a localized string similar to An invalid ErrorAction ({0}) was specified for rule at {1}. Ignore | Stop are supported.. /// - internal static string InvalidErrorAction { - get { + internal static string InvalidErrorAction + { + get + { return ResourceManager.GetString("InvalidErrorAction", resourceCulture); } } - + /// /// Looks up a localized string similar to The resource name '{0}' is not valid at {1}. Each resource name must be between 3-128 characters in length, must start and end with a letter or number, and only contain letters, numbers, hyphens, dots, or underscores. See https://aka.ms/ps-rule/naming for more information.. /// - internal static string InvalidResourceName { - get { + internal static string InvalidResourceName + { + get + { return ResourceManager.GetString("InvalidResourceName", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule nesting was detected for rule at {0}. Rules must not be nested.. /// - internal static string InvalidRuleNesting { - get { + internal static string InvalidRuleNesting + { + get + { return ResourceManager.GetString("InvalidRuleNesting", resourceCulture); } } - + /// /// Looks up a localized string similar to An invalid rule result was returned for {0}. Conditions must return boolean $True or $False.. /// - internal static string InvalidRuleResult { - get { + internal static string InvalidRuleResult + { + get + { return ResourceManager.GetString("InvalidRuleResult", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can only be used within a Rule block.. /// - internal static string KeywordConditionScope { - get { + internal static string KeywordConditionScope + { + get + { return ResourceManager.GetString("KeywordConditionScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can only be used within a Rule block or script precondition.. /// - internal static string KeywordRuleScope { - get { + internal static string KeywordRuleScope + { + get + { return ResourceManager.GetString("KeywordRuleScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The keyword '{0}' can not be nested in a Rule block.. /// - internal static string KeywordSourceScope { - get { + internal static string KeywordSourceScope + { + get + { return ResourceManager.GetString("KeywordSourceScope", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][{0}][Trace] -- {1}: {2}. /// - internal static string LanguageExpressionTraceP2 { - get { + internal static string LanguageExpressionTraceP2 + { + get + { return ResourceManager.GetString("LanguageExpressionTraceP2", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][{0}][Trace] -- {1}: {2} {1} {3}. /// - internal static string LanguageExpressionTraceP3 { - get { + internal static string LanguageExpressionTraceP3 + { + get + { return ResourceManager.GetString("LanguageExpressionTraceP3", resourceCulture); } } - + + internal static string LanguageExpressionTrace + { + get + { + return ResourceManager.GetString("LanguageExpressionTrace", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please open your browser to the following location: {0}. /// - internal static string LaunchBrowser { - get { + internal static string LaunchBrowser + { + get + { return ResourceManager.GetString("LaunchBrowser", resourceCulture); } } - + /// /// Looks up a localized string similar to Wildcard match requires exactly one name.. /// - internal static string MatchSingleName { - get { + internal static string MatchSingleName + { + get + { return ResourceManager.GetString("MatchSingleName", resourceCulture); } } - + /// /// Looks up a localized string similar to Matches: {0}. /// - internal static string MatchTrue { - get { + internal static string MatchTrue + { + get + { return ResourceManager.GetString("MatchTrue", resourceCulture); } } - + /// /// Looks up a localized string similar to Update module '{0}' to set the default baseline using a module configuration resource instead. Configuring the default baseline via manifest will be removed in the next major version. See https://aka.ms/ps-rule/module-config.. /// - internal static string ModuleManifestBaseline { - get { + internal static string ModuleManifestBaseline + { + get + { return ResourceManager.GetString("ModuleManifestBaseline", resourceCulture); } } - + /// /// Looks up a localized string similar to No valid module can be found with that name.. /// - internal static string ModuleNotFound { - get { + internal static string ModuleNotFound + { + get + { return ResourceManager.GetString("ModuleNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Target object '{0}' has not been processed because no matching rules were found.. /// - internal static string ObjectNotProcessed { - get { + internal static string ObjectNotProcessed + { + get + { return ResourceManager.GetString("ObjectNotProcessed", resourceCulture); } } - + /// /// Looks up a localized string similar to Object path not found.. /// - internal static string ObjectPathNotFound { - get { + internal static string ObjectPathNotFound + { + get + { return ResourceManager.GetString("ObjectPathNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Options file does not exist.. /// - internal static string OptionsNotFound { - get { + internal static string OptionsNotFound + { + get + { return ResourceManager.GetString("OptionsNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to # Source: {0}. /// - internal static string OptionsSourceComment { - get { + internal static string OptionsSourceComment + { + get + { return ResourceManager.GetString("OptionsSourceComment", resourceCulture); } } - + /// /// Looks up a localized string similar to Error. /// - internal static string OutcomeError { - get { + internal static string OutcomeError + { + get + { return ResourceManager.GetString("OutcomeError", resourceCulture); } } - + /// /// Looks up a localized string similar to Fail. /// - internal static string OutcomeFail { - get { + internal static string OutcomeFail + { + get + { return ResourceManager.GetString("OutcomeFail", resourceCulture); } } - + /// /// Looks up a localized string similar to Pass. /// - internal static string OutcomePass { - get { + internal static string OutcomePass + { + get + { return ResourceManager.GetString("OutcomePass", resourceCulture); } } - + /// /// Looks up a localized string similar to [FAIL] -- {0}:: Reported for '{1}'. /// - internal static string OutcomeRuleFail { - get { + internal static string OutcomeRuleFail + { + get + { return ResourceManager.GetString("OutcomeRuleFail", resourceCulture); } } - + /// /// Looks up a localized string similar to [PASS] -- {0}:: Reported for '{1}'. /// - internal static string OutcomeRulePass { - get { + internal static string OutcomeRulePass + { + get + { return ResourceManager.GetString("OutcomeRulePass", resourceCulture); } } - + /// /// Looks up a localized string similar to Unknown. /// - internal static string OutcomeUnknown { - get { + internal static string OutcomeUnknown + { + get + { return ResourceManager.GetString("OutcomeUnknown", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The language expression property '{0}' doesn't exist.. + /// + internal static string PropertyNotFound + { + get + { + return ResourceManager.GetString("PropertyNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to The property '${0}.{1}' is obsolete and will be removed in the next major version.. /// - internal static string PropertyObsolete { - get { + internal static string PropertyObsolete + { + get + { return ResourceManager.GetString("PropertyObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed to deserialize the file '{0}': {1}. /// - internal static string ReadFileFailed { - get { + internal static string ReadFileFailed + { + get + { return ResourceManager.GetString("ReadFileFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Read JSON failed.. /// - internal static string ReadJsonFailed { - get { + internal static string ReadJsonFailed + { + get + { return ResourceManager.GetString("ReadJsonFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Read JSON failed because the token ({0}) was not expected.. /// - internal static string ReadJsonFailedExpectedToken { - get { + internal static string ReadJsonFailedExpectedToken + { + get + { return ResourceManager.GetString("ReadJsonFailedExpectedToken", resourceCulture); } } - + /// /// Looks up a localized string similar to The module version '{1}' for '{0}' does not match the required version '{2}'. To continue, first update the module to match the version requirement.. /// - internal static string RequiredVersionMismatch { - get { + internal static string RequiredVersionMismatch + { + get + { return ResourceManager.GetString("RequiredVersionMismatch", resourceCulture); } } - + /// /// Looks up a localized string similar to The {0} '{1}' is obsolete. Consider switching to an alternative {0}.. /// - internal static string ResourceObsolete { - get { + internal static string ResourceObsolete + { + get + { return ResourceManager.GetString("ResourceObsolete", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed for '{1}'.. /// - internal static string RuleCountSuppressed { - get { + internal static string RuleCountSuppressed + { + get + { return ResourceManager.GetString("RuleCountSuppressed", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more rules reported errors.. /// - internal static string RuleErrorPipelineException { - get { + internal static string RuleErrorPipelineException + { + get + { return ResourceManager.GetString("RuleErrorPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to One or more rules reported failure.. /// - internal static string RuleFailPipelineException { - get { + internal static string RuleFailPipelineException + { + get + { return ResourceManager.GetString("RuleFailPipelineException", resourceCulture); } } - + /// /// Looks up a localized string similar to Inconclusive result reported for '{1}' @{0}.. /// - internal static string RuleInconclusive { - get { + internal static string RuleInconclusive + { + get + { return ResourceManager.GetString("RuleInconclusive", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][R][Trace] -- {0}. /// - internal static string RuleMatchTrace { - get { + internal static string RuleMatchTrace + { + get + { return ResourceManager.GetString("RuleMatchTrace", resourceCulture); } } - + /// /// Looks up a localized string similar to Could not find a matching rule. Please check that Path, Name and Tag parameters are correct.. /// - internal static string RuleNotFound { - get { + internal static string RuleNotFound + { + get + { return ResourceManager.GetString("RuleNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Could not find required rule definition parameter '{0}' on rule at {1}.. /// - internal static string RuleParameterNotFound { - get { + internal static string RuleParameterNotFound + { + get + { return ResourceManager.GetString("RuleParameterNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to No matching .Rule.ps1 files were found. Rule definitions should be saved into script files with the .Rule.ps1 extension.. /// - internal static string RulePathNotFound { - get { + internal static string RulePathNotFound + { + get + { return ResourceManager.GetString("RulePathNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to at Rule '{0}', {1}: line {2}. /// - internal static string RuleStackTrace { - get { + internal static string RuleStackTrace + { + get + { return ResourceManager.GetString("RuleStackTrace", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed for '{1}'.. /// - internal static string RuleSuppressed { - get { + internal static string RuleSuppressed + { + get + { return ResourceManager.GetString("RuleSuppressed", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed by suppression group '{1}' for '{2}'.. /// - internal static string RuleSuppressionGroup { - get { + internal static string RuleSuppressionGroup + { + get + { return ResourceManager.GetString("RuleSuppressionGroup", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed by suppression group '{1}' for '{2}'.. /// - internal static string RuleSuppressionGroupCount { - get { + internal static string RuleSuppressionGroupCount + { + get + { return ResourceManager.GetString("RuleSuppressionGroupCount", resourceCulture); } } - + /// /// Looks up a localized string similar to Rule '{0}' was suppressed by suppression group '{1}' for '{2}'. {3}. /// - internal static string RuleSuppressionGroupExtended { - get { + internal static string RuleSuppressionGroupExtended + { + get + { return ResourceManager.GetString("RuleSuppressionGroupExtended", resourceCulture); } } - + /// /// Looks up a localized string similar to {0} rule/s were suppressed by suppression group '{1}' for '{2}'. {3}. /// - internal static string RuleSuppressionGroupExtendedCount { - get { + internal static string RuleSuppressionGroupExtendedCount + { + get + { return ResourceManager.GetString("RuleSuppressionGroupExtendedCount", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][D] -- Scanning for source files in module: {0}. /// - internal static string ScanModule { - get { + internal static string ScanModule + { + get + { return ResourceManager.GetString("ScanModule", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][D] -- Scanning for source files: {0}. /// - internal static string ScanSource { - get { + internal static string ScanSource + { + get + { return ResourceManager.GetString("ScanSource", resourceCulture); } } - + /// /// Looks up a localized string similar to The script was not found.. /// - internal static string ScriptNotFound { - get { + internal static string ScriptNotFound + { + get + { return ResourceManager.GetString("ScriptNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to [PSRule][S][Trace] -- {0}. /// - internal static string SelectorMatchTrace { - get { + internal static string SelectorMatchTrace + { + get + { return ResourceManager.GetString("SelectorMatchTrace", resourceCulture); } } - + + /// + /// Looks up a localized string similar to [PSRule][S][Trace] -- {0}: {1}. + /// + internal static string SelectorTrace + { + get + { + return ResourceManager.GetString("SelectorTrace", resourceCulture); + } + } + /// /// Looks up a localized string similar to Can not serialize a null PSObject.. /// - internal static string SerializeNullPSObject { - get { + internal static string SerializeNullPSObject + { + get + { return ResourceManager.GetString("SerializeNullPSObject", resourceCulture); } } - + /// /// Looks up a localized string similar to Create path. /// - internal static string ShouldCreatePath { - get { + internal static string ShouldCreatePath + { + get + { return ResourceManager.GetString("ShouldCreatePath", resourceCulture); } } - + /// /// Looks up a localized string similar to Write file. /// - internal static string ShouldWriteFile { - get { + internal static string ShouldWriteFile + { + get + { return ResourceManager.GetString("ShouldWriteFile", resourceCulture); } } - + /// /// Looks up a localized string similar to The source was not found.. /// - internal static string SourceNotFound { - get { + internal static string SourceNotFound + { + get + { return ResourceManager.GetString("SourceNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to Using invariant culture may cause rule infomation to be displayed incorrectly. Consider using -Culture or set the Output.Culture option.. /// - internal static string UsingInvariantCulture { - get { + internal static string UsingInvariantCulture + { + get + { return ResourceManager.GetString("UsingInvariantCulture", resourceCulture); } } - + /// /// Looks up a localized string similar to The variable '${0}' can only be used within a Rule block.. /// - internal static string VariableConditionScope { - get { + internal static string VariableConditionScope + { + get + { return ResourceManager.GetString("VariableConditionScope", resourceCulture); } } - + /// /// Looks up a localized string similar to The version constraint '{0}' is not valid.. /// - internal static string VersionConstraintInvalid { - get { + internal static string VersionConstraintInvalid + { + get + { return ResourceManager.GetString("VersionConstraintInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to The Within parameter Value must be a string when the Like parameter is used.. /// - internal static string WithinLikeNotString { - get { + internal static string WithinLikeNotString + { + get + { return ResourceManager.GetString("WithinLikeNotString", resourceCulture); } } - + /// /// Looks up a localized string similar to Within: {0}. /// - internal static string WithinTrue { - get { + internal static string WithinTrue + { + get + { return ResourceManager.GetString("WithinTrue", resourceCulture); } } diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index a816838778..378f86ad61 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -121,6 +121,25 @@ The {0} '{1}' is obsolete. Consider switching to an alternative {0}. Occurs when a resource is used that has been flagged as obsolete. + + The arguments for '{0}' are not in the expected format or type. + + + The argument '{0}' for '{1}' is not a valid boolean. + + + The argument '{0}' for '{1}' is not a valid integer. + + + The argument '{0}' for '{1}' is not a valid string. + + + The number of arguments '{1}' is not within the allowed range for '{0}'. + + + The baseline '{0}' is obsolete. Consider switching to an alternative baseline. + Occurs when a baseline is used that has been flagged as obsolete. + Binding functions are not supported in this language mode. @@ -164,9 +183,18 @@ File + + Failed to parse expression. The expression may not be valid. Expression: "{0}" + [PSRule][D] -- Found {0} PSRule module(s) + + The function "{0}" was not found. + + + The language expression index '{0}' is not valid for the object. + An invalid ErrorAction ({0}) was specified for rule at {1}. Ignore | Stop are supported. @@ -229,6 +257,9 @@ Unknown + + The language expression property '{0}' doesn't exist. + The property '${0}.{1}' is obsolete and will be removed in the next major version. diff --git a/src/PSRule/Resources/ReasonStrings.Designer.cs b/src/PSRule/Resources/ReasonStrings.Designer.cs index f19da8323d..b9dacde3f6 100644 --- a/src/PSRule/Resources/ReasonStrings.Designer.cs +++ b/src/PSRule/Resources/ReasonStrings.Designer.cs @@ -8,11 +8,10 @@ // //------------------------------------------------------------------------------ -namespace PSRule.Resources -{ +namespace PSRule.Resources { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -23,85 +22,80 @@ namespace PSRule.Resources [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class ReasonStrings - { - + internal class ReasonStrings { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal ReasonStrings() - { + internal ReasonStrings() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if (object.ReferenceEquals(resourceMan, null)) - { + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Resources.ReasonStrings", typeof(ReasonStrings).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { + internal static global::System.Globalization.CultureInfo Culture { + get { return resourceCulture; } - set - { + set { resourceCulture = value; } } - + /// /// Looks up a localized string similar to The value '{0}' contains '{1}'.. /// - internal static string Assert_Contains - { - get - { + internal static string Assert_Contains { + get { return ResourceManager.GetString("Assert_Contains", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The number of items is '{0}' instead of '{1}'.. + /// + internal static string Assert_Count { + get { + return ResourceManager.GetString("Assert_Count", resourceCulture); + } + } + /// /// Looks up a localized string similar to The value '{0}' does not match the expression '{1}'.. /// - internal static string Assert_DoesNotMatch - { - get - { + internal static string Assert_DoesNotMatch { + get { return ResourceManager.GetString("Assert_DoesNotMatch", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' ends with '{1}'.. /// - internal static string Assert_EndsWith - { - get - { + internal static string Assert_EndsWith { + get { return ResourceManager.GetString("Assert_EndsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to Does not exist.. /// @@ -114,135 +108,129 @@ internal static string Assert_Exists { /// /// Looks up a localized string similar to The value '{0}' does not contain only lowercase characters.. /// - internal static string Assert_IsLower - { - get - { + internal static string Assert_IsLower { + get { return ResourceManager.GetString("Assert_IsLower", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The value is null.. + /// + internal static string Assert_IsNull { + get { + return ResourceManager.GetString("Assert_IsNull", resourceCulture); + } + } + /// /// Looks up a localized string similar to Is null or empty.. /// - internal static string Assert_IsNullOrEmpty - { - get - { + internal static string Assert_IsNullOrEmpty { + get { return ResourceManager.GetString("Assert_IsNullOrEmpty", resourceCulture); } } - + /// /// Looks up a localized string similar to Is set to '{0}'.. /// - internal static string Assert_IsSetTo - { - get - { + internal static string Assert_IsSetTo { + get { return ResourceManager.GetString("Assert_IsSetTo", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not contain only uppercase characters.. /// - internal static string Assert_IsUpper - { - get - { + internal static string Assert_IsUpper { + get { return ResourceManager.GetString("Assert_IsUpper", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is like '{1}'.. /// - internal static string Assert_Like - { - get - { + internal static string Assert_Like { + get { return ResourceManager.GetString("Assert_Like", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' matches the expression '{1}'.. /// - internal static string Assert_Matches - { - get - { + internal static string Assert_Matches { + get { return ResourceManager.GetString("Assert_Matches", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not an array.. /// - internal static string Assert_NotArray - { - get - { + internal static string Assert_NotArray { + get { return ResourceManager.GetString("Assert_NotArray", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not a boolean.. /// - internal static string Assert_NotBoolean - { - get - { + internal static string Assert_NotBoolean { + get { return ResourceManager.GetString("Assert_NotBoolean", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not {1} {2}.. /// - internal static string Assert_NotComparedTo - { - get - { + internal static string Assert_NotComparedTo { + get { return ResourceManager.GetString("Assert_NotComparedTo", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not contain any of {1}.. /// - internal static string Assert_NotContains - { - get - { + internal static string Assert_NotContains { + get { return ResourceManager.GetString("Assert_NotContains", resourceCulture); } } - + + /// + /// Looks up a localized string similar to The number of items is '{0}'.. + /// + internal static string Assert_NotCount { + get { + return ResourceManager.GetString("Assert_NotCount", resourceCulture); + } + } + /// /// Looks up a localized string similar to The value '{0}' is not a date.. /// - internal static string Assert_NotDateTime - { - get - { + internal static string Assert_NotDateTime { + get { return ResourceManager.GetString("Assert_NotDateTime", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not end with any of {1}.. /// - internal static string Assert_NotEndsWith - { - get - { + internal static string Assert_NotEndsWith { + get { return ResourceManager.GetString("Assert_NotEndsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to Exists.. /// @@ -255,615 +243,503 @@ internal static string Assert_NotExists { /// /// Looks up a localized string similar to The value '{0}' was not in {1}.. /// - internal static string Assert_NotInSet - { - get - { + internal static string Assert_NotInSet { + get { return ResourceManager.GetString("Assert_NotInSet", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not an integer.. /// - internal static string Assert_NotInteger - { - get - { + internal static string Assert_NotInteger { + get { return ResourceManager.GetString("Assert_NotInteger", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not like '{1}'.. /// - internal static string Assert_NotLike - { - get - { + internal static string Assert_NotLike { + get { return ResourceManager.GetString("Assert_NotLike", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not numeric.. /// - internal static string Assert_NotNumeric - { - get - { + internal static string Assert_NotNumeric { + get { return ResourceManager.GetString("Assert_NotNumeric", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the specified schemas match '{0}'.. /// - internal static string Assert_NotSpecifiedSchema - { - get - { + internal static string Assert_NotSpecifiedSchema { + get { return ResourceManager.GetString("Assert_NotSpecifiedSchema", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not start with any of {1}.. /// - internal static string Assert_NotStartsWith - { - get - { + internal static string Assert_NotStartsWith { + get { return ResourceManager.GetString("Assert_NotStartsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' is not a string.. /// - internal static string Assert_NotString - { - get - { + internal static string Assert_NotString { + get { return ResourceManager.GetString("Assert_NotString", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' starts with '{1}'.. /// - internal static string Assert_StartsWith - { - get - { + internal static string Assert_StartsWith { + get { return ResourceManager.GetString("Assert_StartsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' can not be compared with '{1}'.. /// - internal static string Compare - { - get - { + internal static string Compare { + get { return ResourceManager.GetString("Compare", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' does not contain '{1}'.. /// - internal static string Contains - { - get - { + internal static string Contains { + get { return ResourceManager.GetString("Contains", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' has '{1}' items instead of '{2}'.. /// - internal static string Count - { - get - { + internal static string Count { + get { return ResourceManager.GetString("Count", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' does not end with '{1}'.. /// - internal static string EndsWith - { - get - { + internal static string EndsWith { + get { return ResourceManager.GetString("EndsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the field(s) existed: {0}. /// - internal static string Exists - { - get - { + internal static string Exists { + get { return ResourceManager.GetString("Exists", resourceCulture); } } - + /// /// Looks up a localized string similar to The field(s) existed: {0}. /// - internal static string ExistsNot - { - get - { + internal static string ExistsNot { + get { return ResourceManager.GetString("ExistsNot", resourceCulture); } } - + /// /// Looks up a localized string similar to The header was not set.. /// - internal static string FileHeader - { - get - { + internal static string FileHeader { + get { return ResourceManager.GetString("FileHeader", resourceCulture); } } - + /// /// Looks up a localized string similar to The file '{0}' does not exist.. /// - internal static string FilePath - { - get - { + internal static string FilePath { + get { return ResourceManager.GetString("FilePath", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was not > '{1}'.. /// - internal static string Greater - { - get - { + internal static string Greater { + get { return ResourceManager.GetString("Greater", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was not >= '{1}'.. /// - internal static string GreaterOrEqual - { - get - { + internal static string GreaterOrEqual { + get { return ResourceManager.GetString("GreaterOrEqual", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' is set to '{1}'.. /// - internal static string HasExpectedFieldValue - { - get - { + internal static string HasExpectedFieldValue { + get { return ResourceManager.GetString("HasExpectedFieldValue", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' exists.. /// - internal static string HasField - { - get - { + internal static string HasField { + get { return ResourceManager.GetString("HasField", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the specified schemas match '{0}'.. /// - internal static string HasJsonSchema - { - get - { + internal static string HasJsonSchema { + get { return ResourceManager.GetString("HasJsonSchema", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' was not included in the set.. /// - internal static string In - { - get - { + internal static string In { + get { return ResourceManager.GetString("In", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' does not contain only letters.. /// - internal static string IsLetter - { - get - { + internal static string IsLetter { + get { return ResourceManager.GetString("IsLetter", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed schema validation on {0}. {1}. /// - internal static string JsonSchemaInvalid - { - get - { + internal static string JsonSchemaInvalid { + get { return ResourceManager.GetString("JsonSchemaInvalid", resourceCulture); } } - + /// /// Looks up a localized string similar to The JSON schema '{0}' could not be found.. /// - internal static string JsonSchemaNotFound - { - get - { + internal static string JsonSchemaNotFound { + get { return ResourceManager.GetString("JsonSchemaNotFound", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was not < '{1}'.. /// - internal static string Less - { - get - { + internal static string Less { + get { return ResourceManager.GetString("Less", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was not <= '{1}'.. /// - internal static string LessOrEqual - { - get - { + internal static string LessOrEqual { + get { return ResourceManager.GetString("LessOrEqual", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the regex(s) matched: {0}. /// - internal static string Match - { - get - { + internal static string Match { + get { return ResourceManager.GetString("Match", resourceCulture); } } - + /// /// Looks up a localized string similar to The regex '{0}' matched.. /// - internal static string MatchNot - { - get - { + internal static string MatchNot { + get { return ResourceManager.GetString("MatchNot", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' does not match the pattern '{1}'.. /// - internal static string MatchPattern - { - get - { + internal static string MatchPattern { + get { return ResourceManager.GetString("MatchPattern", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' has '{1}' items instead of not '{2}'.. /// - internal static string NotCount - { - get - { + internal static string NotCount { + get { return ResourceManager.GetString("NotCount", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' is not enumerable.. /// - internal static string NotEnumerable - { - get - { + internal static string NotEnumerable { + get { return ResourceManager.GetString("NotEnumerable", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' does not exist.. /// - internal static string NotHasField - { - get - { + internal static string NotHasField { + get { return ResourceManager.GetString("NotHasField", resourceCulture); } } - + /// /// Looks up a localized string similar to The value of '{0}' is null or empty.. /// - internal static string NotHasFieldValue - { - get - { + internal static string NotHasFieldValue { + get { return ResourceManager.GetString("NotHasFieldValue", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' was in the set.. /// - internal static string NotIn - { - get - { + internal static string NotIn { + get { return ResourceManager.GetString("NotIn", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' matches the pattern '{1}'.. /// - internal static string NotMatchPattern - { - get - { + internal static string NotMatchPattern { + get { return ResourceManager.GetString("NotMatchPattern", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' is not null.. /// - internal static string NotNull - { - get - { + internal static string NotNull { + get { return ResourceManager.GetString("NotNull", resourceCulture); } } - + /// /// Looks up a localized string similar to The file '{0}' is within the path '{1}'.. /// - internal static string NotWithinPath - { - get - { + internal static string NotWithinPath { + get { return ResourceManager.GetString("NotWithinPath", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' is null.. /// - internal static string Null - { - get - { + internal static string Null { + get { return ResourceManager.GetString("Null", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' is not empty.. /// - internal static string NullOrEmpty - { - get - { + internal static string NullOrEmpty { + get { return ResourceManager.GetString("NullOrEmpty", resourceCulture); } } - + /// /// Looks up a localized string similar to The parameter '{0}' is null or empty.. /// - internal static string NullOrEmptyParameter - { - get - { + internal static string NullOrEmptyParameter { + get { return ResourceManager.GetString("NullOrEmptyParameter", resourceCulture); } } - + /// /// Looks up a localized string similar to The parameter '{0}' is null.. /// - internal static string NullParameter - { - get - { + internal static string NullParameter { + get { return ResourceManager.GetString("NullParameter", resourceCulture); } } - + /// /// Looks up a localized string similar to No results were provided.. /// - internal static string ResultsNotProvided - { - get - { + internal static string ResultsNotProvided { + get { return ResourceManager.GetString("ResultsNotProvided", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' does not start with '{1}'.. /// - internal static string StartsWith - { - get - { + internal static string StartsWith { + get { return ResourceManager.GetString("StartsWith", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' is not a string.. /// - internal static string String - { - get - { + internal static string String { + get { return ResourceManager.GetString("String", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' did not contain '{1}'.. /// - internal static string Subset - { - get - { + internal static string Subset { + get { return ResourceManager.GetString("Subset", resourceCulture); } } - + /// /// Looks up a localized string similar to The field '{0}' included multiple instances of '{1}'.. /// - internal static string SubsetDuplicate - { - get - { + internal static string SubsetDuplicate { + get { return ResourceManager.GetString("SubsetDuplicate", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{2}' of type {1} is not {0}.. /// - internal static string Type - { - get - { + internal static string Type { + get { return ResourceManager.GetString("Type", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{1}' of type {0} is not an integer.. /// - internal static string TypeInteger - { - get - { + internal static string TypeInteger { + get { return ResourceManager.GetString("TypeInteger", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{1}' of type {0} is not numeric.. /// - internal static string TypeNumeric - { - get - { + internal static string TypeNumeric { + get { return ResourceManager.GetString("TypeNumeric", resourceCulture); } } - + /// /// Looks up a localized string similar to None of the type name(s) match: {0}. /// - internal static string TypeOf - { - get - { + internal static string TypeOf { + get { return ResourceManager.GetString("TypeOf", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value '{0}' is not a version string.. /// - internal static string Version - { - get - { + internal static string Version { + get { return ResourceManager.GetString("Version", resourceCulture); } } - + /// /// Looks up a localized string similar to The version '{0}' does not match the constraint '{1}'.. /// - internal static string VersionContraint - { - get - { + internal static string VersionContraint { + get { return ResourceManager.GetString("VersionContraint", resourceCulture); } } - + /// /// Looks up a localized string similar to The field value didn't match the set.. /// - internal static string Within - { - get - { + internal static string Within { + get { return ResourceManager.GetString("Within", resourceCulture); } } - + /// /// Looks up a localized string similar to The value '{0}' was within the set.. /// - internal static string WithinNot - { - get - { + internal static string WithinNot { + get { return ResourceManager.GetString("WithinNot", resourceCulture); } } - + /// /// Looks up a localized string similar to The file '{0}' is not within the path '{1}'.. /// - internal static string WithinPath - { - get - { + internal static string WithinPath { + get { return ResourceManager.GetString("WithinPath", resourceCulture); } } diff --git a/src/PSRule/Resources/ReasonStrings.resx b/src/PSRule/Resources/ReasonStrings.resx index 80c0bf534e..2b51ac2acb 100644 --- a/src/PSRule/Resources/ReasonStrings.resx +++ b/src/PSRule/Resources/ReasonStrings.resx @@ -345,4 +345,13 @@ Exists. + + The number of items is '{0}' instead of '{1}'. + + + The value is null. + + + The number of items is '{0}'. + \ No newline at end of file diff --git a/src/PSRule/Runtime/Configuration.cs b/src/PSRule/Runtime/Configuration.cs index e9f09d51ac..d64874d768 100644 --- a/src/PSRule/Runtime/Configuration.cs +++ b/src/PSRule/Runtime/Configuration.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections; @@ -69,23 +69,7 @@ public int GetIntegerOrDefault(string configurationKey, int defaultValue) 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) diff --git a/src/PSRule/Runtime/Operand.cs b/src/PSRule/Runtime/Operand.cs index 550c8597a7..224e7fe3ec 100644 --- a/src/PSRule/Runtime/Operand.cs +++ b/src/PSRule/Runtime/Operand.cs @@ -38,7 +38,12 @@ public enum OperandKind /// /// The target object itself. /// - Target = 5 + Target = 5, + + /// + /// A literal value or function. + /// + Value = 6 } /// @@ -117,6 +122,11 @@ internal static IOperand FromTarget() 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)) diff --git a/src/PSRule/Runtime/PSRule.cs b/src/PSRule/Runtime/PSRule.cs index 0285b8a592..83d0a53c39 100644 --- a/src/PSRule/Runtime/PSRule.cs +++ b/src/PSRule/Runtime/PSRule.cs @@ -136,7 +136,7 @@ public IEnumerable Output /// /// The current target object. /// - public PSObject TargetObject => GetContext().RuleRecord.TargetObject; + public PSObject TargetObject => GetContext().TargetObject.Value; /// /// The bound name of the target object. diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index a7ee9eac7c..31b5c2046e 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -771,7 +771,6 @@ public void Init(Source[] source) { InitLanguageScopes(source); Host.HostHelper.ImportResource(source, this); - foreach (var languageScope in _LanguageScopes.Get()) Pipeline.Baseline.BuildScope(languageScope); } @@ -841,6 +840,32 @@ internal bool ShouldWarnOnce(params string[] key) 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() diff --git a/tests/PSRule.Tests/FunctionBuilderTests.cs b/tests/PSRule.Tests/FunctionBuilderTests.cs new file mode 100644 index 0000000000..963551a2c1 --- /dev/null +++ b/tests/PSRule.Tests/FunctionBuilderTests.cs @@ -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 + } +} diff --git a/tests/PSRule.Tests/FunctionTests.cs b/tests/PSRule.Tests/FunctionTests.cs new file mode 100644 index 0000000000..8dedd43b18 --- /dev/null +++ b/tests/PSRule.Tests/FunctionTests.cs @@ -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 +{ + /// + /// Define tests for expression functions are working correctly. + /// + 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 + } +} diff --git a/tests/PSRule.Tests/Functions.Rule.jsonc b/tests/PSRule.Tests/Functions.Rule.jsonc new file mode 100644 index 0000000000..4caba42731 --- /dev/null +++ b/tests/PSRule.Tests/Functions.Rule.jsonc @@ -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" + } + ] + } + } + } + } + } +] diff --git a/tests/PSRule.Tests/Functions.Rule.yaml b/tests/PSRule.Tests/Functions.Rule.yaml new file mode 100644 index 0000000000..ba3f42f0eb --- /dev/null +++ b/tests/PSRule.Tests/Functions.Rule.yaml @@ -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 diff --git a/tests/PSRule.Tests/PSRule.Tests.csproj b/tests/PSRule.Tests/PSRule.Tests.csproj index ccae856278..3feade5467 100644 --- a/tests/PSRule.Tests/PSRule.Tests.csproj +++ b/tests/PSRule.Tests/PSRule.Tests.csproj @@ -70,6 +70,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 65595f1059..1f1c087178 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -27,6 +27,8 @@ public sealed class SelectorTests { 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 @@ public void Name(string type, string path) #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 @@ private static PSObject GetObject(params (string name, object value)[] propertie 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);