зеркало из https://github.com/microsoft/PSRule.git
* 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:
Родитель
4f2f5c6001
Коммит
6cb2df7d82
|
@ -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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче