* Added object path expressions #808 #693
* Apply suggestions from code review

Co-authored-by: Armaan Mcleod <armaan_mcleod@outlook.com>
This commit is contained in:
Bernie White 2022-01-03 13:44:31 +10:00 коммит произвёл GitHub
Родитель 4f2f5c6001
Коммит 6cb2df7d82
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
34 изменённых файлов: 3007 добавлений и 438 удалений

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

@ -11,6 +11,15 @@ See [upgrade notes][upgrade-notes] for helpful information when upgrading from p
## Unreleased
What's changed since v1.11.0:
- General improvements:
- Added support for object path expressions. [#808](https://github.com/microsoft/PSRule/issues/808) [#693](https://github.com/microsoft/PSRule/issues/693)
- Inspired by JSONPath, object path expressions can be used to access nested objects.
- Array members can be filtered and enumerated using object path expressions.
- Object path expressions can be used in YAML, JSON, and PowerShell rules and selectors.
- See [about_PSRule_Assert] for details.
## v1.11.0
What's changed since v1.10.0:

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

@ -91,22 +91,38 @@ Rule 'Assert.HasRequiredFields' {
### Field names
Many of the built-in assertion methods accept a field name.
The field name is an expression that traverses object properties, keys or indexes of the _input object_.
Many of the built-in assertion methods accept an object path or field name.
An object path is an expression that traverses object properties, keys or indexes of the _input object_.
The syntax for an object path is inspired by JSONPath which is current an IETF Internet-Draft.
The field name can contain:
The object path expression can contain:
- Property names for PSObjects or .NET objects.
- Keys for hash table or dictionaries.
- Indexes for arrays or collections.
- Queries that filter items from array or collection properties.
For example:
- `.` refers to _input object_ itself.
- `Name` or `.Name` refers to the name property/ key of the _input object_.
- `Properties.enabled` refers to the enabled property under the Properties property.
- `Tags.env` refers to the env key under a hash table property of the _input object_.
- `Properties.securityRules[0].name` references to the name property of the first security rule.
- `.`, or `$` refers to _input object_ itself.
- `Name`, `.Name`, or `$.Name` refers to the _name_ member of the _input object_.
- `Properties.enabled` refers to the _enabled_ member under the Properties member.
Alternatively this can also be written as `Properties['enabled']`.
- `Tags.env` refers to the env member under a hash table property of the _input object_.
- `Tags+env` refers to the env member using a case-sensitive match.
- `Properties.securityRules[0].name` references to the name member of the first security rule.
- `Properties.securityRules[-1].name` references to the name member of the last security rule.
- `Properties.securityRules[?@direction == 'Inbound'].name` returns the name of any inbound rules.
This will return an array of security rule names.
Notable differences between object paths and JSONPath are:
- Member names (properties and keys) are case-insensitive by default.
To perform a case-sensitive match of a member name use a plus selector `+` in front of the member name.
Some assertions such as `HasField` provide an option to match case when matching member names.
When this is used, the plus selector perform an case-insensitive match.
- Quoted member names with single or double quotes are supported with dot selector.
i.e. `Properties.'spaced name'` is valid.
### Contains

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

@ -210,6 +210,7 @@ The following helper methods are available:
Services should implement the `IDisposable` interface to perform additional cleanup.
This method can only be called within the `-Initialize` block of a convention.
- `GetService(string id)` - Retrieves a service previously added by a convention.
- `GetPath(object sourceObject, string path)` - Evalute an object path expression and returns the resulting objects.
The file format is detected based on the same file formats as the option `Input.Format`.
i.e. Yaml, Json, Markdown, and PowerShell Data.

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

@ -0,0 +1,26 @@
``` ini
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=5.0.404
[Host] : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
DefaultJob : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
```
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
|------------------------- |-------------:|------------:|------------:|-----------:|----------:|----------:|
| Invoke | 50,742.5 μs | 908.47 μs | 709.27 μs | 4100.0000 | 400.0000 | 17,758 KB |
| InvokeIf | 53,048.6 μs | 698.34 μs | 619.06 μs | 4500.0000 | 200.0000 | 20,008 KB |
| InvokeType | 50,575.6 μs | 794.27 μs | 663.25 μs | 4000.0000 | 200.0000 | 17,760 KB |
| InvokeSummary | 50,449.0 μs | 698.80 μs | 619.47 μs | 4100.0000 | 400.0000 | 17,758 KB |
| Assert | 52,152.6 μs | 765.95 μs | 678.99 μs | 4200.0000 | 300.0000 | 18,462 KB |
| Get | 5,793.8 μs | 86.70 μs | 81.10 μs | 78.1250 | - | 364 KB |
| GetHelp | 5,799.6 μs | 76.72 μs | 71.77 μs | 85.9375 | 7.8125 | 364 KB |
| Within | 89,538.2 μs | 1,754.26 μs | 1,555.11 μs | 8000.0000 | 1000.0000 | 34,102 KB |
| WithinBulk | 128,126.9 μs | 1,928.80 μs | 1,709.83 μs | 14666.6667 | 1333.3333 | 61,131 KB |
| WithinLike | 112,174.1 μs | 1,132.30 μs | 1,003.76 μs | 11666.6667 | 1666.6667 | 48,258 KB |
| DefaultTargetNameBinding | 695.6 μs | 13.57 μs | 14.52 μs | 38.0859 | - | 156 KB |
| CustomTargetNameBinding | 851.0 μs | 10.35 μs | 8.64 μs | 85.9375 | - | 352 KB |
| NestedTargetNameBinding | 961.5 μs | 17.83 μs | 15.80 μs | 85.9375 | - | 352 KB |
| AssertHasFieldValue | 3,033.5 μs | 60.15 μs | 66.85 μs | 253.9063 | 7.8125 | 1,040 KB |

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

@ -0,0 +1,26 @@
``` ini
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=5.0.404
[Host] : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
DefaultJob : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
```
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
|------------------------- |-------------:|------------:|------------:|-----------:|----------:|----------:|
| Invoke | 50,529.4 μs | 1,006.40 μs | 941.38 μs | 4000.0000 | 444.4444 | 17,758 KB |
| InvokeIf | 51,974.4 μs | 667.26 μs | 591.51 μs | 4500.0000 | 200.0000 | 20,008 KB |
| InvokeType | 49,901.2 μs | 679.83 μs | 567.69 μs | 4000.0000 | 363.6364 | 17,758 KB |
| InvokeSummary | 51,198.9 μs | 862.22 μs | 922.57 μs | 4000.0000 | 363.6364 | 17,758 KB |
| Assert | 52,136.6 μs | 588.93 μs | 550.88 μs | 4100.0000 | 300.0000 | 18,461 KB |
| Get | 5,710.0 μs | 111.69 μs | 104.47 μs | 85.9375 | 7.8125 | 364 KB |
| GetHelp | 5,777.4 μs | 97.83 μs | 91.51 μs | 85.9375 | 7.8125 | 364 KB |
| Within | 88,106.3 μs | 1,752.66 μs | 1,799.86 μs | 8000.0000 | 1000.0000 | 34,102 KB |
| WithinBulk | 125,319.9 μs | 2,303.80 μs | 2,154.98 μs | 14666.6667 | 1000.0000 | 61,133 KB |
| WithinLike | 115,376.3 μs | 1,866.04 μs | 1,654.20 μs | 11666.6667 | 1666.6667 | 48,258 KB |
| DefaultTargetNameBinding | 669.5 μs | 6.52 μs | 6.10 μs | 38.0859 | - | 156 KB |
| CustomTargetNameBinding | 837.6 μs | 6.70 μs | 6.27 μs | 85.9375 | - | 352 KB |
| NestedTargetNameBinding | 854.1 μs | 9.50 μs | 7.42 μs | 85.9375 | - | 352 KB |
| AssertHasFieldValue | 2,967.0 μs | 38.88 μs | 34.47 μs | 253.9063 | 7.8125 | 1,040 KB |

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

@ -0,0 +1,29 @@
``` ini
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=5.0.404
[Host] : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
DefaultJob : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
```
| Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Allocated |
|------------------------- |-----------------:|----------------:|----------------:|-----------------:|-----------:|----------:|----------:|
| Invoke | 57,242,675.2 ns | 1,046,305.99 ns | 1,659,552.18 ns | 56,566,420.0 ns | 4000.0000 | 400.0000 | 17,758 KB |
| InvokeIf | 55,980,115.4 ns | 1,025,771.27 ns | 856,565.47 ns | 55,884,877.8 ns | 4555.5556 | 222.2222 | 20,009 KB |
| InvokeType | 54,975,254.3 ns | 1,092,979.70 ns | 2,105,800.83 ns | 54,328,650.0 ns | 4000.0000 | 400.0000 | 17,758 KB |
| InvokeSummary | 54,241,116.2 ns | 1,084,485.01 ns | 1,065,109.29 ns | 54,387,930.0 ns | 4100.0000 | 400.0000 | 17,758 KB |
| Assert | 56,276,886.0 ns | 1,125,031.59 ns | 1,295,588.05 ns | 56,145,355.0 ns | 4100.0000 | 300.0000 | 18,461 KB |
| Get | 6,151,314.0 ns | 119,861.25 ns | 179,402.68 ns | 6,120,217.2 ns | 85.9375 | 7.8125 | 364 KB |
| GetHelp | 6,099,816.5 ns | 71,918.32 ns | 63,753.72 ns | 6,103,212.5 ns | 85.9375 | 7.8125 | 364 KB |
| Within | 96,332,873.3 ns | 1,229,258.29 ns | 1,149,848.97 ns | 96,156,225.0 ns | 8250.0000 | 1000.0000 | 34,196 KB |
| WithinBulk | 150,572,688.2 ns | 2,948,247.26 ns | 4,760,869.78 ns | 149,014,300.0 ns | 14000.0000 | 2000.0000 | 61,224 KB |
| WithinLike | 131,081,361.1 ns | 2,560,132.70 ns | 4,277,406.62 ns | 130,734,100.0 ns | 11000.0000 | 2000.0000 | 48,351 KB |
| DefaultTargetNameBinding | 782,038.6 ns | 15,156.92 ns | 23,146.22 ns | 777,590.8 ns | 37.1094 | - | 156 KB |
| CustomTargetNameBinding | 938,919.9 ns | 12,345.26 ns | 10,943.75 ns | 939,380.4 ns | 85.9375 | - | 352 KB |
| NestedTargetNameBinding | 915,123.7 ns | 8,567.18 ns | 7,153.98 ns | 917,281.0 ns | 85.9375 | - | 352 KB |
| AssertHasFieldValue | 3,080,458.7 ns | 57,566.72 ns | 112,279.46 ns | 3,037,285.5 ns | 253.9063 | 7.8125 | 1,040 KB |
| PathTokenize | 822.9 ns | 8.02 ns | 7.50 ns | 821.9 ns | 0.2632 | - | 1 KB |
| PathExpressionBuild | 557.2 ns | 11.17 ns | 26.34 ns | 546.3 ns | 0.3500 | - | 1 KB |
| PathExpressionGet | 364,673.4 ns | 4,007.73 ns | 3,748.84 ns | 364,478.3 ns | 17.0898 | - | 70 KB |

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

@ -9,6 +9,7 @@ using System.Reflection;
using BenchmarkDotNet.Attributes;
using PSRule.Configuration;
using PSRule.Pipeline;
using PSRule.Runtime.ObjectPath;
namespace PSRule.Benchmark
{
@ -47,6 +48,9 @@ namespace PSRule.Benchmark
private IPipeline _InvokeWithinPipeline;
private IPipeline _InvokeWithinBulkPipeline;
private IPipeline _InvokeWithinLikePipeline;
private PathExpressionBuilder _PathExpressionBuilder;
private IPathToken[] _PathExpressionTokens;
private PathExpression _PathExpression;
[GlobalSetup]
public void Prepare()
@ -63,6 +67,8 @@ namespace PSRule.Benchmark
PrepareInvokeWithinLikePipeline();
PrepareTargetObjects();
PrepareAssertHasFieldValuePipeline();
PreparePathExpressionBuild();
PreparePathExpressionSelect();
}
private void PrepareGetPipeline()
@ -155,6 +161,17 @@ namespace PSRule.Benchmark
_AssertHasFieldValuePipeline = builder.Build();
}
private void PreparePathExpressionBuild()
{
_PathExpressionBuilder = new PathExpressionBuilder();
_PathExpressionTokens = PathTokenizer.Get("$.Properties.logs[?@.enabled && @.enabled==true].category");
}
private void PreparePathExpressionSelect()
{
_PathExpression = PathExpression.Create("$.Properties.logs[?@.enabled && @.enabled==true].category");
}
private Source[] GetSource()
{
var builder = new SourcePipelineBuilder(null, null);
@ -252,38 +269,32 @@ namespace PSRule.Benchmark
[Benchmark]
public void DefaultTargetNameBinding()
{
foreach (var targetObject in _TargetObject)
{
PipelineHookActions.BindTargetName(null, false, false, targetObject);
}
for (var i = 0; i < _TargetObject.Length; i++)
PipelineHookActions.BindTargetName(null, false, false, _TargetObject[i]);
}
[Benchmark]
public void CustomTargetNameBinding()
{
foreach (var targetObject in _TargetObject)
{
for (var i = 0; i < _TargetObject.Length; i++)
PipelineHookActions.BindTargetName(
propertyNames: new string[] { "TargetName", "Name" },
caseSensitive: true,
preferTargetInfo: false,
targetObject: targetObject
targetObject: _TargetObject[i]
);
}
}
[Benchmark]
public void NestedTargetNameBinding()
{
foreach (var targetObject in _TargetObject)
{
for (var i = 0; i < _TargetObject.Length; i++)
PipelineHookActions.BindTargetName(
propertyNames: new string[] { "TargetName", "Name" },
caseSensitive: true,
preferTargetInfo: false,
targetObject: targetObject
targetObject: _TargetObject[i]
);
}
}
[Benchmark]
@ -292,6 +303,25 @@ namespace PSRule.Benchmark
RunPipelineTargets(_AssertHasFieldValuePipeline);
}
[Benchmark]
public void PathTokenize()
{
PathTokenizer.Get("$.Properties.logs[?@.enabled && @.enabled==true].category");
}
[Benchmark]
public void PathExpressionBuild()
{
_PathExpressionBuilder.Build(_PathExpressionTokens);
}
[Benchmark]
public void PathExpressionGet()
{
for (var i = 0; i < _TargetObject.Length; i++)
_PathExpression.TryGet(_TargetObject[i], false, out object _);
}
private void RunPipelineNull(IPipeline pipeline)
{
pipeline.Begin();

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

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#if BENCHMARK
@ -82,8 +82,6 @@ namespace PSRule.Benchmark
{
cmd.OnExecute(() =>
{
Console.WriteLine("Press ENTER to start.");
Console.ReadLine();
RunDebug();
return 0;
});
@ -95,6 +93,9 @@ namespace PSRule.Benchmark
var profile = new PSRule();
profile.Prepare();
Console.WriteLine("Press ENTER to start.");
Console.ReadLine();
ProfileBlock();
for (var i = 0; i < DebugIterations; i++)
profile.Invoke();
@ -134,6 +135,18 @@ namespace PSRule.Benchmark
ProfileBlock();
for (var i = 0; i < DebugIterations; i++)
profile.AssertHasFieldValue();
ProfileBlock();
for (var i = 0; i < DebugIterations; i++)
profile.PathTokenize();
ProfileBlock();
for (var i = 0; i < DebugIterations; i++)
profile.PathExpressionBuild();
ProfileBlock();
for (var i = 0; i < DebugIterations; i++)
profile.PathExpressionGet();
}
[DebuggerStepThrough]

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

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
@ -57,7 +57,7 @@ namespace PSRule.Commands
for (var i = 0; i < Field.Length && found < required; i++)
{
if (ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: targetObject, name: Field[i], caseSensitive: CaseSensitive, value: out _))
if (ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: targetObject, path: Field[i], caseSensitive: CaseSensitive, value: out object _))
{
RunspaceContext.CurrentThread.VerboseConditionMessage(condition: RuleLanguageNouns.Exists, message: PSRuleResources.ExistsTrue, args: Field[i]);
foundFields.Add(Field[i]);

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

@ -68,7 +68,7 @@ namespace PSRule.Commands
// Pass with any match, or (-Not) fail with any match
if (ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: targetObject, name: Field, caseSensitive: false, value: out object fieldValue))
if (ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: targetObject, path: Field, caseSensitive: false, value: out object fieldValue))
{
for (var i = 0; i < _Expressions.Length && !match; i++)
{

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

@ -69,7 +69,7 @@ namespace PSRule.Commands
// Pass with any match, or (-Not) fail with any match
if (ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: targetObject, name: Field, caseSensitive: false, value: out object fieldValue))
if (ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: targetObject, path: Field, caseSensitive: false, value: out object fieldValue))
{
for (var i = 0; (Value == null || i < Value.Length) && !match; i++)
{

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

@ -37,7 +37,7 @@ namespace PSRule
internal static bool Exists(IBindingContext bindingContext, object inputObject, string field, bool caseSensitive)
{
return ObjectHelper.GetField(bindingContext, inputObject, field, caseSensitive, out _);
return ObjectHelper.GetPath(bindingContext, inputObject, field, caseSensitive, out object _);
}
internal static bool Equal(object expectedValue, object actualValue, bool caseSensitive, bool convertExpected = false, bool convertActual = false)
@ -379,7 +379,8 @@ namespace PSRule
{
count = 0;
var expectedBase = GetBaseObject(expectedValue);
if (actualValue is IEnumerable items)
var actualBase = GetBaseObject(actualValue);
if (actualBase is IEnumerable items)
{
foreach (var item in items)
{
@ -388,7 +389,7 @@ namespace PSRule
}
return count > 0;
}
if (Equal(expectedBase, actualValue, caseSensitive))
else if (Equal(expectedBase, actualValue, caseSensitive))
{
count = 1;
return true;
@ -495,7 +496,7 @@ namespace PSRule
private static object GetBaseObject(object o)
{
return o is PSObject pso && pso.BaseObject != null ? pso.BaseObject : o;
return o is PSObject pso && pso.BaseObject != null && !(pso.BaseObject is PSCustomObject) ? pso.BaseObject : o;
}
private static PSRuleTargetInfo GetTargetInfo(object o)

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

@ -35,7 +35,7 @@ namespace PSRule
public static string ValueAsString(this PSObject o, string propertyName, bool caseSensitive)
{
return ObjectHelper.GetField(o, propertyName, caseSensitive, out object value) && value != null ? value.ToString() : null;
return ObjectHelper.GetPath(o, propertyName, caseSensitive, out object value) && value != null ? value.ToString() : null;
}
public static bool HasProperty(this PSObject o, string propertyName)

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

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using PSRule.Runtime;
using PSRule.Runtime.ObjectPath;
namespace PSRule.Definitions.Expressions
{
@ -22,28 +23,28 @@ namespace PSRule.Definitions.Expressions
internal sealed class ExpressionContext : IExpressionContext, IBindingContext
{
private readonly Dictionary<string, NameToken> _NameTokenCache;
private readonly Dictionary<string, PathExpression> _NameTokenCache;
private List<string> _Reason;
internal ExpressionContext(string languageScope)
{
LanguageScope = languageScope;
_NameTokenCache = new Dictionary<string, NameToken>();
_NameTokenCache = new Dictionary<string, PathExpression>();
}
public string LanguageScope { get; }
[DebuggerStepThrough]
void IBindingContext.CacheNameToken(string expression, NameToken nameToken)
void IBindingContext.CachePathExpression(string path, PathExpression expression)
{
_NameTokenCache[expression] = nameToken;
_NameTokenCache[path] = expression;
}
[DebuggerStepThrough]
bool IBindingContext.GetNameToken(string expression, out NameToken nameToken)
bool IBindingContext.GetPathExpression(string path, out PathExpression expression)
{
return _NameTokenCache.TryGetValue(expression, out nameToken);
return _NameTokenCache.TryGetValue(path, out expression);
}
internal void Debug(string message, params object[] args)

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

@ -541,7 +541,7 @@ namespace PSRule.Definitions.Expressions
if (TryPropertyArray(properties, SETOF, out Array expectedValue) && TryField(properties, out string field) && GetCaseSensitive(properties, out bool caseSensitive))
{
context.ExpressionTrace(SETOF, field, expectedValue);
if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out object actualValue))
if (!ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object actualValue))
return NotHasField(context, field);
if (!ExpressionHelpers.TryEnumerableLength(actualValue, out int count))
@ -567,7 +567,7 @@ namespace PSRule.Definitions.Expressions
GetCaseSensitive(properties, out bool caseSensitive) && GetUnique(properties, out bool unique))
{
context.ExpressionTrace(SUBSET, field, expectedValue);
if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out object actualValue))
if (!ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object actualValue))
return NotHasField(context, field);
if (!ExpressionHelpers.TryEnumerableLength(actualValue, out _))
@ -589,7 +589,7 @@ namespace PSRule.Definitions.Expressions
if (TryPropertyLong(properties, COUNT, out long? expectedValue) && TryField(properties, out string field))
{
context.ExpressionTrace(COUNT, field, expectedValue);
if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out object value))
if (!ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object value))
return NotHasField(context, field);
if (value == null)
@ -872,10 +872,10 @@ namespace PSRule.Definitions.Expressions
TryPropertyBoolOrDefault(properties, IGNORESCHEME, out bool ignoreScheme, false))
{
context.ExpressionTrace(HASSCHEMA, field, expectedValue);
if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out object actualValue))
if (!ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object actualValue))
return NotHasField(context, field);
if (!ObjectHelper.GetField(context, actualValue, PROPERTY_SCHEMA, caseSensitive: false, out object schemaValue))
if (!ObjectHelper.GetPath(context, actualValue, PROPERTY_SCHEMA, caseSensitive: false, out object schemaValue))
return NotHasField(context, PROPERTY_SCHEMA);
if (!ExpressionHelpers.TryString(schemaValue, out string actualSchema))
@ -1029,7 +1029,7 @@ namespace PSRule.Definitions.Expressions
if (!properties.TryGetString(FIELD, out string field))
return false;
if (ObjectHelper.GetField(context, o, field, caseSensitive: false, out object value))
if (ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object value))
operand = Operand.FromField(field, value);
return operand != null || NotHasField(context, field);
@ -1089,7 +1089,7 @@ namespace PSRule.Definitions.Expressions
if (!properties.TryGetString(FIELD, out string field))
return false;
return !ObjectHelper.GetField(context, o, field, caseSensitive: false, out _);
return !ObjectHelper.GetPath(context, o, field, caseSensitive: false, out object _);
}
private static bool TryOperand(ExpressionContext context, string name, object o, LanguageExpression.PropertyBag properties, out IOperand operand)

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

@ -16,6 +16,7 @@ using PSRule.Definitions.ModuleConfigs;
using PSRule.Definitions.Selectors;
using PSRule.Host;
using PSRule.Runtime;
using PSRule.Runtime.ObjectPath;
namespace PSRule.Pipeline
{
@ -32,7 +33,7 @@ namespace PSRule.Pipeline
// Configuration parameters
private readonly IList<ResourceRef> _Unresolved;
private readonly LanguageMode _LanguageMode;
private readonly Dictionary<string, NameToken> _NameTokenCache;
private readonly Dictionary<string, PathExpression> _PathExpressionCache;
private readonly List<ResourceIssue> _TrackedIssues;
// Objects kept for caching and disposal
@ -78,7 +79,7 @@ namespace PSRule.Pipeline
BindTargetType = bindTargetType;
BindField = bindField;
_LanguageMode = option.Execution.LanguageMode ?? ExecutionOption.Default.LanguageMode.Value;
_NameTokenCache = new Dictionary<string, NameToken>();
_PathExpressionCache = new Dictionary<string, PathExpression>();
LocalizedDataCache = new Dictionary<string, Hashtable>();
ExpressionCache = new Dictionary<string, object>();
ContentCache = new Dictionary<string, PSObject[]>();
@ -230,20 +231,14 @@ namespace PSRule.Pipeline
#region IBindingContext
public bool GetNameToken(string expression, out NameToken nameToken)
public bool GetPathExpression(string path, out PathExpression expression)
{
if (!_NameTokenCache.ContainsKey(expression))
{
nameToken = null;
return false;
}
nameToken = _NameTokenCache[expression];
return true;
return _PathExpressionCache.TryGetValue(path, out expression);
}
public void CacheNameToken(string expression, NameToken nameToken)
public void CachePathExpression(string path, PathExpression expression)
{
_NameTokenCache[expression] = nameToken;
_PathExpressionCache[path] = expression;
}
#endregion IBindingContext
@ -267,7 +262,7 @@ namespace PSRule.Pipeline
if (_Runspace != null)
_Runspace.Dispose();
_NameTokenCache.Clear();
_PathExpressionCache.Clear();
LocalizedDataCache.Clear();
ExpressionCache.Clear();
ContentCache.Clear();

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

@ -104,7 +104,7 @@ namespace PSRule.Pipeline
int score = int.MaxValue;
for (var i = 0; i < propertyNames.Length && score > propertyNames.Length; i++)
{
if (ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: targetObject, name: propertyNames[i], caseSensitive: caseSensitive, value: out object value))
if (ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: targetObject, path: propertyNames[i], caseSensitive: caseSensitive, value: out object value))
{
targetName = value.ToString();
score = i;

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

@ -193,7 +193,7 @@ namespace PSRule.Pipeline
public static IEnumerable<TargetObject> ReadObjectPath(TargetObject targetObject, VisitTargetObject source, string objectPath, bool caseSensitive)
{
if (!ObjectHelper.GetField(bindingContext: null, targetObject: targetObject.Value, name: objectPath, caseSensitive: caseSensitive, value: out object nestedObject))
if (!ObjectHelper.GetPath(bindingContext: null, targetObject: targetObject.Value, path: objectPath, caseSensitive: caseSensitive, value: out object nestedObject))
return EmptyArray;
var nestedType = nestedObject.GetType();

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

@ -226,7 +226,7 @@ namespace PSRule.Runtime
result = Pass();
for (var i = 0; field != null && i < field.Length; i++)
{
if (ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field[i], caseSensitive: caseSensitive, value: out _))
if (ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field[i], caseSensitive: caseSensitive, value: out object _))
{
if (result.Result)
result = Fail();
@ -251,7 +251,7 @@ namespace PSRule.Runtime
var missing = 0;
for (var i = 0; field != null && i < field.Length; i++)
{
if (!ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field[i], caseSensitive: caseSensitive, value: out _))
if (!ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field[i], caseSensitive: caseSensitive, value: out object _))
{
result.AddReason(ReasonStrings.NotHasField, field[i]);
missing++;
@ -271,7 +271,7 @@ namespace PSRule.Runtime
return result;
// Assert
if (!ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field, caseSensitive: false, value: out object fieldValue))
if (!ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field, caseSensitive: false, value: out object fieldValue))
return Fail(ReasonStrings.NotHasField, field);
else if (ExpressionHelpers.NullOrEmpty(fieldValue))
return Fail(ReasonStrings.NotHasFieldValue, field);
@ -292,7 +292,7 @@ namespace PSRule.Runtime
return result;
// Assert
if (!ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field, caseSensitive: false, value: out object fieldValue)
if (!ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field, caseSensitive: false, value: out object fieldValue)
|| ExpressionHelpers.Equal(defaultValue, fieldValue, caseSensitive: false))
return Pass();
@ -309,7 +309,7 @@ namespace PSRule.Runtime
GuardNullOrEmptyParam(field, nameof(field), out result))
return result;
ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field, caseSensitive: false, value: out object fieldValue);
ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field, caseSensitive: false, value: out object fieldValue);
return fieldValue == null ? Pass() : Fail(ReasonStrings.NotNull, field);
}
@ -338,7 +338,7 @@ namespace PSRule.Runtime
return result;
// Assert
if (ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field, caseSensitive: false, value: out object fieldValue) && !ExpressionHelpers.NullOrEmpty(fieldValue))
if (ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field, caseSensitive: false, value: out object fieldValue) && !ExpressionHelpers.NullOrEmpty(fieldValue))
return Fail(ReasonStrings.NullOrEmpty, field);
return Pass();
@ -724,7 +724,7 @@ namespace PSRule.Runtime
GuardNullParam(values, nameof(values), out result))
return result;
if (!ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field, caseSensitive: caseSensitive, value: out object fieldValue))
if (!ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field, caseSensitive: caseSensitive, value: out object fieldValue))
return Pass();
for (var i = 0; values != null && i < values.Length; i++)
@ -820,7 +820,7 @@ namespace PSRule.Runtime
GuardNullOrEmptyParam(field, nameof(field), out result))
return result;
if (!ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field, caseSensitive: caseSensitive, value: out object fieldValue))
if (!ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field, caseSensitive: caseSensitive, value: out object fieldValue))
return Pass();
if (GuardString(fieldValue, out string value, out result))
@ -1008,7 +1008,7 @@ namespace PSRule.Runtime
private bool GuardField(PSObject inputObject, string field, bool caseSensitive, out object fieldValue, out AssertResult result)
{
result = null;
if (ObjectHelper.GetField(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, name: field, caseSensitive: caseSensitive, value: out fieldValue))
if (ObjectHelper.GetPath(bindingContext: PipelineContext.CurrentThread, targetObject: inputObject, path: field, caseSensitive: caseSensitive, value: out fieldValue))
return false;
result = Fail(ReasonStrings.NotHasField, field);

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

@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Runtime.ObjectPath;
namespace PSRule.Runtime
{
internal interface IBindingContext
{
bool GetNameToken(string expression, out NameToken nameToken);
bool GetPathExpression(string path, out PathExpression expression);
void CacheNameToken(string expression, NameToken nameToken);
void CachePathExpression(string path, PathExpression expression);
}
}

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

@ -3,13 +3,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Dynamic;
using System.Management.Automation;
using System.Reflection;
using System.Threading;
using PSRule.Runtime.ObjectPath;
namespace PSRule.Runtime
{
@ -18,253 +14,23 @@ namespace PSRule.Runtime
/// </summary>
internal static class ObjectHelper
{
[DebuggerStepThrough]
private sealed class NameTokenStream
{
private const char Separator = '.';
private const char Quoted = '\'';
private const char OpenIndex = '[';
private const char CloseIndex = ']';
private readonly string Name;
private readonly int Last;
private bool inQuote;
private bool inIndex;
public int Position = -1;
public char Current;
public NameTokenStream(string name)
{
Name = name;
Last = Name.Length - 1;
}
/// <summary>
/// Find the start of the sequence.
/// </summary>
/// <returns>Return true when more characters follow.</returns>
public bool Next()
{
if (Position < Last)
{
Position++;
if (Name[Position] == Separator && Position > 0)
{
Position++;
}
else if (Name[Position] == Quoted)
{
Position++;
inQuote = true;
}
Current = Name[Position];
return true;
}
return false;
}
/// <summary>
/// Find the end of the sequence and return the index.
/// </summary>
/// <returns>The index of the sequence end.</returns>
public int IndexOf(out NameTokenType tokenType)
{
tokenType = Position == 0 && Current == Separator ? NameTokenType.Self : NameTokenType.Field;
if (tokenType == NameTokenType.Self)
return Position;
while (Position < Last)
{
Position++;
Current = Name[Position];
if (inQuote)
{
if (Current == Quoted)
{
inQuote = false;
return Position - 1;
}
}
else if (Current == Separator)
{
return Position - 1;
}
else if (inIndex)
{
if (Current == CloseIndex)
{
tokenType = NameTokenType.Index;
inIndex = false;
return Position - 1;
}
}
else if (Current == OpenIndex)
{
// Next token will be an Index
inIndex = true;
// Return end of token
return Position - 1;
}
}
return Position;
}
public NameToken Get()
{
var token = new NameToken();
NameToken result = token;
while (Next())
{
var start = Position;
if (start > 0)
{
token.Next = new NameToken();
token = token.Next;
}
// Jump to the next separator or end
var end = IndexOf(out NameTokenType tokenType);
token.Type = tokenType;
if (tokenType == NameTokenType.Field)
{
token.Name = Name.Substring(start, end - start + 1);
}
else if (tokenType == NameTokenType.Index)
{
token.Index = int.Parse(Name.Substring(start, end - start + 1), Thread.CurrentThread.CurrentCulture);
}
}
return result;
}
}
private sealed class DynamicPropertyBinder : GetMemberBinder
{
internal DynamicPropertyBinder(string name, bool ignoreCase)
: base(name, ignoreCase) { }
public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
{
return null;
}
}
public static bool GetField(PSObject targetObject, string name, bool caseSensitive, out object value)
public static bool GetPath(PSObject targetObject, string path, bool caseSensitive, out object value)
{
return targetObject.BaseObject is IDictionary dictionary ?
TryDictionary(dictionary, name, caseSensitive, out value) :
TryPropertyValue(targetObject, name, caseSensitive, out value);
TryDictionary(dictionary, path, caseSensitive, out value) :
TryPropertyValue(targetObject, path, caseSensitive, out value);
}
public static bool GetField(IBindingContext bindingContext, object targetObject, string name, bool caseSensitive, out object value)
public static bool GetPath(IBindingContext bindingContext, object targetObject, string path, bool caseSensitive, out object value)
{
return GetField(
targetObject,
token: GetNameToken(bindingContext, name),
caseSensitive: caseSensitive,
value: out value
);
var expression = GetPathExpression(bindingContext, path);
return expression.TryGet(targetObject, caseSensitive, out value);
}
private static bool GetField(object targetObject, NameToken token, bool caseSensitive, out object value)
public static bool GetPath(IBindingContext bindingContext, object targetObject, string path, bool caseSensitive, out object[] value)
{
value = null;
var baseObject = GetBaseObject(targetObject);
if (baseObject == null)
return false;
var baseType = baseObject.GetType();
object field = null;
bool foundField = false;
// Handle this object
if (token.Type == NameTokenType.Self)
{
field = baseObject;
foundField = true;
}
// Handle dictionaries and hashtables
else if (token.Type == NameTokenType.Field && baseObject is IDictionary dictionary)
{
if (TryDictionary(dictionary, token.Name, caseSensitive, out field))
foundField = true;
}
// Handle PSObjects
else if (token.Type == NameTokenType.Field && targetObject is PSObject psObject)
{
if (TryPropertyValue(psObject, token.Name, caseSensitive, out field))
foundField = true;
}
// Handle DynamicObjects
else if (token.Type == NameTokenType.Field && targetObject is DynamicObject dynamicObject)
{
if (TryPropertyValue(dynamicObject, token.Name, caseSensitive, out field))
foundField = true;
}
// Handle all other CLR types
else if (token.Type == NameTokenType.Field)
{
if (TryPropertyValue(targetObject, token.Name, baseType, caseSensitive, out field) ||
TryFieldValue(targetObject, token.Name, baseType, caseSensitive, out field) ||
TryIndexerProperty(targetObject, token.Name, baseType, out field))
foundField = true;
}
// Handle array indexes
else if (token.Type == NameTokenType.Index && baseType.IsArray && baseObject is Array array && token.Index < array.Length)
{
field = array.GetValue(token.Index);
foundField = true;
}
// Handle IList
else if (token.Type == NameTokenType.Index && baseObject is IList list && token.Index < list.Count)
{
field = list[token.Index];
foundField = true;
}
// Handle IEnumerable
else if (token.Type == NameTokenType.Index && baseObject is IEnumerable enumerable && TryEnumerableIndex(enumerable, token.Index, out object element))
{
field = element;
foundField = true;
}
else if (token.Type == NameTokenType.Index)
{
if (TryIndexerProperty(targetObject, token.Index, baseType, out field))
foundField = true;
}
if (foundField)
{
if (token.Next == null)
{
value = field;
return true;
}
else
{
return GetField(targetObject: field, token: token.Next, caseSensitive: caseSensitive, value: out value);
}
}
return false;
}
private static bool TryEnumerableIndex(IEnumerable o, int index, out object value)
{
value = null;
var e = o.GetEnumerator();
for (var i = 0; e.MoveNext(); i++)
{
if (i == index)
{
value = e.Current;
return true;
}
}
return false;
var expression = GetPathExpression(bindingContext, path);
return expression.TryGet(targetObject, caseSensitive, out value);
}
private static bool TryDictionary(IDictionary dictionary, string key, bool caseSensitive, out object value)
@ -282,18 +48,6 @@ namespace PSRule.Runtime
return false;
}
private static bool TryPropertyValue(object targetObject, string propertyName, Type baseType, bool caseSensitive, out object value)
{
value = null;
var bindingFlags = caseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase;
var propertyInfo = baseType.GetProperty(propertyName, bindingAttr: bindingFlags | BindingFlags.Instance | BindingFlags.Public);
if (propertyInfo == null)
return false;
value = propertyInfo.GetValue(targetObject);
return true;
}
private static bool TryPropertyValue(PSObject targetObject, string propertyName, bool caseSensitive, out object value)
{
value = null;
@ -308,110 +62,20 @@ namespace PSRule.Runtime
return true;
}
private static bool TryPropertyValue(DynamicObject targetObject, string propertyName, bool caseSensitive, out object value)
{
if (!targetObject.TryGetMember(new DynamicPropertyBinder(propertyName, !caseSensitive), out value))
return false;
return true;
}
private static bool TryFieldValue(object targetObject, string fieldName, Type baseType, bool caseSensitive, out object value)
{
value = null;
var bindingFlags = caseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase;
var fieldInfo = baseType.GetField(fieldName, bindingAttr: bindingFlags | BindingFlags.Instance | BindingFlags.Public);
if (fieldInfo == null)
return false;
value = fieldInfo.GetValue(targetObject);
return true;
}
private static bool TryIndexerProperty(object targetObject, object index, Type baseType, out object value)
{
value = null;
var properties = baseType.GetProperties();
foreach (PropertyInfo pi in GetIndexerProperties(baseType))
{
var p = pi.GetIndexParameters();
if (p.Length > 0)
{
try
{
var converter = GetConverter(p[0].ParameterType);
var p1 = converter(index);
value = pi.GetValue(targetObject, new object[] { p1 });
return true;
}
catch
{
// Discard converter exceptions
}
}
}
return false;
}
private static Converter<object, object> GetConverter(Type targetType)
{
var convertAtribute = targetType.GetCustomAttribute<TypeConverterAttribute>();
if (convertAtribute != null)
{
var converterType = Type.GetType(convertAtribute.ConverterTypeName);
if (converterType.IsSubclassOf(typeof(TypeConverter)))
{
var converter = (TypeConverter)Activator.CreateInstance(converterType);
return s => converter.ConvertFrom(s);
}
else if (converterType.IsSubclassOf(typeof(PSTypeConverter)))
{
var converter = (PSTypeConverter)Activator.CreateInstance(converterType);
return s => converter.ConvertFrom(s, targetType, Thread.CurrentThread.CurrentCulture, true);
}
}
return s => Convert.ChangeType(s, targetType);
}
private static IEnumerable<PropertyInfo> GetIndexerProperties(Type baseType)
{
var attribute = baseType.GetCustomAttribute<DefaultMemberAttribute>();
if (attribute != null)
{
var property = baseType.GetProperty(attribute.MemberName);
yield return property;
}
else
{
var properties = baseType.GetProperties();
foreach (PropertyInfo pi in properties)
{
var p = pi.GetIndexParameters();
if (p.Length > 0)
yield return pi;
}
}
}
/// <summary>
/// Get a token for the specified name either by creating or reading from cache.
/// </summary>
[DebuggerStepThrough]
private static NameToken GetNameToken(IBindingContext bindingContext, string name)
private static PathExpression GetPathExpression(IBindingContext bindingContext, string path)
{
// Try to load nameToken from cache
if (bindingContext == null || !bindingContext.GetNameToken(expression: name, nameToken: out NameToken nameToken))
if (bindingContext == null || !bindingContext.GetPathExpression(path, out PathExpression expression))
{
nameToken = new NameTokenStream(name).Get();
expression = PathExpression.Create(path);
if (bindingContext != null)
bindingContext.CacheNameToken(expression: name, nameToken: nameToken);
bindingContext.CachePathExpression(path, expression);
}
return nameToken;
}
private static object GetBaseObject(object value)
{
return value is PSObject ovalue && ovalue.BaseObject != null && !(ovalue.BaseObject is PSCustomObject) ? ovalue.BaseObject : value;
return expression;
}
}
}

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

@ -0,0 +1,129 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace PSRule.Runtime.ObjectPath
{
/// <summary>
/// An expression function that returns one or more values when successful.
/// </summary>
internal delegate bool PathExpressionFn(IPathExpressionContext context, object input, out IEnumerable<object> value);
/// <summary>
/// A function for filter objects that simply returns true or false.
/// </summary>
internal delegate bool PathExpressionFilterFn(IPathExpressionContext context, object input);
/// <summary>
/// A context ojbect used using evaluating a path expression.
/// </summary>
internal interface IPathExpressionContext
{
object Input { get; }
bool CaseSensitive { get; }
}
/// <summary>
/// The default context object used using evaluating a path expression.
/// </summary>
internal sealed class PathExpressionContext : IPathExpressionContext
{
public PathExpressionContext(object input, bool caseSensitive)
{
Input = input;
CaseSensitive = caseSensitive;
}
/// <summary>
/// The original root object passed into the expression.
/// </summary>
public object Input { get; }
/// <summary>
/// Determines if member name matching is case-sensitive.
/// </summary>
public bool CaseSensitive { get; }
}
/// <summary>
/// A path expression using JSONPath inspired syntax.
/// </summary>
[DebuggerDisplay("{Path}")]
internal sealed class PathExpression
{
private readonly PathExpressionFn _Expression;
[DebuggerStepThrough]
private PathExpression(string path, PathExpressionFn expression, bool isArray)
{
Path = path;
IsArray = isArray;
_Expression = expression;
}
/// <summary>
/// The path expression.
/// </summary>
public string Path { get; }
/// <summary>
/// Specifies if the result is an array.
/// </summary>
public bool IsArray { get; }
/// <summary>
/// Create the expression from the specified path.
/// </summary>
public static PathExpression Create(string path)
{
var tokens = PathTokenizer.Get(path);
var builder = new PathExpressionBuilder();
return new PathExpression(path, builder.Build(tokens), builder.IsArray);
}
/// <summary>
/// Use the expression to return an array of results.
/// </summary>
[DebuggerStepThrough]
public bool TryGet(object o, bool caseSensitive, out object[] value)
{
value = null;
if (!TryGet(o, caseSensitive, out IEnumerable<object> result))
return false;
value = result.ToArray();
return true;
}
/// <summary>
/// Use the expression to return a single or an array of results.
/// </summary>
[DebuggerStepThrough]
public bool TryGet(object o, bool caseSensitive, out object value)
{
value = null;
if (!TryGet(o, caseSensitive, out object[] result))
return false;
value = IsArray ? result : result[0];
return true;
}
/// <summary>
/// Use the path to selector one or more values from the object.
/// </summary>
/// <param name="o">The object to navigate the path for.</param>
/// <param name="caseSensitive">Determines if member name matching is case-sensitive.</param>
/// <param name="value">The values selected from the object.</param>
/// <returns>Returns true when the path exists within the object. Returns false if the path does not exist.</returns>
private bool TryGet(object o, bool caseSensitive, out IEnumerable<object> value)
{
var context = new PathExpressionContext(o, caseSensitive);
return _Expression.Invoke(context, o, out value);
}
}
}

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

