From 56c62328726350cc28756f53f1952db2e98d26ad Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 13 Jul 2021 00:23:12 +1000 Subject: [PATCH] Added string selector conditions #178 (#179) --- CHANGELOG.md | 11 +- README.md | 8 +- .../PSDocs/en-US/about_PSDocs_Selectors.md | 204 +++++++++++++++++- schemas/PSDocs-language.schema.json | 146 +++++++++++++ src/PSDocs/Common/DictionaryExtensions.cs | 21 ++ src/PSDocs/Common/ExpressionHelpers.cs | 56 +++++ .../Selectors/SelectorExpressions.cs | 136 +++++++++++- tests/PSDocs.Tests/SelectorTests.cs | 143 +++++++++++- tests/PSDocs.Tests/Selectors.Doc.yaml | 124 +++++++++++ 9 files changed, 834 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52e6136..c49c17c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +What's changed since pre-release v0.9.0-B2107015: + +- General improvements: + - Added string selector conditions. [#178](https://github.com/microsoft/PSDocs/issues/178) + - Use `startWith`, `contains`, and `endsWith` to check for a sub-string. + - Use `isString`, `isLower`, and `isUpper` to check for string type and casing. + - See [about_PSDocs_Selectors] and [about_PSDocs_Options] for more details. + ## v0.9.0-B2107015 (pre-release) What's changed since pre-release v0.9.0-B2107010: @@ -22,7 +30,7 @@ What's changed since pre-release v0.9.0-B2107002: - Script block based conditions are PowerShell code that can be added to `Document` blocks with `-If`. - Selector block based conditions are YAML filters that can be added to `Document` blocks with `-With`. - Added options for configuring processing of input. - - See [about_PSDocs_Options] for more details. + - See [about_PSDocs_Selectors] and [about_PSDocs_Options] for more details. - General improvements: - Added schema for PSDocs configuration options within `ps-docs.yaml`. [#113](https://github.com/Microsoft/PSDocs/issues/113) @@ -326,3 +334,4 @@ What's changed since v0.1.0: [about_PSDocs_Conventions]: docs/concepts/PSDocs/en-US/about_PSDocs_Conventions.md [about_PSDocs_Keywords]: docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md [about_PSDocs_Options]: docs/concepts/PSDocs/en-US/about_PSDocs_Options.md +[about_PSDocs_Selectors]: docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md diff --git a/README.md b/README.md index c8787b3..b55c331 100644 --- a/README.md +++ b/README.md @@ -135,13 +135,18 @@ The following conceptual topics exist in the `PSDocs` module: - [Selectors](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md) - [AllOf](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#allof) - [AnyOf](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#anyof) - - [Exists](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#exists) + - [Contains](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#contains) - [Equals](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#equals) + - [EndsWith](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#endswith) + - [Exists](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#exists) - [Field](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#field) - [Greater](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#greater) - [GreaterOrEquals](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#greaterorequals) - [HasValue](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#hasvalue) - [In](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#in) + - [IsLower](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#islower) + - [IsString](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#isstring) + - [IsUpper](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#isupper) - [Less](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#less) - [LessOrEquals](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#lessorequals) - [Match](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#match) @@ -149,6 +154,7 @@ The following conceptual topics exist in the `PSDocs` module: - [NotEquals](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notequals) - [NotIn](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notin) - [NotMatch](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notmatch) + - [StartsWith](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#startswith) - [Variables](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md) - [$Culture](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#culture) - [$Document](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#document) diff --git a/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md b/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md index 111194d..a38f3cc 100644 --- a/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md +++ b/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md @@ -19,18 +19,24 @@ When evaluating an object from input, PSDocs can use selectors to perform comple The following conditions are available: -- [Exists](#exists) +- [Contains](#contains) - [Equals](#equals) +- [EndsWith](#endswith) +- [Exists](#exists) - [Greater](#greater) - [GreaterOrEquals](#greaterorequals) - [HasValue](#hasvalue) - [In](#in) +- [IsLower](#islower) +- [IsString](#isstring) +- [IsUpper](#isupper) - [Less](#less) - [LessOrEquals](#lessorequals) - [Match](#match) - [NotEquals](#notequals) - [NotIn](#notin) - [NotMatch](#notmatch) +- [StartsWith](#startswith) The following operators are available: @@ -59,8 +65,8 @@ Document 'SampleWithSelector' -With 'BasicSelector' { } ``` -Selector pre-conditions can be used together with type and script block pre-conditions. -If one or more selector pre-conditions are used, they are evaluated before type or script block pre-conditions. +Selector pre-conditions can be used together with script block pre-conditions. +If one or more selector pre-conditions are used, they are evaluated before script block pre-conditions. ### Defining selectors @@ -140,18 +146,18 @@ spec: exists: true ``` -### Exists +### Contains -The `exists` condition determines if the specified field exists. +The `contains` condition can be used to determine if the operand contains a specified sub-string. +One or more strings to compare can be specified. Syntax: ```yaml -exists: +contains: ``` -- When `exists: true`, exists will return `true` if the field exists. -- When `exists: false`, exists will return `true` if the field does not exist. +- If the operand is a field, and the field does not exist, _contains_ always returns `false`. For example: @@ -159,11 +165,16 @@ For example: apiVersion: github.com/microsoft/PSDocs/v1 kind: Selector metadata: - name: 'ExampleExists' + name: 'ExampleContains' spec: if: - field: 'Name' - exists: true + anyOf: + - field: 'url' + contains: '/azure/' + - field: 'url' + contains: + - 'github.io' + - 'github.com' ``` ### Equals @@ -189,6 +200,63 @@ spec: equals: 'TargetObject1' ``` +### EndsWith + +The `endsWith` condition can be used to determine if the operand ends with a specified string. +One or more strings to compare can be specified. + +Syntax: + +```yaml +endsWith: +``` + +- If the operand is a field, and the field does not exist, _endsWith_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleEndsWith' +spec: + if: + anyOf: + - field: 'hostname' + endsWith: '.com' + - field: 'hostname' + endsWith: + - '.com.au' + - '.com' +``` + +### Exists + +The `exists` condition determines if the specified field exists. + +Syntax: + +```yaml +exists: +``` + +- When `exists: true`, exists will return `true` if the field exists. +- When `exists: false`, exists will return `true` if the field does not exist. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleExists' +spec: + if: + field: 'Name' + exists: true +``` + ### Field The comparison property `field` is used with a condition to determine field of the object to evaluate. @@ -311,6 +379,89 @@ spec: - 'Value2' ``` +### IsLower + +The `isLower` condition determines if the operand is a lowercase string. + +Syntax: + +```yaml +isLower: +``` + +- When `isLower: true`, _isLower_ will return `true` if the operand is a lowercase string. + Non-letter characters are ignored. +- When `isLower: false`, _isLower_ will return `true` if the operand is not a lowercase string. +- If the operand is a field, and the field does not exist _isLower_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleIsLower' +spec: + if: + field: 'Name' + isLower: true +``` + +### IsString + +The `isString` condition determines if the operand is a string or other type. + +Syntax: + +```yaml +isString: +``` + +- When `isString: true`, _isString_ will return `true` if the operand is a string. +- When `isString: false`, _isString_ will return `true` if the operand is not a string or is null. +- If the operand is a field, and the field does not exist _isString_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleIsString' +spec: + if: + field: 'Name' + isString: true +``` + +### IsUpper + +The `isUpper` condition determines if the operand is an uppercase string. + +Syntax: + +```yaml +isUpper: +``` + +- When `isUpper: true`, _isUpper_ will return `true` if the operand is an uppercase string. + Non-letter characters are ignored. +- When `isUpper: false`, _isUpper_ will return `true` if the operand is not an uppercase string. +- If the operand is a field, and the field does not exist _isUpper_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleIsUpper' +spec: + if: + field: 'Name' + isUpper: true +``` + ### Less Syntax: @@ -474,6 +625,37 @@ spec: notMatch: '$(abc|efg)$' ``` +### StartsWith + +The `startsWith` condition can be used to determine if the operand starts with a specified string. +One or more strings to compare can be specified. + +Syntax: + +```yaml +startsWith: +``` + +- If the operand is a field, and the field does not exist, _startsWith_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleStartsWith' +spec: + if: + anyOf: + - field: 'url' + startsWith: 'http' + - field: 'url' + startsWith: + - 'http://' + - 'https://' +``` + ## EXAMPLES ### Example Selectors.Doc.yaml diff --git a/schemas/PSDocs-language.schema.json b/schemas/PSDocs-language.schema.json index dc8070b..a5e503b 100644 --- a/schemas/PSDocs-language.schema.json +++ b/schemas/PSDocs-language.schema.json @@ -144,6 +144,24 @@ }, { "$ref": "#/definitions/selectorConditionGreaterOrEquals" + }, + { + "$ref": "#/definitions/selectorConditionStartsWith" + }, + { + "$ref": "#/definitions/selectorConditionEndsWith" + }, + { + "$ref": "#/definitions/selectorConditionContains" + }, + { + "$ref": "#/definitions/selectorConditionIsString" + }, + { + "$ref": "#/definitions/selectorConditionIsLower" + }, + { + "$ref": "#/definitions/selectorConditionIsUpper" } ] }, @@ -476,6 +494,134 @@ "$ref": "#/definitions/selectorProperties" } ] + }, + "selectorConditionStartsWith": { + "type": "object", + "properties": { + "startsWith": { + "title": "Starts with", + "description": "Must start with one of the specified values.", + "markdownDescription": "Must start with one of the specified values. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#startswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + } + }, + "required": [ + "startsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionEndsWith": { + "type": "object", + "properties": { + "endsWith": { + "title": "Ends with", + "description": "Must end with one of the specified values.", + "markdownDescription": "Must end with one of the specified values. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#endswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + } + }, + "required": [ + "endsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionContains": { + "type": "object", + "properties": { + "contains": { + "title": "Contains", + "description": "Must contain one of the specified values.", + "markdownDescription": "Must contain one of the specified values. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#contains)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + } + }, + "required": [ + "contains" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionIsString": { + "type": "object", + "properties": { + "isString": { + "type": "boolean", + "title": "Is string", + "description": "Must be a string type.", + "markdownDescription": "Must be a string type. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#isstring)" + } + }, + "required": [ + "isString" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionIsLower": { + "type": "object", + "properties": { + "isLower": { + "type": "boolean", + "title": "Is Lowercase", + "description": "Must be a lowercase string.", + "markdownDescription": "Must be a lowercase string. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#islower)" + } + }, + "required": [ + "isLower" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionIsUpper": { + "type": "object", + "properties": { + "isUpper": { + "type": "boolean", + "title": "Is Uppercase", + "description": "Must be an uppercase string.", + "markdownDescription": "Must be an uppercase string. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#isupper)" + } + }, + "required": [ + "isUpper" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorExpressionValueMultiString": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] } } } diff --git a/src/PSDocs/Common/DictionaryExtensions.cs b/src/PSDocs/Common/DictionaryExtensions.cs index 406e89b..ff964f7 100644 --- a/src/PSDocs/Common/DictionaryExtensions.cs +++ b/src/PSDocs/Common/DictionaryExtensions.cs @@ -105,6 +105,16 @@ public static bool TryGetString(this IDictionary dictionary, str return false; } + [DebuggerStepThrough] + public static bool TryGetStringArray(this IDictionary dictionary, string key, out string[] value) + { + value = null; + if (!dictionary.TryGetValue(key, out object o)) + return false; + + return TryStringArray(o, out value); + } + [DebuggerStepThrough] public static void AddUnique(this IDictionary dictionary, IEnumerable> values) { @@ -117,5 +127,16 @@ public static void AddUnique(this IDictionary dictionary, IEnume dictionary.Add(kv.Key, kv.Value); } } + + [DebuggerStepThrough] + private static bool TryStringArray(object o, out string[] value) + { + value = default; + if (o == null) + return false; + + value = o.GetType().IsArray ? ((object[])o).OfType().ToArray() : new string[] { o.ToString() }; + return true; + } } } diff --git a/src/PSDocs/Common/ExpressionHelpers.cs b/src/PSDocs/Common/ExpressionHelpers.cs index 664a6eb..139c1ff 100644 --- a/src/PSDocs/Common/ExpressionHelpers.cs +++ b/src/PSDocs/Common/ExpressionHelpers.cs @@ -336,6 +336,62 @@ internal static bool Match(object pattern, object value, bool caseSensitive) return TryString(pattern, out string patternString) && TryString(value, out string s) && Match(patternString, s, caseSensitive); } + internal static bool StartsWith(string actualValue, object expectedValue, bool caseSensitive) + { + if (!TryString(expectedValue, out string expected)) + return false; + + return actualValue.StartsWith(expected, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + } + + internal static bool EndsWith(string actualValue, object expectedValue, bool caseSensitive) + { + if (!TryString(expectedValue, out string expected)) + return false; + + return actualValue.EndsWith(expected, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + } + + internal static bool Contains(string actualValue, object expectedValue, bool caseSensitive) + { + if (!TryString(expectedValue, out string expected)) + return false; + + return actualValue.IndexOf(expected, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) >= 0; + } + + internal static bool IsLower(string actualValue, bool requireLetters, out bool notLetter) + { + notLetter = false; + for (var i = 0; i < actualValue.Length; i++) + { + if (!char.IsLetter(actualValue, i) && requireLetters) + { + notLetter = true; + return false; + } + if (char.IsLetter(actualValue, i) && !char.IsLower(actualValue, i)) + return false; + } + return true; + } + + internal static bool IsUpper(string actualValue, bool requireLetters, out bool notLetter) + { + notLetter = false; + for (var i = 0; i < actualValue.Length; i++) + { + if (!char.IsLetter(actualValue, i) && requireLetters) + { + notLetter = true; + return false; + } + if (char.IsLetter(actualValue, i) && !char.IsUpper(actualValue, i)) + return false; + } + return true; + } + internal static bool AnyValue(object actualValue, object expectedValue, bool caseSensitive, out object foundValue) { foundValue = actualValue; diff --git a/src/PSDocs/Definitions/Selectors/SelectorExpressions.cs b/src/PSDocs/Definitions/Selectors/SelectorExpressions.cs index d365828..f4b9065 100644 --- a/src/PSDocs/Definitions/Selectors/SelectorExpressions.cs +++ b/src/PSDocs/Definitions/Selectors/SelectorExpressions.cs @@ -172,6 +172,7 @@ private static bool DebuggerFn(SelectorContext context, string path, SelectorExp /// internal sealed class SelectorExpressions { + // Conditions private const string EXISTS = "exists"; private const string EQUALS = "equals"; private const string NOTEQUALS = "notEquals"; @@ -184,7 +185,14 @@ internal sealed class SelectorExpressions private const string LESSOREQUALS = "lessOrEquals"; private const string GREATER = "greater"; private const string GREATEROREQUALS = "greaterOrEquals"; - + private const string STARTSWITH = "startsWith"; + private const string ENDSWITH = "endsWith"; + private const string CONTAINS = "contains"; + private const string ISSTRING = "isString"; + private const string ISLOWER = "isLower"; + private const string ISUPPER = "isUpper"; + + // Operators private const string IF = "if"; private const string ANYOF = "anyOf"; private const string ALLOF = "allOf"; @@ -213,6 +221,12 @@ internal sealed class SelectorExpressions new SelectorExpresssionDescriptor(LESSOREQUALS, SelectorExpressionType.Condition, LessOrEquals), new SelectorExpresssionDescriptor(GREATER, SelectorExpressionType.Condition, Greater), new SelectorExpresssionDescriptor(GREATEROREQUALS, SelectorExpressionType.Condition, GreaterOrEquals), + new SelectorExpresssionDescriptor(STARTSWITH, SelectorExpressionType.Condition, StartsWith), + new SelectorExpresssionDescriptor(ENDSWITH, SelectorExpressionType.Condition, EndsWith), + new SelectorExpresssionDescriptor(CONTAINS, SelectorExpressionType.Condition, Contains), + new SelectorExpresssionDescriptor(ISSTRING, SelectorExpressionType.Condition, IsString), + new SelectorExpresssionDescriptor(ISLOWER, SelectorExpressionType.Condition, IsLower), + new SelectorExpresssionDescriptor(ISUPPER, SelectorExpressionType.Condition, IsUpper), }; #region Operators @@ -454,6 +468,102 @@ internal static bool GreaterOrEquals(SelectorContext context, SelectorInfo info, return false; } + internal static bool StartsWith(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyStringArray(properties, STARTSWITH, out string[] propertyValue) && TryOperand(context, o, properties, out object operand)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, STARTSWITH, operand, propertyValue); + if (!ExpressionHelpers.TryString(operand, out string value)) + return false; + + for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + { + if (ExpressionHelpers.StartsWith(value, propertyValue[i], caseSensitive: false)) + return true; + } + return false; + } + return false; + } + + internal static bool EndsWith(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyStringArray(properties, ENDSWITH, out string[] propertyValue) && TryOperand(context, o, properties, out object operand)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, ENDSWITH, operand, propertyValue); + if (!ExpressionHelpers.TryString(operand, out string value)) + return false; + + for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + { + if (ExpressionHelpers.EndsWith(value, propertyValue[i], caseSensitive: false)) + return true; + } + return false; + } + return false; + } + + internal static bool Contains(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyStringArray(properties, CONTAINS, out string[] propertyValue) && TryOperand(context, o, properties, out object operand)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, CONTAINS, operand, propertyValue); + if (!ExpressionHelpers.TryString(operand, out string value)) + return false; + + for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + { + if (ExpressionHelpers.Contains(value, propertyValue[i], caseSensitive: false)) + return true; + } + return false; + } + return false; + } + + internal static bool IsString(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyBool(properties, ISSTRING, out bool? propertyValue) && TryOperand(context, o, properties, out object operand)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, ISSTRING, operand, propertyValue); + return propertyValue == ExpressionHelpers.TryString(operand, out _); + } + return false; + } + + internal static bool IsLower(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyBool(properties, ISLOWER, out bool? propertyValue) && TryOperand(context, o, properties, out object operand)) + { + if (!ExpressionHelpers.TryString(operand, out string value)) + return !propertyValue.Value; + + context.Debug(PSDocsResources.SelectorExpressionTrace, ISLOWER, operand, propertyValue); + return propertyValue == ExpressionHelpers.IsLower(value, requireLetters: false, notLetter: out _); + } + return false; + } + + internal static bool IsUpper(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyBool(properties, ISUPPER, out bool? propertyValue) && TryOperand(context, o, properties, out object operand)) + { + if (!ExpressionHelpers.TryString(operand, out string value)) + return !propertyValue.Value; + + context.Debug(PSDocsResources.SelectorExpressionTrace, ISUPPER, operand, propertyValue); + return propertyValue == ExpressionHelpers.IsUpper(value, requireLetters: false, notLetter: out _); + } + return false; + } + #endregion Conditions #region Helper methods @@ -478,6 +588,15 @@ private static bool TryField(SelectorExpression.PropertyBag properties, out stri return properties.TryGetString(FIELD, out field); } + private static bool TryOperand(SelectorContext context, object o, SelectorExpression.PropertyBag properties, out object operand) + { + operand = null; + if (properties.TryGetString(FIELD, out string field)) + return ObjectHelper.GetField(context, o, field, caseSensitive: false, out operand); + + return false; + } + private static bool TryPropertyArray(SelectorExpression.PropertyBag properties, string propertyName, out Array propertyValue) { if (properties.TryGetValue(propertyName, out object array) && array is Array arrayValue) @@ -489,6 +608,21 @@ private static bool TryPropertyArray(SelectorExpression.PropertyBag properties, return false; } + private static bool TryPropertyStringArray(SelectorExpression.PropertyBag properties, string propertyName, out string[] propertyValue) + { + if (properties.TryGetStringArray(propertyName, out propertyValue)) + { + return true; + } + else if (properties.TryGetString(propertyName, out string s)) + { + propertyValue = new string[] { s }; + return true; + } + propertyValue = null; + return false; + } + private static SelectorExpression.PropertyBag GetProperties(object[] args) { return (SelectorExpression.PropertyBag)args[0]; diff --git a/tests/PSDocs.Tests/SelectorTests.cs b/tests/PSDocs.Tests/SelectorTests.cs index 00988aa..21fdcd9 100644 --- a/tests/PSDocs.Tests/SelectorTests.cs +++ b/tests/PSDocs.Tests/SelectorTests.cs @@ -21,7 +21,7 @@ public void ReadSelector() var context = new RunspaceContext(new PipelineContext(GetOption(), null, null, null, null, null)); var selector = HostHelper.GetSelector(context, GetSource()).ToArray(); Assert.NotNull(selector); - Assert.Equal(22, selector.Length); + Assert.Equal(31, selector.Length); Assert.Equal("BasicSelector", selector[0].Name); Assert.Equal("YamlAllOf", selector[4].Name); @@ -279,6 +279,147 @@ public void GreaterOrEqualsExpression() Assert.False(greaterOrEquals.Match(actual7)); } + [Fact] + public void StartsWithExpression() + { + var startsWith = GetSelectorVisitor("YamlStartsWith"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "efg")); + var actual3 = GetObject((name: "value", value: "hij")); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + Assert.True(startsWith.Match(actual1)); + Assert.True(startsWith.Match(actual2)); + Assert.False(startsWith.Match(actual3)); + Assert.False(startsWith.Match(actual4)); + Assert.False(startsWith.Match(actual5)); + Assert.False(startsWith.Match(actual6)); + } + + [Fact] + public void EndsWithExpression() + { + var endsWith = GetSelectorVisitor("YamlEndsWith"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "efg")); + var actual3 = GetObject((name: "value", value: "hij")); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + Assert.True(endsWith.Match(actual1)); + Assert.True(endsWith.Match(actual2)); + Assert.False(endsWith.Match(actual3)); + Assert.False(endsWith.Match(actual4)); + Assert.False(endsWith.Match(actual5)); + Assert.False(endsWith.Match(actual6)); + } + + [Fact] + public void ContainsExpression() + { + var contains = GetSelectorVisitor("YamlContains"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "bcd")); + var actual3 = GetObject((name: "value", value: "hij")); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + Assert.True(contains.Match(actual1)); + Assert.True(contains.Match(actual2)); + Assert.False(contains.Match(actual3)); + Assert.False(contains.Match(actual4)); + Assert.False(contains.Match(actual5)); + Assert.False(contains.Match(actual6)); + } + + [Fact] + public void IsStringExpression() + { + var isStringTrue = GetSelectorVisitor("YamlIsStringTrue"); + var isStringFalse = GetSelectorVisitor("YamlIsStringFalse"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: 4)); + var actual3 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: null)); + var actual5 = GetObject(); + + // isString: true + Assert.True(isStringTrue.Match(actual1)); + Assert.False(isStringTrue.Match(actual2)); + Assert.False(isStringTrue.Match(actual3)); + Assert.False(isStringTrue.Match(actual4)); + Assert.False(isStringTrue.Match(actual5)); + + // isString: false + Assert.False(isStringFalse.Match(actual1)); + Assert.True(isStringFalse.Match(actual2)); + Assert.True(isStringFalse.Match(actual3)); + Assert.True(isStringFalse.Match(actual4)); + Assert.False(isStringFalse.Match(actual5)); + } + + [Fact] + public void IsLowerExpression() + { + var isLowerTrue = GetSelectorVisitor("YamlIsLowerTrue"); + var isLowerFalse = GetSelectorVisitor("YamlIsLowerFalse"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "aBc")); + var actual3 = GetObject((name: "value", value: "a-b-c")); + var actual4 = GetObject((name: "value", value: 4)); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + // isLower: true + Assert.True(isLowerTrue.Match(actual1)); + Assert.False(isLowerTrue.Match(actual2)); + Assert.True(isLowerTrue.Match(actual3)); + Assert.False(isLowerTrue.Match(actual4)); + Assert.False(isLowerTrue.Match(actual5)); + Assert.False(isLowerTrue.Match(actual6)); + + // isLower: false + Assert.False(isLowerFalse.Match(actual1)); + Assert.True(isLowerFalse.Match(actual2)); + Assert.False(isLowerFalse.Match(actual3)); + Assert.True(isLowerFalse.Match(actual4)); + Assert.True(isLowerFalse.Match(actual5)); + Assert.False(isLowerTrue.Match(actual6)); + } + + [Fact] + public void IsUpperExpression() + { + var isUpperTrue = GetSelectorVisitor("YamlIsUpperTrue"); + var isUpperFalse = GetSelectorVisitor("YamlIsUpperFalse"); + var actual1 = GetObject((name: "value", value: "ABC")); + var actual2 = GetObject((name: "value", value: "aBc")); + var actual3 = GetObject((name: "value", value: "A-B-C")); + var actual4 = GetObject((name: "value", value: 4)); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + // isUpper: true + Assert.True(isUpperTrue.Match(actual1)); + Assert.False(isUpperTrue.Match(actual2)); + Assert.True(isUpperTrue.Match(actual3)); + Assert.False(isUpperTrue.Match(actual4)); + Assert.False(isUpperTrue.Match(actual5)); + Assert.False(isUpperTrue.Match(actual6)); + + // isUpper: false + Assert.False(isUpperFalse.Match(actual1)); + Assert.True(isUpperFalse.Match(actual2)); + Assert.False(isUpperFalse.Match(actual3)); + Assert.True(isUpperFalse.Match(actual4)); + Assert.True(isUpperFalse.Match(actual5)); + Assert.False(isUpperFalse.Match(actual6)); + } + #endregion Conditions #region Operators diff --git a/tests/PSDocs.Tests/Selectors.Doc.yaml b/tests/PSDocs.Tests/Selectors.Doc.yaml index 54b93f9..9c8bdf8 100644 --- a/tests/PSDocs.Tests/Selectors.Doc.yaml +++ b/tests/PSDocs.Tests/Selectors.Doc.yaml @@ -263,6 +263,130 @@ spec: field: 'Value' greaterOrEquals: 3 +--- +# Synopsis: Test startsWith +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlStartsWith +spec: + if: + allOf: + - anyOf: + - field: 'Value' + startsWith: 'a' + - field: 'Value' + startsWith: 'e' + - field: 'Value' + startsWith: + - 'a' + - 'e' + +--- +# Synopsis: Test endsWith +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlEndsWith +spec: + if: + allOf: + - anyOf: + - field: 'Value' + endsWith: 'c' + - field: 'Value' + endsWith: 'g' + - field: 'Value' + endsWith: + - 'c' + - 'g' + +--- +# Synopsis: Test contains +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlContains +spec: + if: + allOf: + - anyOf: + - field: 'Value' + contains: 'ab' + - field: 'Value' + contains: 'bc' + - field: 'Value' + contains: + - 'ab' + - 'bc' + +--- +# Synopsis: Test isString +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsStringTrue +spec: + if: + field: 'Value' + isString: true + +--- +# Synopsis: Test isString +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsStringFalse +spec: + if: + field: 'Value' + isString: false + +--- +# Synopsis: Test isLower +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsLowerTrue +spec: + if: + field: 'Value' + isLower: true + +--- +# Synopsis: Test isLower +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsLowerFalse +spec: + if: + field: 'Value' + isLower: false + + +--- +# Synopsis: Test isUpper +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsUpperTrue +spec: + if: + field: 'Value' + isUpper: true + +--- +# Synopsis: Test isUpper +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsUpperFalse +spec: + if: + field: 'Value' + isUpper: false + --- # Synopsis: A selector to match basic objects apiVersion: github.com/microsoft/PSDocs/v1