@ -0,0 +1,651 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Management.Automation;
using System.Reflection;
using System.Threading;
using Newtonsoft.Json.Linq;
namespace PSRule.Runtime.ObjectPath
{
/// <summary>
/// A helper class to build an expression tree from path tokens.
/// </summary>
internal sealed class PathExpressionBuilder
{
private sealed class DynamicPropertyBinder : GetMemberBinder
{
internal DynamicPropertyBinder(string name, bool ignoreCase)
: base(name, ignoreCase) { }
public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
{
return null;
}
}
/// <summary>
/// Determines if the output should be an array.
/// </summary>
public bool IsArray { get; private set; }
/// <summary>
/// Build a delegate function to evaluate the object path.
/// </summary>
public PathExpressionFn Build(IPathToken[] tokens)
{
return BuildSelector(new TokenReader(tokens));
}
private void UseArray()
{
IsArray = true;
}
private PathExpressionFn BuildSelector(ITokenReader reader)
{
if (!reader.Peak(out IPathToken token) || token.Type == PathTokenType.EndFilter || token.Type == PathTokenType.EndGroup)
return Return;
reader.Next(out token);
switch (token.Type)
{
case PathTokenType.DotSelector:
return DotSelector(reader, token.As<string>(), token.Option);
case PathTokenType.RootRef:
return RootRef(reader);
case PathTokenType.CurrentRef:
return CurrentRef(reader);
case PathTokenType.IndexWildSelector:
return IndexWildSelector(reader);
case PathTokenType.IndexSelector:
return IndexSelector(reader, token.As<int>());
case PathTokenType.StartFilter:
return FilterSelector(reader);
case PathTokenType.ArraySliceSelector:
return ArraySliceSelector(reader, token.As<int?[]>());
case PathTokenType.Boolean:
case PathTokenType.Integer:
case PathTokenType.String:
return Literal(token.Arg);
default:
return Return;
}
}
private PathExpressionFn FilterSelector(ITokenReader reader)
{
UseArray();
var filter = BuildExpression(reader, PathTokenType.EndFilter);
var next = BuildSelector(reader);
return (IPathExpressionContext context, object input, out IEnumerable<object> value) =>
{
var result = new List<object>();
var success = 0;
foreach (var i in GetAll(input))
{
if (!filter(context, i))
continue;
if (!next(context, i, out IEnumerable<object> items))
continue;
success++;
result.AddRange(items);
}
value = success > 0 ? result.ToArray() : null;
return success > 0;
};
}
private PathExpressionFn IndexSelector(ITokenReader reader, int index)
{
var next = BuildSelector(reader);
return (IPathExpressionContext context, object input, out IEnumerable<object> value) =>
{
value = null;
if (!TryGetIndex(input, index, out object item))
return false;
return next(context, item, out value);
};
}
private PathExpressionFn IndexWildSelector(ITokenReader reader)
{
UseArray();
var next = BuildSelector(reader);
return (IPathExpressionContext context, object input, out IEnumerable<object> value) =>
{
var result = new List<object>();
var success = 0;
foreach (var i in GetAll(input))
{
if (!next(context, i, out IEnumerable<object> items))
continue;
success++;
result.AddRange(items);
}
value = success > 0 ? result.ToArray() : null;
return success > 0;
};
}
private PathExpressionFn ArraySliceSelector(ITokenReader reader, int?[] arg)
{
UseArray();
var next = BuildSelector(reader);
var step = arg[2].GetValueOrDefault(1);
var start = arg[0].GetValueOrDefault(step >= 0 ? 0 : -1);
var end = arg[1];
return (IPathExpressionContext context, object input, out IEnumerable<object> value) =>
{
var result = new List<object>();
var currentIndex = start;
while ((!end.HasValue || (step > 0 && currentIndex < end) || (step < 0 && currentIndex > end)) && TryGetIndex(input, currentIndex, out object slice))
{
currentIndex += step;
if (!next(context, slice, out IEnumerable<object> items))
continue;
result.AddRange(items);
}
value = result.ToArray();
return true;
};
}
private PathExpressionFn DotSelector(ITokenReader reader, string memberName, PathTokenOption option)
{
var caseSensitiveFlag = option == PathTokenOption.CaseSensitive;
var next = BuildSelector(reader);
return (IPathExpressionContext context, object input, out IEnumerable<object> value) =>
{
value = null;
var caseSensitive = context.CaseSensitive != caseSensitiveFlag;
if (!TryGetField(input, memberName, caseSensitive, out object item))
return false;
return next(context, item, out value);
};
}
private PathExpressionFn CurrentRef(ITokenReader reader)
{
var next = BuildSelector(reader);
return (IPathExpressionContext context, object input, out IEnumerable<object> value) => next(context, input, out value);
}
private PathExpressionFn RootRef(ITokenReader reader)
{
var next = BuildSelector(reader);
return (IPathExpressionContext context, object input, out IEnumerable<object> value) => next(context, context.Input, out value);
}
private PathExpressionFilterFn BuildExpression(ITokenReader reader, PathTokenType stop)
{
var result = new Stack<PathExpressionFilterFn>(4);
while (reader.Next(out IPathToken token) && token.Type != stop)
{
if (token.Type == PathTokenType.LogicalOperator && token.As<FilterOperator>() == FilterOperator.Or)
continue;
if (token.Type == PathTokenType.LogicalOperator && token.As<FilterOperator>() == FilterOperator.And)
{
var left = result.Pop();
var right = BuildBasicExpression(reader);
result.Push((IPathExpressionContext context, object input) =>
{
// All expression must return true
return left(context, input) && right(context, input);
});
continue;
}
result.Push(BuildBasicExpression(reader));
}
var expressions = result.ToArray();
return (IPathExpressionContext context, object input) =>
{
// Any one expression returns true
for (var i = 0; i < expressions.Length; i++)
if (expressions[i](context, input))
return true;
return false;
};
}
private PathExpressionFilterFn BuildBasicExpression(ITokenReader reader)
{
var token = reader.Current;
switch (token.Type)
{
case PathTokenType.NotOperator:
if (reader.Consume(PathTokenType.StartGroup))
return NotCondition(BuildExpression(reader, PathTokenType.EndGroup));
return NotCondition(ExistCondition(BuildSelector(reader)));
case PathTokenType.StartGroup:
return BuildExpression(reader, PathTokenType.EndGroup);
default:
return BuildRelationExpression(reader);
}
}
private PathExpressionFilterFn BuildRelationExpression(ITokenReader reader)
{
var left = BuildSelector(reader);
if (reader.Current.Type == PathTokenType.ComparisonOperator)
{
var op = reader.Current;
var right = BuildSelector(reader);
return BinaryCondition(left, right, op.As<FilterOperator>());
}
return ExistCondition(left);
}
private static PathExpressionFilterFn ExistCondition(PathExpressionFn next)
{
return (IPathExpressionContext context, object input) => next(context, input, out _);
}
private static PathExpressionFilterFn NotCondition(PathExpressionFilterFn next)
{
return (IPathExpressionContext context, object input) => !next(context, input);
}
private static PathExpressionFilterFn BinaryCondition(PathExpressionFn left, PathExpressionFn right, FilterOperator op)
{
return (IPathExpressionContext context, object input) =>
{
if (!left(context, input, out IEnumerable<object> leftValue) || !right(context, input, out IEnumerable<object> rightValue))
return false;
var operand1 = leftValue.FirstOrDefault();
var operand2 = rightValue.FirstOrDefault();
// Get the specific operator
switch (op)
{
case FilterOperator.Equal:
return ExpressionHelpers.Equal(operand1, operand2, context.CaseSensitive);
case FilterOperator.NotEqual:
return !ExpressionHelpers.Equal(operand1, operand2, context.CaseSensitive);
case FilterOperator.Less:
return ExpressionHelpers.CompareNumeric(operand1, operand2, convert: false, compare: out int compare, value: out _) && compare < 0;
case FilterOperator.LessOrEqual:
return ExpressionHelpers.CompareNumeric(operand1, operand2, convert: false, compare: out compare, value: out _) && compare <= 0;
case FilterOperator.Greater:
return ExpressionHelpers.CompareNumeric(operand1, operand2, convert: false, compare: out compare, value: out _) && compare > 0;
case FilterOperator.GreaterOrEqual:
return ExpressionHelpers.CompareNumeric(operand1, operand2, convert: false, compare: out compare, value: out _) && compare >= 0;
case FilterOperator.RegEx:
return ExpressionHelpers.Match(operand1, operand2, context.CaseSensitive);
}
return false;
};
}
private static bool Return(IPathExpressionContext context, object input, out IEnumerable<object> value)
{
// Unwrap primitive types
if (input is JValue jValue && (jValue.Type == JTokenType.String || jValue.Type == JTokenType.Integer || jValue.Type == JTokenType.Boolean))
input = jValue.Value;
value = new object[] { input };
return true;
}
private static PathExpressionFn Literal(object arg)
{
var result = new object[] { arg };
return (IPathExpressionContext context, object input, out IEnumerable<object> value) =>
{
value = result;
return true;
};
}
#region Enumerators
private static IEnumerable<object> GetAll(object o)
{
var baseObject = GetBaseObject(o);
if (baseObject is IEnumerable)
return GetAllIndex(baseObject);
return GetAllField(baseObject);
}
private static IEnumerable<object> GetAllIndex(object o)
{
if (o is IEnumerable enumerable)
foreach (var i in enumerable)
yield return i;
}
private static IEnumerable<object> GetAllField(object o)
{
var baseObject = GetBaseObject(o);
if (baseObject == null)
yield break;
// Handle dictionaries and hashtables
if (baseObject is IDictionary dictionary)
{
foreach (var value in dictionary.Values)
yield return value;
}
// Handle PSObjects
else if (o is PSObject psObject)
{
foreach (var property in psObject.Properties)
yield return property.Value;
}
// Handle DynamicObjects
else if (o is DynamicObject dynamicObject)
{
}
// Handle all other CLR types
else
{
var baseType = baseObject.GetType();
var properties = baseType.GetProperties(bindingAttr: BindingFlags.Instance | BindingFlags.Public);
for (var i = 0; properties != null && i < properties.Length; i++)
yield return properties[i].GetValue(baseObject);
var fields = baseType.GetFields(bindingAttr: BindingFlags.Instance | BindingFlags.Public);
for (var i = 0; fields != null && i < fields.Length; i++)
yield return fields[i].GetValue(baseObject);
}
}
#endregion Enumerators
#region Lookup
private static bool TryGetField(object o, string fieldName, bool caseSensitive, out object value)
{
value = null;
var baseObject = GetBaseObject(o);
if (baseObject == null || (baseObject is JValue jValue && jValue.Type == JTokenType.Null))
return false;
// Handle dictionaries and hashtables
if (baseObject is IDictionary dictionary)
{
return TryDictionary(dictionary, fieldName, caseSensitive, out value);
}
// Handle JToken
else if (baseObject is JObject jObject)
{
return TryPropertyValue(jObject, fieldName, caseSensitive, out value);
}
// Handle PSObjects
else if (o is PSObject psObject)
{
return TryPropertyValue(psObject, fieldName, caseSensitive, out value);
}
// Handle DynamicObjects
else if (o is DynamicObject dynamicObject)
{
return TryPropertyValue(dynamicObject, fieldName, caseSensitive, out value);
}
// Handle all other CLR types
var baseType = baseObject.GetType();
return TryPropertyValue(o, fieldName, baseType, caseSensitive, out value) ||
TryFieldValue(o, fieldName, baseType, caseSensitive, out value) ||
TryIndexerProperty(o, fieldName, baseType, out value);
}
private static bool TryGetIndex(object o, int index, out object value)
{
value = null;
var baseObject = GetBaseObject(o);
if (baseObject == null)
return false;
// Handle array indexes
if (baseObject is Array array && index < array.Length)
{
if (index < 0)
index = array.Length + index;
if (index < 0 || index >= array.Length)
return false;
value = array.GetValue(index);
return true;
}
// Handle IList
else if (baseObject is IList list && index < list.Count)
{
if (index < 0)
index = list.Count + index;
if (index < 0 || index >= list.Count)
return false;
value = list[index];
return true;
}
// Handle IEnumerable
else if (baseObject is IEnumerable enumerable)
{
return TryEnumerableIndex(enumerable, index, out value);
}
// Handle all other CLR types
return TryIndexerProperty(o, index, baseObject.GetType(), out value);
}
#endregion Lookup
private static bool TryEnumerableIndex(IEnumerable o, int index, out object value)
{
value = null;
var e = o.GetEnumerator();
if (index < 0)
{
var items = new List<object>();
while (e.MoveNext())
items.Add(e.Current);
index = items.Count + index;
if (index < 0 || index >= items.Count)
return false;
value = items[index];
return true;
}
for (var i = 0; e.MoveNext(); i++)
{
if (i == index)
{
value = e.Current;
return true;
}
}
return false;
}
private static bool TryDictionary(IDictionary dictionary, string key, bool caseSensitive, out object value)
{
value = null;
var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
foreach (var k in dictionary.Keys)
{
if (comparer.Equals(key, k))
{
value = dictionary[k];
return true;
}
}
return false;
}
private static bool TryPropertyValue(object targetObject, string propertyName, Type baseType, bool caseSensitive, out object value)
{
value = null;
var bindingFlags = caseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase;
var propertyInfo = baseType.GetProperty(propertyName, bindingAttr: bindingFlags | BindingFlags.Instance | BindingFlags.Public);
if (propertyInfo == null)
return false;
value = propertyInfo.GetValue(targetObject);
return true;
}
private static bool TryPropertyValue(PSObject targetObject, string propertyName, bool caseSensitive, out object value)
{
value = null;
var p = targetObject.Properties[propertyName];
if (p == null)
return false;
if (caseSensitive && !StringComparer.Ordinal.Equals(p.Name, propertyName))
return false;
value = p.Value;
return true;
}
private static bool TryPropertyValue(JObject targetObject, string propertyName, bool caseSensitive, out object value)
{
value = null;
if (!targetObject.TryGetValue(propertyName, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase, out JToken result))
return false;
value = GetTokenValue(result);
return true;
}
private static bool TryPropertyValue(DynamicObject targetObject, string propertyName, bool caseSensitive, out object value)
{
return targetObject.TryGetMember(new DynamicPropertyBinder(propertyName, !caseSensitive), out value);
}
private static object GetTokenValue(JToken o)
{
if (o == null || o.Type == JTokenType.Null)
return null;
if (o.Type == JTokenType.String)
return o.Value<string>();
if (o.Type == JTokenType.Boolean)
return o.Value<bool>();
if (o.Type == JTokenType.Integer)
return o.Value<long>();
return o;
}
private static bool TryFieldValue(object targetObject, string fieldName, Type baseType, bool caseSensitive, out object value)
{
value = null;
var bindingFlags = caseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase;
var fieldInfo = baseType.GetField(fieldName, bindingAttr: bindingFlags | BindingFlags.Instance | BindingFlags.Public);
if (fieldInfo == null)
return false;
value = fieldInfo.GetValue(targetObject);
return true;
}
private static bool TryIndexerProperty(object targetObject, object index, Type baseType, out object value)
{
value = null;
var properties = baseType.GetProperties();
foreach (var property in GetIndexerProperties(baseType))
{
var parameters = property.GetIndexParameters();
if (parameters.Length > 0)
{
try
{
var converter = GetConverter(parameters[0].ParameterType);
var p1 = converter(index);
value = property.GetValue(targetObject, new object[] { p1 });
return true;
}
catch
{
// Discard converter exceptions
}
}
}
return false;
}
private static Converter<object, object> GetConverter(Type targetType)
{
var convertAtribute = targetType.GetCustomAttribute<TypeConverterAttribute>();
if (convertAtribute != null)
{
var converterType = Type.GetType(convertAtribute.ConverterTypeName);
if (converterType.IsSubclassOf(typeof(TypeConverter)))
{
var converter = (TypeConverter)Activator.CreateInstance(converterType);
return s => converter.ConvertFrom(s);
}
else if (converterType.IsSubclassOf(typeof(PSTypeConverter)))
{
var converter = (PSTypeConverter)Activator.CreateInstance(converterType);
return s => converter.ConvertFrom(s, targetType, Thread.CurrentThread.CurrentCulture, true);
}
}
return s => Convert.ChangeType(s, targetType);
}
private static IEnumerable<PropertyInfo> GetIndexerProperties(Type baseType)
{
var attribute = baseType.GetCustomAttribute<DefaultMemberAttribute>();
if (attribute != null)
{
var property = baseType.GetProperty(attribute.MemberName);
yield return property;
}
else
{
var properties = baseType.GetProperties();
foreach (var property in properties)
{
var parameters = property.GetIndexParameters();
if (parameters.Length > 0)
yield return property;
}
}
}
private static object GetBaseObject(object value)
{
return value is PSObject ovalue && ovalue.BaseObject != null && !(ovalue.BaseObject is PSCustomObject) ? ovalue.BaseObject : value;
}
}
}

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

@ -0,0 +1,680 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace PSRule.Runtime.ObjectPath
{
/// <summary>
/// A helper to tokenize an object path expression.
/// </summary>
internal static class PathTokenizer
{
private sealed class TokenStream : ITokenWriter
{
private readonly List<IPathToken> _Items;
public TokenStream()
{
_Items = new List<IPathToken>();
}
public IPathToken Last
{
get
{
return _Items.Count > 0 ? _Items[_Items.Count - 1] : null;
}
}
public void Add(IPathToken token)
{
_Items.Add(token);
}
public IPathToken[] ToArray()
{
return _Items.ToArray();
}
}
[DebuggerDisplay("Position = {Position}")]
private sealed class PathStream
{
private const char ROOTREF = '$';
private const char CURRENTREF = '@';
private const char DOT = '.';
private const char QUOTED_SINGLE = '\'';
private const char QUOTED_DOUBLE = '"';
private const char INDEX_OPEN = '[';
private const char INDEX_CLOSE = ']';
private const char ANY = '*';
private const char DASH = '-';
private const char QUERY = '?';
private const char GROUP_OPEN = '(';
private const char GROUP_CLOSE = ')';
private const char EQUALS = '=';
private const char NOT = '!';
private const char LESSTHAN = '<';
private const char GREATERTHAN = '>';
private const char TILDA = '~';
private const char NULL = '\0';
private const char COLON = ':';
private const char COMMA = ',';
private const char OR = '|';
private const char AND = '&';
private const char UNDERSCORE = '_';
private const char PLUS = '+';
private readonly string Path;
private readonly int Last;
public PathStream(string path)
{
Path = path;
Last = Path.Length - 1;
}
public bool EOF(int position)
{
return position > Last;
}
public char Current(int pos)
{
return pos > Last ? NULL : Path[pos];
}
public bool Current(int pos, char c)
{
return pos <= Last && Path[pos] == c;
}
/// <summary>
/// Find the start of the sequence.
/// </summary>
/// <returns>Return true when more characters follow.</returns>
public void Next(ref int position)
{
if (position <= Last)
position++;
}
/// <summary>
/// Capture a token for $ and @.
/// </summary>
internal bool TryConsumeRef(ref int position, ITokenWriter tokens)
{
if ((Current(position) == ROOTREF && !IsMemberName(position)) || (position == 0 && Current(position) == DOT))
{
tokens.Add(PathToken.RootRef);
Next(ref position);
return true;
}
if (Current(position) == CURRENTREF)
{
tokens.Add(position == 0 ? PathToken.RootRef : PathToken.CurrentRef);
Next(ref position);
return true;
}
return false;
}
internal bool TryConsumeChild(ref int position, ITokenWriter tokens)
{
return TryConsumeDotWildSelector(ref position, tokens) ||
TryConsumeDotSelector(ref position, tokens) ||
TryConsumeIndexSelector(ref position, tokens) ||
TryConsumeDescendantSelector(ref position, tokens);
}
/// <summary>
/// Capture a token for "[?(@.enabled==true)]".
/// </summary>
internal bool TryConsumeFilter(ref int position, ITokenWriter tokens)
{
if (Current(position) != INDEX_OPEN || position + 5 >= Last || Path[position + 1] != QUERY)
return false;
var groupOpen = Path[position + 2] == GROUP_OPEN;
var pos = groupOpen ? position + 3 : position + 2;
tokens.Add(new PathToken(PathTokenType.StartFilter));
while (!EOF(pos) && Current(pos) != INDEX_CLOSE)
{
if (!TryConsumeBooleanExpression(ref pos, tokens))
Next(ref pos);
pos = SkipPadding(pos);
}
if (Current(pos) == INDEX_CLOSE)
Next(ref pos);
tokens.Add(new PathToken(PathTokenType.EndFilter));
position = SkipPadding(pos);
return true;
}
private bool TryConsumeBooleanExpression(ref int position, ITokenWriter tokens)
{
if (!TryConsumeRef(ref position, tokens) && !TryConsumeDotSelector(ref position, tokens) && !TryConsumeNot(ref position, tokens))
return false;
if (tokens.Last.Type == PathTokenType.NotOperator)
TryConsumeRef(ref position, tokens);
TryConsumeDotSelector(ref position, tokens);
TryConsumeComparisonOperator(ref position, tokens);
TryConsumePrimitive(ref position, tokens);
TryConsumeLogicalOperator(ref position, tokens);
return true;
}
private bool TryConsumeNot(ref int position, ITokenWriter tokens)
{
if (Current(position) != NOT)
return false;
tokens.Add(new PathToken(PathTokenType.NotOperator));
position += 1;
return true;
}
private bool TryConsumePrimitive(ref int position, ITokenWriter tokens)
{
return TryConsumeNumberLiteral(ref position, tokens) ||
TryConsumeStringLiteral(ref position, tokens) ||
TryConsumeBooleanLiteral(ref position, tokens);
}
private bool TryConsumeStringLiteral(ref int position, ITokenWriter tokens)
{
if (!IsQuoted(Current(position)))
return false;
if (!UntilQuote(ref position, out string value))
return false;
tokens.Add(new PathToken(PathTokenType.String, value));
position = SkipPadding(position);
return true;
}
private bool TryConsumeNumberLiteral(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (!TryInteger(pos, out int value))
return false;
tokens.Add(new PathToken(PathTokenType.Integer, value));
position = pos + value.ToString().Length;
return true;
}
private bool TryConsumeBooleanLiteral(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (!TryBoolean(pos, out bool value))
return false;
tokens.Add(new PathToken(PathTokenType.Boolean, value));
position = pos + value.ToString().Length;
return true;
}
private void TryConsumeComparisonOperator(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (!IsComparisonOperator(Current(pos)))
return;
var op = FilterOperator.None;
var c1 = ConsumeChar(ref pos);
var c2 = Current(pos);
if (c1 == EQUALS && c2 == EQUALS)
op = FilterOperator.Equal;
if (c1 == NOT && c2 == EQUALS)
op = FilterOperator.NotEqual;
if (c1 == LESSTHAN && c2 == EQUALS)
op = FilterOperator.LessOrEqual;
if (c1 == LESSTHAN && !IsComparisonOperator(c2))
op = FilterOperator.Less;
if (c1 == GREATERTHAN && c2 == EQUALS)
op = FilterOperator.GreaterOrEqual;
if (c1 == GREATERTHAN && !IsComparisonOperator(c2))
op = FilterOperator.Greater;
if (c1 == TILDA && c2 == EQUALS)
op = FilterOperator.RegEx;
if (op != FilterOperator.None)
{
position = SkipPadding(op == FilterOperator.Less || op == FilterOperator.Greater ? pos : pos + 1);
tokens.Add(new PathToken(PathTokenType.ComparisonOperator, op));
}
}
private void TryConsumeLogicalOperator(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (!IsLogicalOperator(Current(pos)))
return;
IPathToken token = null;
var c1 = ConsumeChar(ref pos);
var c2 = ConsumeChar(ref pos);
if (c1 == OR && c2 == OR)
token = new PathToken(PathTokenType.LogicalOperator, FilterOperator.Or);
if (c1 == AND && c2 == AND)
token = new PathToken(PathTokenType.LogicalOperator, FilterOperator.And);
if (token != null)
{
position = SkipPadding(pos);
tokens.Add(token);
}
}
private char ConsumeChar(ref int position)
{
return position > Last ? NULL : Path[position++];
}
/// <summary>
/// Check if current is a property operator.
/// </summary>
/// <remarks>
/// "." or "+" but not ".."
/// </remarks>
private bool IsDotSelector(int position)
{
return (Current(position, DOT) && !Current(position + 1, DOT)) || Current(position, PLUS);
}
private bool IsDotWildcardSelector(int position)
{
return Current(position, DOT) && Current(position + 1, ANY);
}
private bool IsDescendantSelector(int position)
{
return Current(position, DOT) && Current(position + 1, DOT);
}
private static bool IsComparisonOperator(char c)
{
return c == EQUALS || c == NOT || c == LESSTHAN || c == GREATERTHAN || c == TILDA;
}
private static bool IsLogicalOperator(char c)
{
return c == OR || c == AND;
}
private static bool IsMemberNameCharacter(char c)
{
return char.IsLetterOrDigit(c) || c == UNDERSCORE;
}
private static bool IsQuoted(char c)
{
return c == QUOTED_SINGLE || c == QUOTED_DOUBLE;
}
private bool TryConsumeDotSelector(ref int position, ITokenWriter tokens)
{
if (!IsDotSelector(position) && !Current(position, QUOTED_SINGLE) && !Current(position, QUOTED_DOUBLE) && !IsMemberName(position))
return false;
var pos = IsDotSelector(position) ? position + 1 : position;
var option = Current(position, PLUS) ? PathTokenOption.CaseSensitive : PathTokenOption.None;
var field = CaptureMemberName(ref pos);
if (string.IsNullOrEmpty(field))
return false;
tokens.Add(new PathToken(PathTokenType.DotSelector, field, option));
position = pos;
return true;
}
private bool TryConsumeDotWildSelector(ref int position, ITokenWriter tokens)
{
if (!IsDotWildcardSelector(position))
return false;
tokens.Add(new PathToken(PathTokenType.DotWildSelector));
position += 2;
return true;
}
private bool TryConsumeDescendantSelector(ref int position, ITokenWriter tokens)
{
if (!IsDescendantSelector(position))
return false;
var pos = position + 2;
var field = CaptureMemberName(ref pos);
if (string.IsNullOrEmpty(field))
return false;
tokens.Add(new PathToken(PathTokenType.DescendantSelector, field));
position = pos;
return true;
}
private bool TryConsumeIndexSelector(ref int position, ITokenWriter tokens)
{
if (!(Current(position) == INDEX_OPEN && Current(position + 1) != QUERY && position + 1 < Last))
return false;
// Move past "["
position++;
return TryConsumeArraySliceSelector(ref position, tokens) ||
TryConsumeUnionSelector(ref position, tokens) ||
TryConsumeNumericIndex(ref position, tokens) ||
TryConsumeIndexWildSelector(ref position, tokens) ||
TryConsumeStringIndex(ref position, tokens);
}
/// <summary>
/// Capture a token for: [::1]
/// </summary>
private bool TryConsumeArraySliceSelector(ref int position, ITokenWriter tokens)
{
if (!AnyUntilIndexClose(position, COLON))
return false;
var pos = SkipPadding(position);
var slice = new int?[] { null, null, null };
for (var i = 0; i <= 2 && pos <= Last && Path[pos] != INDEX_CLOSE; i++)
{
if (WhileNumeric(pos, out int end) && end > pos)
{
slice[i] = int.Parse(Substring(pos, end));
pos = Current(end, COLON) ? end + 1 : end;
}
else
{
pos++;
}
}
position = ++pos;
tokens.Add(new PathToken(PathTokenType.ArraySliceSelector, slice));
return true;
}
/// <summary>
/// Capture a token for: [,]
/// </summary>
private bool TryConsumeUnionSelector(ref int position, ITokenWriter tokens)
{
if (!AnyUntilIndexClose(position, COMMA))
return false;
return TryConsumeUnionQuotedMemberSelector(ref position, tokens) ||
TryConsumeUnionIndexSelector(ref position, tokens);
}
private bool TryConsumeUnionIndexSelector(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (pos + 2 >= Last || !WhileNumeric(pos, out int end) || end == pos)
return false;
var members = new List<int>();
while (pos <= Last && Path[pos] != INDEX_CLOSE)
{
pos = SkipPadding(pos);
if (!WhileNumeric(pos, out end) || !int.TryParse(Substring(pos, end), out int member))
break;
members.Add(member);
pos = SkipPadding(end);
if (Current(pos, COMMA))
pos++;
}
position = pos + 1;
tokens.Add(new PathToken(PathTokenType.UnionIndexSelector, members.ToArray()));
return true;
}
private bool TryConsumeUnionQuotedMemberSelector(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (pos + 3 >= Last || !IsQuoted(Path[pos]))
return false;
var members = new List<string>();
while (pos <= Last && Path[pos] != INDEX_CLOSE)
{
pos = SkipPadding(pos);
var member = CaptureMemberName(ref pos);
if (string.IsNullOrEmpty(member))
break;
members.Add(member);
pos = SkipPadding(pos);
if (Current(pos, COMMA))
pos++;
}
position = pos + 1;
tokens.Add(new PathToken(PathTokenType.UnionQuotedMemberSelector, members.ToArray()));
return true;
}
/// <summary>
/// Capture a token for "['store']".
/// </summary>
private bool TryConsumeStringIndex(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (pos + 3 >= Last || !IsQuoted(Path[pos]))
return false;
var field = CaptureMemberName(ref pos);
if (string.IsNullOrEmpty(field))
return false;
tokens.Add(new PathToken(PathTokenType.DotSelector, field));
position = pos + 1;
return true;
}
/// <summary>
/// Capture a token for "[*]".
/// </summary>
private bool TryConsumeIndexWildSelector(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (pos >= Last || Path[pos] != ANY)
return false;
pos = SkipPadding(pos + 1);
if (pos > Last || Path[pos] != INDEX_CLOSE)
return false;
pos++;
tokens.Add(new PathToken(PathTokenType.IndexWildSelector));
position = pos;
return true;
}
/// <summary>
/// Capture a token for "[0]".
/// </summary>
private bool TryConsumeNumericIndex(ref int position, ITokenWriter tokens)
{
var pos = SkipPadding(position);
if (!WhileNumeric(pos, out int end) || !int.TryParse(Substring(pos, end), out int index))
return false;
pos = SkipPadding(end);
if (pos > Last || Path[pos] != INDEX_CLOSE)
return false;
tokens.Add(new PathToken(PathTokenType.IndexSelector, index));
position = pos;
return true;
}
private string CaptureMemberName(ref int position)
{
return UntilQuote(ref position, out string value) || WhileMemberName(ref position, out value) ? value : null;
}
private bool TryBoolean(int position, out bool value)
{
value = default;
if (IsSequence(position, bool.FalseString))
{
value = false;
return true;
}
if (IsSequence(position, bool.TrueString))
{
value = true;
return true;
}
return false;
}
private bool TryInteger(int position, out int value)
{
value = default;
return WhileNumeric(position, out int end) && int.TryParse(Substring(position, end), out value);
}
private bool IsSequence(int position, string sequence)
{
return position + sequence.Length <= Last && string.Compare(Path, position, sequence, 0, sequence.Length, StringComparison.OrdinalIgnoreCase) == 0;
}
private bool IsMemberName(int position)
{
var p = Current(position);
var p1 = Current(position + 1);
return IsMemberNameCharacter(p) || (p == ROOTREF && IsMemberNameCharacter(p1));
}
/// <summary>
/// Skip whitespace.
/// </summary>
private int SkipPadding(int pos)
{
while (pos < Last && char.IsWhiteSpace(Path[pos]))
pos++;
return pos;
}
private string Substring(int pos, int end)
{
return pos == end ? null : Path.Substring(pos, end - pos);
}
/// <summary>
/// Continue while the character is a member name.
/// </summary>
private bool WhileMemberName(ref int position, out string value)
{
value = null;
if (position >= Last)
return false;
var end = Path[position] == ROOTREF ? position + 1 : position;
while (end <= Last && IsMemberNameCharacter(Path[end]))
end++;
if (position == end)
return false;
value = Substring(position, end);
position = end;
return true;
}
/// <summary>
/// Continue while the character is numeric.
/// </summary>
private bool WhileNumeric(int position, out int end)
{
end = position;
if (position >= Last)
return false;
var i = position;
if (i <= Last && Path[i] == DASH)
i++;
while (i <= Last && (char.IsDigit(Path[i]) || (Path[i] == DASH && i + 1 < Last && char.IsDigit(Path[i + 1]))))
end = ++i;
return end > position;
}
/// <summary>
/// Find the end of the quote (').
/// </summary>
private bool UntilQuote(ref int position, out string value)
{
value = null;
if (position >= Last || !IsQuoted(Path[position]))
return false;
var endQuote = Path[position];
var pos = position + 1;
var end = pos;
while (end <= Last && Path[end] != endQuote)
end++;
if (pos == end)
return false;
value = Substring(pos, end);
position = end + 1;
return true;
}
private bool AnyUntilIndexClose(int position, char c)
{
for (var i = position; i <= Last && Path[i] != INDEX_CLOSE; i++)
if (Path[i] == c)
return true;
return false;
}
}
/// <summary>
/// Get path tokens for a specific object path expression.
/// </summary>
/// <param name="path">The object path expression.</param>
/// <returns>One or more path tokens.</returns>
internal static IPathToken[] Get(string path)
{
var stream = new PathStream(path);
var tokens = new TokenStream();
var position = 0;
while (!stream.EOF(position))
{
if (!(stream.TryConsumeRef(ref position, tokens) ||
stream.TryConsumeChild(ref position, tokens) ||
stream.TryConsumeFilter(ref position, tokens)))
{
stream.Next(ref position);
}
}
return tokens.ToArray();
}
}
}

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

@ -0,0 +1,195 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Diagnostics;
namespace PSRule.Runtime.ObjectPath
{
internal enum PathTokenType
{
None = 0,
/// <summary>
/// Token: $
/// </summary>
RootRef,
/// <summary>
/// Token: @
/// </summary>
CurrentRef,
/// <summary>
/// Token: .Name
/// </summary>
DotSelector,
/// <summary>
/// Token: [index]
/// </summary>
IndexSelector,
/// <summary>
/// Token: [*]
/// </summary>
IndexWildSelector,
StartFilter,
ComparisonOperator,
Boolean,
EndFilter,
String,
Integer,
LogicalOperator,
StartGroup,
EndGroup,
/// <summary>
/// Token: !
/// </summary>
NotOperator,
/// <summary>
/// Token: ..
/// </summary>
DescendantSelector,
/// <summary>
/// Token: .*
/// </summary>
DotWildSelector,
ArraySliceSelector,
UnionIndexSelector,
UnionQuotedMemberSelector,
}
internal enum PathTokenOption
{
None = 0,
CaseSensitive
}
internal enum FilterOperator
{
None = 0,
// Comparison
Equal,
NotEqual,
LessOrEqual,
Less,
GreaterOrEqual,
Greater,
RegEx,
// Logical
Or,
And,
}
internal interface IPathToken
{
PathTokenType Type { get; }
PathTokenOption Option { get; }
object Arg { get; }
T As<T>();
}
[DebuggerDisplay("Type = {Type}, Arg = {Arg}")]
internal sealed class PathToken : IPathToken
{
public readonly static PathToken RootRef = new PathToken(PathTokenType.RootRef);
public readonly static PathToken CurrentRef = new PathToken(PathTokenType.CurrentRef);
public PathTokenType Type { get; }
public PathTokenOption Option { get; }
public PathToken(PathTokenType type)
{
Type = type;
}
public PathToken(PathTokenType type, object arg, PathTokenOption option = PathTokenOption.None)
{
Type = type;
Arg = arg;
Option = option;
}
public object Arg { get; }
public T As<T>()
{
return Arg is T result ? result : default;
}
}
internal interface ITokenWriter
{
IPathToken Last { get; }
void Add(IPathToken token);
}
internal interface ITokenReader
{
IPathToken Current { get; }
bool Next(out IPathToken token);
bool Consume(PathTokenType type);
bool Peak(out IPathToken token);
}
internal sealed class TokenReader : ITokenReader
{
private readonly IPathToken[] _Tokens;
private readonly int _Last;
private int _Index;
public TokenReader(IPathToken[] tokens)
{
_Tokens = tokens;
_Last = tokens.Length - 1;
_Index = -1;
}
public IPathToken Current { get; private set; }
public bool Consume(PathTokenType type)
{
return Peak(out IPathToken token) && token.Type == type && Next();
}
public bool Next(out IPathToken token)
{
token = null;
if (!Next())
return false;
token = Current;
return true;
}
private bool Next()
{
Current = _Index < _Last ? _Tokens[++_Index] : null;
return Current != null;
}
public bool Peak(out IPathToken token)
{
token = _Index < _Last ? _Tokens[_Index + 1] : null;
return token != null;
}
}
}

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

@ -162,7 +162,7 @@ namespace PSRule.Runtime
var result = new List<PSObject>();
for (var i = 0; i < content.Length; i++)
{
if (ObjectHelper.GetField(content[i], field, false, out object value) && value != null)
if (ObjectHelper.GetPath(content[i], field, false, out object value) && value != null)
{
if (value is IEnumerable evalue)
{
@ -189,6 +189,19 @@ namespace PSRule.Runtime
return content[0];
}
/// <summary>
/// Evalute an object path expression and returns the resulting objects.
/// </summary>
public object[] GetPath(object sourceObject, string path)
{
return (!ObjectHelper.GetPath(
bindingContext: GetContext()?.Pipeline,
targetObject: sourceObject,
path: path,
caseSensitive: false,
out object[] value)) ? Array.Empty<object>() : value;
}
/// <summary>
/// Imports source objects into the pipeline for processing.
/// </summary>

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

@ -194,6 +194,15 @@ Rule 'VariableContextVariable' {
# Get first content object
$first = $PSRule.GetContentFirstOrDefault((Get-Item -Path (Join-Path -Path $PSScriptRoot -ChildPath 'ObjectFromFile.json')));
$Assert.HasField($first, 'Spec.Properties');
# Get object path
$firstObject = $PSRule.GetContentFirstOrDefault((Get-Item -Path (Join-Path -Path $PSScriptRoot -ChildPath 'ObjectFromFile3.json')));
$categories = $PSRule.GetPath($firstObject, 'resources[?@.type==''diagnosticSettings''].properties.logs[?@.enabled==true].category');
$Assert.Subset($categories, '.', @(
'audit'
'debug'
'firewall'
));
}
# Synopsis: Test $Rule automatic variables

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

@ -102,3 +102,16 @@ spec:
condition:
field: 'Name'
equals: 'TestValue'
---
# Synopsis: Test a complex object path
apiVersion: github.com/microsoft/PSRule/v1
kind: Rule
metadata:
name: YamlObjectPath
spec:
condition:
field: '$.resources[?@.type==''diagnosticSettings''].properties.logs[?@.enabled==true].category'
subset:
- 'audit'
- 'firewall'

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

@ -0,0 +1,96 @@
[
{
"name": "TestObject1",
"type": "cluster",
"resources": [
{
"name": "security",
"type": "diagnosticSettings",
"properties": {
"logs": [
{
"category": "audit",
"enabled": true
},
{
"category": "firewall",
"enabled": false
}
]
}
},
{
"name": "monitoring",
"type": "diagnosticSettings",
"properties": {
"logs": [
{
"category": "debug",
"enabled": true
},
{
"category": "firewall",
"enabled": true
}
]
}
}
]
},
{
"name": "TestObject2",
"type": "cluster",
"resources": [
{
"name": "security",
"type": "diagnosticSettings",
"properties": {
"logs": [
{
"category": "audit",
"enabled": true
},
{
"category": "firewall",
"enabled": false
}
]
}
},
{
"name": "monitoring",
"type": "diagnosticSettings",
"properties": {
"logs": [
{
"category": "debug",
"enabled": true
}
]
}
}
]
},
{
"name": "TestObject3",
"type": "cluster",
"resources": [
{
"name": "security",
"type": "DiagnosticSettings",
"properties": {
"logs": [
{
"category": "audit",
"enabled": true
},
{
"category": "firewall",
"enabled": true
}
]
}
}
]
}
]

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

@ -1,10 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using System.Collections.Generic;
using PSRule.Definitions;
using Xunit;
using ObjectHelper = PSRule.Runtime.ObjectHelper;
namespace PSRule
{
@ -15,16 +16,16 @@ namespace PSRule
{
var testObject = GetTestObject();
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: "Name", caseSensitive: true, value: out object actual1);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: "Value.Value1", caseSensitive: false, value: out object actual2);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: "Metadata.'app.kubernetes.io/name'", caseSensitive: false, value: out object actual3);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: "Value2[1]", caseSensitive: false, value: out object actual4);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: ".", caseSensitive: true, value: out object actual5);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: ".Value2[1]", caseSensitive: false, value: out object actual6);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: ".Value3[1]", caseSensitive: false, value: out object actual7);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: ".Value4[0]", caseSensitive: false, value: out object actual8);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: ".Value5.name", caseSensitive: false, value: out object actual9);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: ".Value5[2]", caseSensitive: false, value: out object actual10);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: "Name", caseSensitive: true, value: out object actual1);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: "Value.Value1", caseSensitive: false, value: out object actual2);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: "Metadata.'app.kubernetes.io/name'", caseSensitive: false, value: out object actual3);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: "Value2[1]", caseSensitive: false, value: out object actual4);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: ".", caseSensitive: true, value: out object actual5);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: ".Value2[1]", caseSensitive: false, value: out object actual6);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: ".Value3[1]", caseSensitive: false, value: out object actual7);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: ".Value4[0]", caseSensitive: false, value: out object actual8);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: ".Value5.name", caseSensitive: false, value: out object actual9);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: ".Value5[2]", caseSensitive: false, value: out object actual10);
Assert.Equal(expected: testObject.Name, actual: actual1);
Assert.Equal(expected: testObject.Value.Value1, actual: actual2);
@ -48,13 +49,21 @@ namespace PSRule
};
var testObject = ResourceTags.FromHashtable(hashtable);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: "Name", caseSensitive: true, value: out object actual1);
Runtime.ObjectHelper.GetField(bindingContext: null, targetObject: testObject, name: "Value", caseSensitive: true, value: out object actual2);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: "Name", caseSensitive: true, value: out object actual1);
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, path: "Value", caseSensitive: true, value: out object actual2);
Assert.Equal(expected: testObject["Name"], actual: actual1);
Assert.Equal(expected: testObject["Value"], actual: actual2);
}
[Fact]
public void JsonPath()
{
var testObject = GetTestObject();
ObjectHelper.GetPath(bindingContext: null, targetObject: testObject, "$.Value2[*]", caseSensitive: true, value: out object actual1);
}
private static TestObject1 GetTestObject()
{
var value5 = new TestObject3();

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

@ -58,6 +58,9 @@
<None Update="ObjectFromFile.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ObjectFromFile3.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ObjectFromFile3.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

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

@ -0,0 +1,222 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.IO;
using Newtonsoft.Json;
using PSRule.Runtime.ObjectPath;
using Xunit;
namespace PSRule
{
/// <summary>
/// Tests for a JSONPath expression.
/// </summary>
public sealed class PathExpressionTests
{
[Fact]
public void Basic()
{
var testObject = GetJsonContent();
var expression = PathExpression.Create("$[*]");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out object actual1));
var actualArray = actual1 as object[];
Assert.NotNull(actualArray);
Assert.Equal(2, actualArray.Length);
expression = PathExpression.Create("$[-1].TargetName");
Assert.False(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out object actual2));
Assert.False(actual2 is object[]);
Assert.NotNull(actual2);
Assert.Equal("TestObject2", actual2);
expression = PathExpression.Create("$[-3].TargetName");
Assert.False(expression.IsArray);
Assert.False(expression.TryGet(testObject, false, out object actual3));
Assert.Null(actual3);
expression = PathExpression.Create("$[*].Spec.Properties.array[*].id");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out object[] actual4));
Assert.NotNull(actual4);
Assert.Equal(4, actual4.Length);
Assert.Equal("1", actual4[0]);
Assert.Equal("2", actual4[1]);
Assert.Equal("1", actual4[2]);
Assert.Equal("2", actual4[3]);
}
[Fact]
public void WithMemberCase()
{
var testObject = GetJsonContent();
var expression = PathExpression.Create("$[*].spec.Properties.array[*].id");
Assert.True(expression.TryGet(testObject, false, out object[] actual));
Assert.NotNull(actual);
Assert.Equal(4, actual.Length);
expression = PathExpression.Create("$[*].spec.Properties.array[*].id");
Assert.False(expression.TryGet(testObject, true, out actual));
Assert.Null(actual);
expression = PathExpression.Create("$[0].targetName");
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
expression = PathExpression.Create("$[0]+targetName");
Assert.False(expression.TryGet(testObject, false, out actual));
Assert.Null(actual);
expression = PathExpression.Create("$[0].targetName");
Assert.False(expression.TryGet(testObject, true, out actual));
Assert.Null(actual);
expression = PathExpression.Create("$[0]+targetName");
Assert.True(expression.TryGet(testObject, true, out actual));
Assert.NotNull(actual);
}
[Fact]
public void WithFilter()
{
var testObject = GetJsonContent();
var expression = PathExpression.Create("$[*].Spec.Properties.array[?(@.id=='1')].id");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out object[] actual));
Assert.NotNull(actual);
Assert.Equal(2, actual.Length);
Assert.Equal("1", actual[0]);
Assert.Equal("1", actual[1]);
expression = PathExpression.Create("$[*].Spec.Properties.array[?(@.id==1)].id");
Assert.True(expression.IsArray);
Assert.False(expression.TryGet(testObject, false, out object[] _));
expression = PathExpression.Create("$[?@.TargetName == 'TestObject1'].TargetName");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
Assert.Single(actual);
Assert.Equal("TestObject1", actual[0]);
}
[Fact]
public void WithOrFilter()
{
var testObject = GetJsonContent();
var expression = PathExpression.Create("$[*].Spec.Properties.array[?(@.id=='1' || @.id=='2')].id");
Assert.True(expression.TryGet(testObject, false, out object[] actual));
Assert.NotNull(actual);
Assert.Equal(4, actual.Length);
Assert.Equal("1", actual[0]);
Assert.Equal("2", actual[1]);
Assert.Equal("1", actual[2]);
Assert.Equal("2", actual[3]);
}
[Fact]
public void WithAndFilter()
{
var testObject = GetJsonContent();
var expression = PathExpression.Create("$[*].Spec.Properties.array[?(@.id=='1' && @.id=='2')].id");
Assert.False(expression.TryGet(testObject, false, out object[] actual));
Assert.Null(actual);
expression = PathExpression.Create("$[*].Spec.Properties.array[?(@.id=='1' && @.id=='1')].id");
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
Assert.Equal(2, actual.Length);
Assert.Equal("1", actual[0]);
Assert.Equal("1", actual[1]);
}
[Fact]
public void WithExistsFilter()
{
var testObject = GetJsonContent();
var expression = PathExpression.Create("$[?@.Spec.Properties.Kind].TargetName");
Assert.True(expression.TryGet(testObject, false, out object[] actual));
Assert.NotNull(actual);
Assert.Equal(2, actual.Length);
Assert.Equal("TestObject1", actual[0]);
Assert.Equal("TestObject2", actual[1]);
expression = PathExpression.Create("$[?@.Spec.Properties.Value1].TargetName");
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
Assert.Single(actual);
Assert.Equal("TestObject1", actual[0]);
expression = PathExpression.Create("$[?@.Spec.Properties.Value2].TargetName");
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
Assert.Single(actual);
Assert.Equal("TestObject2", actual[0]);
}
[Fact]
public void WithSlice()
{
var testObject = GetJsonContent();
var expression = PathExpression.Create("$[0].Spec.Properties.array[:1].id");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, true, out object[] actual));
Assert.NotNull(actual);
Assert.Single(actual);
Assert.Equal("1", actual[0]);
expression = PathExpression.Create("$[0].spec.properties.array[:1].id");
Assert.True(expression.IsArray);
Assert.False(expression.TryGet(testObject, true, out actual));
Assert.Null(actual);
expression = PathExpression.Create("$[0].spec.properties.array2[::]");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
Assert.Equal("1", actual[0]);
Assert.Equal("2", actual[1]);
Assert.Equal("3", actual[2]);
expression = PathExpression.Create("$[0].spec.properties.array2[::-1]");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
Assert.Equal("3", actual[0]);
Assert.Equal("2", actual[1]);
Assert.Equal("1", actual[2]);
expression = PathExpression.Create("$[0].spec.properties.array2[:1:-1]");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
Assert.Empty(actual);
expression = PathExpression.Create("$[0].spec.properties.array2[2:1:-1]");
Assert.True(expression.IsArray);
Assert.True(expression.TryGet(testObject, false, out actual));
Assert.NotNull(actual);
Assert.Single(actual);
Assert.Equal("3", actual[0]);
}
#region Helper methods
private object GetJsonContent()
{
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ObjectFromFile.json");
return JsonConvert.DeserializeObject<object>(File.ReadAllText(path));
}
#endregion Helper methods
}
}

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

@ -0,0 +1,705 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Runtime.ObjectPath;
using Xunit;
namespace PSRule
{
/// <summary>
/// Tests for JSONPath tokenenizer.
/// </summary>
public sealed class PathTokenizerTests
{
[Fact]
public void Get()
{
var token = PathTokenizer.Get("$[*].Properties.logs[?(@.enabled==true)].category");
Assert.Equal(11, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.IndexWildSelector, token[1].Type);
Assert.Equal(PathTokenType.DotSelector, token[2].Type);
Assert.Equal("Properties", token[2].As<string>());
Assert.Equal(PathTokenType.DotSelector, token[3].Type);
Assert.Equal("logs", token[3].As<string>());
Assert.Equal(PathTokenType.StartFilter, token[4].Type);
Assert.Equal(PathTokenType.CurrentRef, token[5].Type);
Assert.Equal(PathTokenType.DotSelector, token[6].Type);
Assert.Equal("enabled", token[6].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, token[7].Type);
Assert.Equal(FilterOperator.Equal, token[7].As<FilterOperator>());
Assert.Equal(PathTokenType.Boolean, token[8].Type);
Assert.True(token[8].As<bool>());
Assert.Equal(PathTokenType.EndFilter, token[9].Type);
Assert.Equal(PathTokenType.DotSelector, token[10].Type);
Assert.Equal("category", token[10].As<string>());
}
/// <summary>
/// Check tokenizer against simple test cases.
/// </summary>
[Fact]
public void SimpleTestCases()
{
var path = new string[]
{
"store",
".",
"@",
"$.",
"'store.property'",
"$[10]",
"$[*]",
"$['store.property']",
"$[\"store.property\"]",
"\"store.property\"",
};
// store
var token = PathTokenizer.Get(path[0]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("store", token[0].As<string>());
// .
token = PathTokenizer.Get(path[1]);
Assert.Single(token);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
// @
token = PathTokenizer.Get(path[2]);
Assert.Single(token);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
// $.
token = PathTokenizer.Get(path[3]);
Assert.Single(token);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
// 'store.property'
token = PathTokenizer.Get(path[4]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("store.property", token[0].As<string>());
// $[10]
token = PathTokenizer.Get(path[5]);
Assert.Equal(2, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.IndexSelector, token[1].Type);
Assert.Equal(10, token[1].As<int>());
// $[*]
token = PathTokenizer.Get(path[6]);
Assert.Equal(2, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.IndexWildSelector, token[1].Type);
// $['store.property']
token = PathTokenizer.Get(path[7]);
Assert.Equal(2, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("store.property", token[1].As<string>());
// $["store.property"]
token = PathTokenizer.Get(path[8]);
Assert.Equal(2, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("store.property", token[1].As<string>());
// "store.property"
token = PathTokenizer.Get(path[9]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("store.property", token[0].As<string>());
}
[Fact]
public void PathTests()
{
var path = new string[]
{
"$['store'].book[0].author",
};
// $['store'].book[0].author
var token = PathTokenizer.Get(path[0]);
Assert.Equal(5, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("store", token[1].As<string>());
Assert.Equal(PathTokenType.DotSelector, token[2].Type);
Assert.Equal("book", token[2].As<string>());
Assert.Equal(PathTokenType.IndexSelector, token[3].Type);
Assert.Equal(0, token[3].As<int>());
Assert.Equal(PathTokenType.DotSelector, token[4].Type);
Assert.Equal("author", token[4].As<string>());
}
[Fact]
public void MemberNameSchema()
{
var path = new string[]
{
"$schema"
};
// $schema
var token = PathTokenizer.Get(path[0]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("$schema", token[0].As<string>());
}
[Fact]
public void MemberNameWithUnderscore()
{
var path = new string[]
{
"member_name",
};
// member_name
var token = PathTokenizer.Get(path[0]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("member_name", token[0].As<string>());
}
[Fact]
public void MemberNameWithOption()
{
var path = new string[]
{
"$.name",
"$+name"
};
// $.name
var token = PathTokenizer.Get(path[0]);
Assert.Equal(2, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal(PathTokenOption.None, token[1].Option);
Assert.Equal("name", token[1].As<string>());
// $+name
token = PathTokenizer.Get(path[1]);
Assert.Equal(2, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal(PathTokenOption.CaseSensitive, token[1].Option);
Assert.Equal("name", token[1].As<string>());
}
[Fact]
public void MemberNameQuoted()
{
var path = new string[]
{
"'store.property'",
"\"store.property\"",
"['store.property']",
"[\"store.property\"]",
};
// 'store.property'
var token = PathTokenizer.Get(path[0]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("store.property", token[0].As<string>());
// "store.property"
token = PathTokenizer.Get(path[1]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("store.property", token[0].As<string>());
// ['store.property']
token = PathTokenizer.Get(path[2]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("store.property", token[0].As<string>());
// ["store.property"]
token = PathTokenizer.Get(path[3]);
Assert.Single(token);
Assert.Equal(PathTokenType.DotSelector, token[0].Type);
Assert.Equal("store.property", token[0].As<string>());
}
[Fact]
public void FilterBoolean()
{
var path = new string[]
{
"$[?(@.enabled==true)]",
"$[?@.enabled==false]",
};
// $[?(@.enabled==true)]
var actual = PathTokenizer.Get(path[0]);
Assert.Equal(7, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[2].Type);
Assert.Equal(PathTokenType.DotSelector, actual[3].Type);
Assert.Equal("enabled", actual[3].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, actual[4].Type);
Assert.Equal(FilterOperator.Equal, actual[4].As<FilterOperator>());
Assert.Equal(PathTokenType.Boolean, actual[5].Type);
Assert.True(actual[5].As<bool>());
Assert.Equal(PathTokenType.EndFilter, actual[6].Type);
// $[?(@.enabled==false)]
actual = PathTokenizer.Get(path[1]);
Assert.Equal(7, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[2].Type);
Assert.Equal(PathTokenType.DotSelector, actual[3].Type);
Assert.Equal("enabled", actual[3].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, actual[4].Type);
Assert.Equal(FilterOperator.Equal, actual[4].As<FilterOperator>());
Assert.Equal(PathTokenType.Boolean, actual[5].Type);
Assert.False(actual[5].As<bool>());
Assert.Equal(PathTokenType.EndFilter, actual[6].Type);
}
[Fact]
public void FilterInteger()
{
var path = new string[]
{
"$[?(@.price<10)]",
"$[?(@.price < 10)]",
};
// $[?(@.price<10)]
var actual = PathTokenizer.Get(path[0]);
Assert.Equal(7, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[2].Type);
Assert.Equal(PathTokenType.DotSelector, actual[3].Type);
Assert.Equal("price", actual[3].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, actual[4].Type);
Assert.Equal(FilterOperator.Less, actual[4].As<FilterOperator>());
Assert.Equal(PathTokenType.Integer, actual[5].Type);
Assert.Equal(10, actual[5].As<int>());
Assert.Equal(PathTokenType.EndFilter, actual[6].Type);
// $[?(@.price < 10)]
actual = PathTokenizer.Get(path[1]);
Assert.Equal(7, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[2].Type);
Assert.Equal(PathTokenType.DotSelector, actual[3].Type);
Assert.Equal("price", actual[3].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, actual[4].Type);
Assert.Equal(FilterOperator.Less, actual[4].As<FilterOperator>());
Assert.Equal(PathTokenType.Integer, actual[5].Type);
Assert.Equal(10, actual[5].As<int>());
Assert.Equal(PathTokenType.EndFilter, actual[6].Type);
}
[Fact]
public void FilterString()
{
var path = new string[]
{
"$[?(@.id=='1')]",
"$[?(@.id == \"1\")]",
};
// $[?(@.id=='1')]
var actual = PathTokenizer.Get(path[0]);
//Assert.Equal(7, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[2].Type);
Assert.Equal(PathTokenType.DotSelector, actual[3].Type);
Assert.Equal("id", actual[3].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, actual[4].Type);
Assert.Equal(FilterOperator.Equal, actual[4].As<FilterOperator>());
Assert.Equal(PathTokenType.String, actual[5].Type);
Assert.Equal("1", actual[5].As<string>());
Assert.Equal(PathTokenType.EndFilter, actual[6].Type);
// $[?(@.id == "1")]
actual = PathTokenizer.Get(path[1]);
Assert.Equal(7, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[2].Type);
Assert.Equal(PathTokenType.DotSelector, actual[3].Type);
Assert.Equal("id", actual[3].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, actual[4].Type);
Assert.Equal(FilterOperator.Equal, actual[4].As<FilterOperator>());
Assert.Equal(PathTokenType.String, actual[5].Type);
Assert.Equal("1", actual[5].As<string>());
Assert.Equal(PathTokenType.EndFilter, actual[6].Type);
}
[Fact]
public void FilterExists()
{
var path = new string[]
{
"$[?@.Spec.Properties.Kind].TargetName",
};
// $[?@.Spec.Properties.Kind].TargetName
var actual = PathTokenizer.Get(path[0]);
Assert.Equal(8, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[2].Type);
Assert.Equal(PathTokenType.DotSelector, actual[3].Type);
Assert.Equal("Spec", actual[3].As<string>());
Assert.Equal(PathTokenType.DotSelector, actual[4].Type);
Assert.Equal("Properties", actual[4].As<string>());
Assert.Equal(PathTokenType.DotSelector, actual[5].Type);
Assert.Equal("Kind", actual[5].As<string>());
Assert.Equal(PathTokenType.EndFilter, actual[6].Type);
Assert.Equal(PathTokenType.DotSelector, actual[7].Type);
Assert.Equal("TargetName", actual[7].As<string>());
}
[Fact]
public void FilterNot()
{
var path = new string[]
{
"$[?(!@.enabled)]",
"$[?!@.enabled]"
};
// $[?(!@.enabled)]
var actual = PathTokenizer.Get(path[0]);
Assert.Equal(6, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.NotOperator, actual[2].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[3].Type);
Assert.Equal(PathTokenType.DotSelector, actual[4].Type);
Assert.Equal("enabled", actual[4].As<string>());
Assert.Equal(PathTokenType.EndFilter, actual[5].Type);
// $[?!@.enabled]
actual = PathTokenizer.Get(path[1]);
Assert.Equal(6, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.NotOperator, actual[2].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[3].Type);
Assert.Equal(PathTokenType.DotSelector, actual[4].Type);
Assert.Equal("enabled", actual[4].As<string>());
Assert.Equal(PathTokenType.EndFilter, actual[5].Type);
}
[Fact]
public void FilterOr()
{
var path = new string[]
{
"$[?(@.on == true || @.enabled == true)]",
"$[?(@.on || @.enabled == true)]",
};
// $[?(@.on == true || @.enabled == true)]
var actual = PathTokenizer.Get(path[0]);
Assert.Equal(12, actual.Length);
Assert.Equal(PathTokenType.RootRef, actual[0].Type);
Assert.Equal(PathTokenType.StartFilter, actual[1].Type);
Assert.Equal(PathTokenType.CurrentRef, actual[2].Type);
Assert.Equal(PathTokenType.DotSelector, actual[3].Type);
Assert.Equal("on", actual[3].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, actual[4].Type);
Assert.Equal(FilterOperator.Equal, actual[4].As<FilterOperator>());
Assert.Equal(PathTokenType.Boolean, actual[5].Type);
Assert.True(actual[5].As<bool>());
Assert.Equal(PathTokenType.LogicalOperator, actual[6].Type);
Assert.Equal(FilterOperator.Or, actual[6].As<FilterOperator>());
Assert.Equal(PathTokenType.CurrentRef, actual[7].Type);
Assert.Equal(PathTokenType.DotSelector, actual[8].Type);
Assert.Equal("enabled", actual[8].As<string>());
Assert.Equal(FilterOperator.Equal, actual[9].As<FilterOperator>());
Assert.Equal(PathTokenType.Boolean, actual[10].Type);
Assert.True(actual[10].As<bool>());
Assert.Equal(PathTokenType.EndFilter, actual[11].Type);
}
[Fact]
public void ArraySlice()
{
var path = new string[]
{
"$.items[-1:]",
"$.items[1:2:-1]",
"$.items[:2]",
"$.items[::2]",
"$.items[::-1].id",
"$.items[:1].id",
};
// $.items[-1:]
var token = PathTokenizer.Get(path[0]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.ArraySliceSelector, token[2].Type);
Assert.Equal(-1, token[2].As<int?[]>()[0]);
Assert.Null(token[2].As<int?[]>()[1]);
Assert.Null(token[2].As<int?[]>()[2]);
// $.items[1:2:-1]
token = PathTokenizer.Get(path[1]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.ArraySliceSelector, token[2].Type);
Assert.Equal(1, token[2].As<int?[]>()[0]);
Assert.Equal(2, token[2].As<int?[]>()[1]);
Assert.Equal(-1, token[2].As<int?[]>()[2]);
// $.items[:2]
token = PathTokenizer.Get(path[2]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.ArraySliceSelector, token[2].Type);
Assert.Null(token[2].As<int?[]>()[0]);
Assert.Equal(2, token[2].As<int?[]>()[1]);
Assert.Null(token[2].As<int?[]>()[2]);
// $.items[::2]
token = PathTokenizer.Get(path[3]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.ArraySliceSelector, token[2].Type);
Assert.Null(token[2].As<int?[]>()[0]);
Assert.Null(token[2].As<int?[]>()[1]);
Assert.Equal(2, token[2].As<int?[]>()[2]);
// $.items[::-1].id
token = PathTokenizer.Get(path[4]);
Assert.Equal(4, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.ArraySliceSelector, token[2].Type);
Assert.Null(token[2].As<int?[]>()[0]);
Assert.Null(token[2].As<int?[]>()[1]);
Assert.Equal(-1, token[2].As<int?[]>()[2]);
Assert.Equal(PathTokenType.DotSelector, token[3].Type);
Assert.Equal("id", token[3].As<string>());
// $.items[:1].id
token = PathTokenizer.Get(path[5]);
Assert.Equal(4, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.ArraySliceSelector, token[2].Type);
Assert.Null(token[2].As<int?[]>()[0]);
Assert.Equal(1, token[2].As<int?[]>()[1]);
Assert.Null(token[2].As<int?[]>()[2]);
Assert.Equal(PathTokenType.DotSelector, token[3].Type);
Assert.Equal("id", token[3].As<string>());
}
[Fact]
public void Union()
{
var path = new string[]
{
"$.items[1,2]",
"$.items[ 1 , 2 ]",
"$.items['name','value']",
"$.items[ \"name\" , \"value\" ]",
};
// $.items[1,2]
var token = PathTokenizer.Get(path[0]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.UnionIndexSelector, token[2].Type);
Assert.Equal(1, token[2].As<int[]>()[0]);
Assert.Equal(2, token[2].As<int[]>()[1]);
// $.items[ 1 , 2 ]
token = PathTokenizer.Get(path[1]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.UnionIndexSelector, token[2].Type);
Assert.Equal(1, token[2].As<int[]>()[0]);
Assert.Equal(2, token[2].As<int[]>()[1]);
// $.items['name','value']
token = PathTokenizer.Get(path[2]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.UnionQuotedMemberSelector, token[2].Type);
Assert.Equal("name", token[2].As<string[]>()[0]);
Assert.Equal("value", token[2].As<string[]>()[1]);
// $.items[ "name" , "value" ]
token = PathTokenizer.Get(path[3]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("items", token[1].As<string>());
Assert.Equal(PathTokenType.UnionQuotedMemberSelector, token[2].Type);
Assert.Equal("name", token[2].As<string[]>()[0]);
Assert.Equal("value", token[2].As<string[]>()[1]);
}
/// <summary>
/// Check tokenizer against standard test cases. https://goessner.net/articles/JsonPath/index.html
/// </summary>
[Fact]
public void StandardTestCases()
{
var path = new string[]
{
"$.store.book[*].author",
"$..author",
"$.store.*",
"$.store..price",
"$..book[2]",
"$..book[(@.length-1)]",
"$..book[-1:]",
"$..book[0,1]",
"$..book[:2]",
"$..book[?(@.isbn)]",
"$..book[?(@.price<10)]",
"$..*"
};
// $.store.book[*].author
var token = PathTokenizer.Get(path[0]);
Assert.Equal(5, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("store", token[1].As<string>());
Assert.Equal(PathTokenType.DotSelector, token[2].Type);
Assert.Equal("book", token[2].As<string>());
Assert.Equal(PathTokenType.IndexWildSelector, token[3].Type);
Assert.Equal(PathTokenType.DotSelector, token[4].Type);
Assert.Equal("author", token[4].As<string>());
// $..author
token = PathTokenizer.Get(path[1]);
Assert.Equal(2, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DescendantSelector, token[1].Type);
Assert.Equal("author", token[1].As<string>());
// $.store.*
token = PathTokenizer.Get(path[2]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("store", token[1].As<string>());
Assert.Equal(PathTokenType.DotWildSelector, token[2].Type);
// $.store..price
token = PathTokenizer.Get(path[3]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DotSelector, token[1].Type);
Assert.Equal("store", token[1].As<string>());
Assert.Equal(PathTokenType.DescendantSelector, token[2].Type);
Assert.Equal("price", token[2].As<string>());
// $..book[2]
token = PathTokenizer.Get(path[4]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DescendantSelector, token[1].Type);
Assert.Equal("book", token[1].As<string>());
Assert.Equal(PathTokenType.IndexSelector, token[2].Type);
Assert.Equal(2, token[2].As<int>());
// $..book[(@.length-1)]
token = PathTokenizer.Get(path[5]);
// $..book[-1:]
token = PathTokenizer.Get(path[6]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DescendantSelector, token[1].Type);
Assert.Equal("book", token[1].As<string>());
Assert.Equal(PathTokenType.ArraySliceSelector, token[2].Type);
// $..book[0,1]
token = PathTokenizer.Get(path[7]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DescendantSelector, token[1].Type);
Assert.Equal("book", token[1].As<string>());
Assert.Equal(PathTokenType.UnionIndexSelector, token[2].Type);
Assert.Equal(0, token[2].As<int[]>()[0]);
Assert.Equal(1, token[2].As<int[]>()[1]);
// $..book[:2]
token = PathTokenizer.Get(path[8]);
Assert.Equal(3, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DescendantSelector, token[1].Type);
Assert.Equal("book", token[1].As<string>());
Assert.Equal(PathTokenType.ArraySliceSelector, token[2].Type);
// $..book[?(@.isbn)]
token = PathTokenizer.Get(path[9]);
Assert.Equal(6, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DescendantSelector, token[1].Type);
Assert.Equal("book", token[1].As<string>());
Assert.Equal(PathTokenType.StartFilter, token[2].Type);
Assert.Equal(PathTokenType.CurrentRef, token[3].Type);
Assert.Equal(PathTokenType.DotSelector, token[4].Type);
Assert.Equal("isbn", token[4].As<string>());
Assert.Equal(PathTokenType.EndFilter, token[5].Type);
// $..book[?(@.price<10)]
token = PathTokenizer.Get(path[10]);
Assert.Equal(8, token.Length);
Assert.Equal(PathTokenType.RootRef, token[0].Type);
Assert.Equal(PathTokenType.DescendantSelector, token[1].Type);
Assert.Equal("book", token[1].As<string>());
Assert.Equal(PathTokenType.StartFilter, token[2].Type);
Assert.Equal(PathTokenType.CurrentRef, token[3].Type);
Assert.Equal(PathTokenType.DotSelector, token[4].Type);
Assert.Equal("price", token[4].As<string>());
Assert.Equal(PathTokenType.ComparisonOperator, token[5].Type);
Assert.Equal(FilterOperator.Less, token[5].As<FilterOperator>());
Assert.Equal(PathTokenType.Integer, token[6].Type);
Assert.Equal(10, token[6].As<int>());
Assert.Equal(PathTokenType.EndFilter, token[7].Type);
// $..*
token = PathTokenizer.Get(path[11]);
}
}
}

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

@ -1,10 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Linq;
using System.Management.Automation;
using Newtonsoft.Json;
using PSRule.Configuration;
using PSRule.Host;
using PSRule.Pipeline;
@ -94,6 +95,31 @@ namespace PSRule
Assert.Null(withSelector.Condition.If());
}
[Fact]
public void RuleWithObjectPath()
{
var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContext(), null), new TestWriter(GetOption()));
context.Init(GetSource());
context.Begin();
ImportSelectors(context);
var yamlObjectPath = GetRuleVisitor(context, "YamlObjectPath");
context.EnterSourceScope(yamlObjectPath.Source);
var actual = GetObject(GetSourcePath("ObjectFromFile3.json"));
context.EnterTargetObject(new TargetObject(new PSObject(actual[0])));
context.EnterRuleBlock(yamlObjectPath);
Assert.True(yamlObjectPath.Condition.If().AllOf());
context.EnterTargetObject(new TargetObject(new PSObject(actual[1])));
context.EnterRuleBlock(yamlObjectPath);
Assert.False(yamlObjectPath.Condition.If().AllOf());
context.EnterTargetObject(new TargetObject(new PSObject(actual[2])));
context.EnterRuleBlock(yamlObjectPath);
Assert.True(yamlObjectPath.Condition.If().AllOf());
}
private static PSRuleOption GetOption()
{
return new PSRuleOption();
@ -115,6 +141,11 @@ namespace PSRule
return new TargetObject(result);
}
private static object[] GetObject(string path)
{
return JsonConvert.DeserializeObject<object[]>(File.ReadAllText(path));
}
private static RuleBlock GetRuleVisitor(RunspaceContext context, string name)
{
var block = HostHelper.GetRuleYamlBlocks(GetSource(), context);