diff --git a/Directory.Build.props b/Directory.Build.props index d5bdfe6f..983787c5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,8 +2,8 @@ 1 - 2 - 1 + 3 + 0 diff --git a/Hyperbee.Json.sln b/Hyperbee.Json.sln index c197bd2c..144fb64c 100644 --- a/Hyperbee.Json.sln +++ b/Hyperbee.Json.sln @@ -22,13 +22,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1FA7 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{4DBDB7F5-3F66-4572-80B5-3322449C77A4}" ProjectSection(SolutionItems) = preProject - .github\workflows\create-prerelease.yml = .github\workflows\create-prerelease.yml .github\workflows\create-release.yml = .github\workflows\create-release.yml .github\workflows\format.yml = .github\workflows\format.yml .github\workflows\issue-branch.yml = .github\workflows\issue-branch.yml .github\workflows\publish.yml = .github\workflows\publish.yml .github\workflows\test-report.yml = .github\workflows\test-report.yml .github\workflows\test.yml = .github\workflows\test.yml + .github\workflows\update-version.yml = .github\workflows\update-version.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Json.Tests", "test\Hyperbee.Json.Tests\Hyperbee.Json.Tests.csproj", "{97886205-1467-4EE6-B3DA-496CA3D086E4}" @@ -41,6 +41,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{13CB9B41-0 docs\JSONPATH-SYNTAX.md = docs\JSONPATH-SYNTAX.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Json.Cts", "test\Hyperbee.Json.Cts\Hyperbee.Json.Cts.csproj", "{CC1D3E7F-E6F1-432B-B4D1-9402AED24119}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,10 @@ Global {45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Release|Any CPU.Build.0 = Release|Any CPU + {CC1D3E7F-E6F1-432B-B4D1-9402AED24119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC1D3E7F-E6F1-432B-B4D1-9402AED24119}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC1D3E7F-E6F1-432B-B4D1-9402AED24119}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC1D3E7F-E6F1-432B-B4D1-9402AED24119}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -69,6 +75,7 @@ Global {97886205-1467-4EE6-B3DA-496CA3D086E4} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0} {45C24D4B-4A0B-4FF1-AC66-38374D2455E9} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0} {13CB9B41-0462-4812-8B13-0BFD17F2BC18} = {870D9301-BE3D-44EA-BF9C-FCC2E87FE4CD} + {CC1D3E7F-E6F1-432B-B4D1-9402AED24119} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {32874F5B-B467-4F28-A8E2-82C2536FB228} diff --git a/Hyperbee.Json.sln.DotSettings b/Hyperbee.Json.sln.DotSettings index 83de03a7..441eaa58 100644 --- a/Hyperbee.Json.sln.DotSettings +++ b/Hyperbee.Json.sln.DotSettings @@ -51,6 +51,7 @@ True True True + True True True True diff --git a/README.md b/README.md index 789f2a24..f40a3e5e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + # Hyperbee.Json `Hyperbee.Json` is a high-performance JSONPath parser for .NET, that supports both `JsonElement` and `JsonNode`. @@ -12,12 +12,12 @@ The library is designed to be quick and extensible, allowing support for other J - **`IEnumerable` Results:** Deferred execution queries with `IEnumerable`. - **Conformant:** Adheres to the JSONPath Specification [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html). -## JSONPath Consensus +## JSONPath RFC -Hyperbee.Json aims to follow the RFC and to support the [JSONPath consensus](https://cburgmer.github.io/json-path-comparison) -when the RFC is unopinionated. When the RFC is unopinionated and where the consensus is ambiguous or not aligned with our +Hyperbee.Json conforms to the RFC, and aims to support the [JSONPath consensus](https://cburgmer.github.io/json-path-comparison) +when the RFC is unopinionated. When the RFC is unopinionated, and where the consensus is ambiguous or not aligned with our performance and usability goals, we may deviate. Our goal is always to provide a robust and performant library while -strengthening our alignment with the RFC. +strengthening our alignment with the RFC and the community. ## Installation @@ -31,34 +31,9 @@ dotnet add package Hyperbee.Json ### Basic Examples -#### Selecting a Single Element +#### Selecting Elements ```csharp -using Hyperbee.JsonPath; -using System.Text.Json; - -var json = """ -{ - "store": { - "book": [ - { "category": "fiction" }, - { "category": "science" } - ] - } -} -"""; - -var root = JsonDocument.Parse(json); -var result = JsonPath.Select(root, "$.store.book[0].category"); - -Console.WriteLine(result.First()); // Output: "fiction" -``` - -#### Selecting Multiple Elements - -```csharp -using Hyperbee.JsonPath; -using System.Text.Json; var json = """ { @@ -83,8 +58,6 @@ foreach (var item in result) #### Filtering ```csharp -using Hyperbee.JsonPath; -using System.Text.Json; var json = """ { @@ -114,8 +87,6 @@ foreach (var item in result) #### Working with (JsonElement, Path) pairs ```csharp -using Hyperbee.JsonPath; -using System.Text.Json; var json = """ { @@ -140,8 +111,6 @@ Console.WriteLine(path); // Output: "$.store.book[0].category #### Working with JsonNode ```csharp -using Hyperbee.JsonPath; -using System.Text.Json.Nodes; var json = """ { @@ -160,9 +129,9 @@ var result = JsonPath.Select(root, "$.store.book[0].category"); Console.WriteLine(result.First()); // Output: "fiction" ``` -## JSONPath Syntax Reference +## JSONPath Syntax Overview -Here's a quick reference for JSONPath syntax: +Here's a quick overview of JSONPath syntax: | JSONPath | Description |:---------------------------------------------|:----------------------------------------------------------- @@ -177,9 +146,8 @@ Here's a quick reference for JSONPath syntax: | `..` | Recursive descent | `?` | Filter selector -JSONPath expressions refer to a JSON structure in the same way as XPath expressions -are used in combination with an XML document. JSONPath assumes the name `$` is assigned -to the root level object. +JSONPath expressions refer to a JSON structure, and JSONPath assumes the name `$` is assigned +to the root JSON object. JSONPath expressions can use dot-notation: @@ -189,54 +157,54 @@ or bracket-notation: $['store']['book'][0]['title'] -JSONPath allows the wildcard symbol `*` for member names and array indices. It -borrows the descendant operator `..` from [E4X][e4x], and the array slice -syntax proposal `[start:end:step]` from ECMASCRIPT 4. +- JSONPath allows the wildcard symbol `*` for member names and array indices. +- It borrows the descendant operator `..` from [E4X][e4x] +- It uses the `@` symbol to refer to the current object. +- It uses the `?()` syntax for filtering. +- It uses the array slice syntax proposal `[start:end:step]` from ECMASCRIPT 4. Expressions can be used as an alternative to explicit names or indices, as in: - $.store.book[(@.length-1)].title + $.store.book[(length(@)-1)].title -using the symbol `@` for the current object. Filter expressions are supported via -the syntax `?()`, as in: +Filter expressions are supported via the syntax `?()`, as in: $.store.book[?(@.price < 10)].title -### JSONPath Methods +### JSONPath Functions JsonPath expressions support basic methods calls. | Method | Description | Example |------------|--------------------------------------------------------|------------------------------------------------ | `length()` | Returns the length of an array or string. | `$.store.book[?(length(@.title) > 5)]` -| `count()` | Returns the count of matching elements. | `$.store.book[?(count(@.authors) > 1)]` -| `match()` | Returns true if a string matches a regular expression. | `$.store.book[?(match(@.title, '.*Century.*'))]` -| `search()` | Searches for a string within another string. | `$.store.book[?(search(@.title, 'Sword'))]` +| `count()` | Returns the count of matching elements. | `$.store.book[?(count(@.authors.) > 1)]` +| `match()` | Returns true if a string matches a regular expression. | `$.store.book[?(match(@.title,'.*Century.*'))]` +| `search()` | Searches for a string within another string. | `$.store.book[?(search(@.title,'Sword'))]` | `value()` | Accesses the value of a key in the current object. | `$.store.book[?(value(@.price) < 10)]` -You can extend the supported function set by registering your own functions. +### JSONPath Custom Functions -#### Example: `JsonNode` Path Function +You can also extend the supported function set by registering your own functions. + +**Example:** Implement a `JsonNode` Path Function: **Step 1:** Create a custom function that returns the path of a `JsonNode`. ```csharp -public class PathNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) +public class PathNodeFunction() : FilterExtensionFunction( PathMethodInfo, FilterExtensionInfo.MustCompare ) { public const string Name = "path"; - private static readonly Expression PathExpression = Expression.Constant( (Func, string>) Path ); + private static readonly MethodInfo PathMethodInfo = GetMethod( nameof( Path ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + private static INodeType Path( INodeType arg ) { - return Expression.Invoke( PathExpression, arguments[0] ); - } + if ( arg is not NodesType nodes ) + return Constants.Null; - public static string Path( IEnumerable nodes ) - { var node = nodes.FirstOrDefault(); - return node?.GetPath(); - } + return new ValueType( node?.GetPath() ); } ``` @@ -260,11 +228,11 @@ There are excellent libraries available for RFC-9535 .NET JsonPath. ### [JsonPath.Net](https://docs.json-everything.net/path/basics/) Json-Everything - **Pros:** - - Extensive JSON ecosystem. - Comprehensive feature set. - Deferred execution queries with `IEnumerable`. - Strong community support. - + - .NET Foundation Project. + - **Cons:** - No support for `JsonElement`. - Not quite as fast as other `System.Text.Json` implementations. @@ -285,7 +253,7 @@ There are excellent libraries available for RFC-9535 .NET JsonPath. - Comprehensive feature set. - Documentation and examples. - Strong community support. - - Level 2 .NET Foundation Project. + - .NET Foundation Project. - **Cons:** - No support for `JsonElement`, or `JsonNode`. @@ -296,9 +264,9 @@ There are excellent libraries available for RFC-9535 .NET JsonPath. - Supports both `JsonElement`, and `JsonNode`. - Deferred execution queries with `IEnumerable`. - Extendable to support additional JSON document types and functions. -- Consensus focused JSONPath implementation. +- RFC conforming JSONPath implementation. -- ## Benchmarks +## Benchmarks Here is a performance comparison of various queries on the standard book store document. @@ -342,42 +310,41 @@ Here is a performance comparison of various queries on the standard book store d ``` ``` -| Method | Filter | Mean | Error | StdDev | Allocated -|:----------------------- |:-------------------------------- |:--------- |:--------- |:--------- |:--------- -| Hyperbee_JsonElement | $..* `First()` | 3.026 us | 0.3647 us | 0.0200 us | 4.22 KB -| JsonEverything_JsonNode | $..* `First()` | 3.170 us | 0.3034 us | 0.0166 us | 3.53 KB -| Hyperbee_JsonNode | $..* `First()` | 3.275 us | 1.7533 us | 0.0961 us | 3.37 KB -| JsonCons_JsonElement | $..* `First()` | 5.699 us | 0.2191 us | 0.0120 us | 8.48 KB -| Newtonsoft_JObject | $..* `First()` | 8.671 us | 1.7810 us | 0.0976 us | 14.22 KB -| | | | | | -| JsonCons_JsonElement | $..* | 5.772 us | 3.8960 us | 0.2136 us | 8.45 KB -| Hyperbee_JsonElement | $..* | 8.179 us | 4.9380 us | 0.2707 us | 11.02 KB -| Newtonsoft_JObject | $..* | 9.867 us | 0.9006 us | 0.0494 us | 14.86 KB -| Hyperbee_JsonNode | $..* | 10.188 us | 2.0528 us | 0.1125 us | 10.83 KB -| JsonEverything_JsonNode | $..* | 21.124 us | 5.1117 us | 0.2802 us | 36.81 KB -| | | | | | -| Hyperbee_JsonElement | $..price | 4.867 us | 0.1883 us | 0.0103 us | 6.37 KB -| JsonCons_JsonElement | $..price | 4.924 us | 1.5997 us | 0.0877 us | 5.65 KB -| Hyperbee_JsonNode | $..price | 7.827 us | 5.0475 us | 0.2767 us | 8.77 KB -| Newtonsoft_JObject | $..price | 9.442 us | 1.0020 us | 0.0549 us | 14.4 KB -| JsonEverything_JsonNode | $..price | 15.865 us | 2.1515 us | 0.1179 us | 27.63 KB -| | | | | | -| Hyperbee_JsonElement | $.store.book[?(@.price == 8.99)] | 4.550 us | 1.0340 us | 0.0567 us | 9.08 KB -| JsonCons_JsonElement | $.store.book[?(@.price == 8.99)] | 5.341 us | 1.0738 us | 0.0589 us | 5.05 KB -| Hyperbee_JsonNode | $.store.book[?(@.price == 8.99)] | 7.341 us | 3.6147 us | 0.1981 us | 10.63 KB -| Newtonsoft_JObject | $.store.book[?(@.price == 8.99)] | 9.621 us | 5.1553 us | 0.2826 us | 15.84 KB -| JsonEverything_JsonNode | $.store.book[?(@.price == 8.99)] | 11.789 us | 5.2457 us | 0.2875 us | 15.85 KB -| | | | | | -| Hyperbee_JsonElement | $.store.book[0] | 2.896 us | 0.1069 us | 0.0059 us | 3.41 KB -| JsonCons_JsonElement | $.store.book[0] | 2.967 us | 0.1084 us | 0.0059 us | 3.21 KB -| Hyperbee_JsonNode | $.store.book[0] | 3.352 us | 0.1778 us | 0.0097 us | 3.58 KB -| JsonEverything_JsonNode | $.store.book[0] | 4.779 us | 2.9031 us | 0.1591 us | 5.96 KB -| Newtonsoft_JObject | $.store.book[0] | 8.714 us | 2.5518 us | 0.1399 us | 14.56 KB -``` -``` +| Method | Filter | Mean | Error | StdDev | Allocated +|------------------------ |--------------------------------- |---------- |----------- |---------- |---------- +| Hyperbee_JsonElement | $..* `First()` | 3.186 us | 0.6615 us | 0.0363 us | 4.3 KB +| Hyperbee_JsonNode | $..* `First()` | 3.521 us | 0.1192 us | 0.0065 us | 3.45 KB +| JsonEverything_JsonNode | $..* `First()` | 3.545 us | 0.7400 us | 0.0406 us | 3.53 KB +| JsonCons_JsonElement | $..* `First()` | 5.793 us | 1.3811 us | 0.0757 us | 8.48 KB +| Newtonsoft_JObject | $..* `First()` | 9.119 us | 5.3278 us | 0.2920 us | 14.22 KB +| | | | | | +| JsonCons_JsonElement | $..* | 6.098 us | 2.0947 us | 0.1148 us | 8.45 KB +| Hyperbee_JsonElement | $..* | 8.812 us | 1.6812 us | 0.0922 us | 11.1 KB +| Hyperbee_JsonNode | $..* | 10.621 us | 1.2452 us | 0.0683 us | 10.91 KB +| Newtonsoft_JObject | $..* | 11.037 us | 5.4690 us | 0.2998 us | 14.86 KB +| JsonEverything_JsonNode | $..* | 23.329 us | 2.2255 us | 0.1220 us | 36.81 KB +| | | | | | +| Hyperbee_JsonElement | $..price | 5.248 us | 3.4306 us | 0.1880 us | 6.45 KB +| JsonCons_JsonElement | $..price | 5.402 us | 0.3285 us | 0.0180 us | 5.65 KB +| Hyperbee_JsonNode | $..price | 8.483 us | 2.0999 us | 0.1151 us | 8.86 KB +| Newtonsoft_JObject | $..price | 10.109 us | 9.6403 us | 0.5284 us | 14.4 KB +| JsonEverything_JsonNode | $..price | 17.054 us | 10.5303 us | 0.5772 us | 27.63 KB +| | | | | | +| Hyperbee_JsonElement | $.store.book[?(@.price == 8.99)] | 4.486 us | 3.2931 us | 0.1805 us | 5.82 KB +| JsonCons_JsonElement | $.store.book[?(@.price == 8.99)] | 5.381 us | 3.3826 us | 0.1854 us | 5.05 KB +| Hyperbee_JsonNode | $.store.book[?(@.price == 8.99)] | 7.354 us | 4.9887 us | 0.2734 us | 8.47 KB +| Newtonsoft_JObject | $.store.book[?(@.price == 8.99)] | 10.519 us | 3.5514 us | 0.1947 us | 15.84 KB +| JsonEverything_JsonNode | $.store.book[?(@.price == 8.99)] | 11.912 us | 7.6346 us | 0.4185 us | 15.85 KB +| | | | | | +| Hyperbee_JsonElement | $.store.book[0] | 2.722 us | 0.5813 us | 0.0319 us | 2.27 KB +| JsonCons_JsonElement | $.store.book[0] | 3.150 us | 1.7316 us | 0.0949 us | 3.21 KB +| Hyperbee_JsonNode | $.store.book[0] | 3.339 us | 0.1733 us | 0.0095 us | 2.77 KB +| JsonEverything_JsonNode | $.store.book[0] | 4.974 us | 3.2013 us | 0.1755 us | 5.96 KB +| Newtonsoft_JObject | $.store.book[0] | 9.482 us | 7.0303 us | 0.3854 us | 14.56 KB + ``` -## Additional Documentation +## Additioal Documentation Additional documentation can be found in the project's `/docs` folder. @@ -385,10 +352,11 @@ Additional documentation can be found in the project's `/docs` folder. Hyperbee.Json is built upon the great work of several open-source projects. Special thanks to: -- Stefan Goessner for the original [JSONPath implementation](https://goessner.net/articles/JsonPath/). - System.Text.Json team for their work on the `System.Text.Json` library. +- Stefan Goessner for the original [JSONPath implementation](https://goessner.net/articles/JsonPath/). - Atif Aziz's C# port of Goessner's JSONPath library [.NET JSONPath](https://github.com/atifaziz/JSONPath). - Christoph Burgmer [JSONPath consensus effort](https://cburgmer.github.io/json-path-comparison). +- [JSONPath Compliance Test Suite Team](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite). ## Contributing diff --git a/docs/ADDITIONAL-CLASSES.md b/docs/ADDITIONAL-CLASSES.md index 73d99814..bb90dcf3 100644 --- a/docs/ADDITIONAL-CLASSES.md +++ b/docs/ADDITIONAL-CLASSES.md @@ -5,28 +5,18 @@ property diving, element comparisons, and dynamic property access. ### Property Diving -Property diving acts similarly to JSON Pointer; it expects a path that returns a single element. -Unlike JSON Pointer, property diving notation expects a singular JSON Path. +Property diving acts **similarly** to JSON Pointer; it expects an absolute path that returns a single element. +Unlike JSON Pointer, property diving notation expects normalized JSON Path notation. | Method | Description |:-----------------------------------|:----------- | `JsonElement.FromJsonPathPointer` | Dives for properties using absolute locations like `$.store.book[2].author` -The syntax supports singular paths; dotted notation, quoted names, and simple bracketed array accessors only. +The syntax supports absolute (normalized) paths; dotted notation, quoted names, and simple bracketed array accessors only. The intention is to return a single element by literal path. -Json path style '$', wildcard '*', '..', and '[a,b]' multi-result selector notations and filters are **not** supported. +Json path style wildcard '*', '..', and '[a,b]' multi-result selector notations and filters are **not** supported. -``` -Examples of valid path syntax: - - prop1.prop2 - prop1[0] - 'prop.2' - prop1[0].prop2 - prop1['prop.2'] - prop1.'prop.2'[0].prop3 -``` ### JsonElement Path @@ -35,7 +25,7 @@ for a given `JsonElement`. | Method | Description |:---------------------------|:----------- -| `JsonPathBuilder.GetPath` | Returns the JsonPath location string for a given element +| `JsonPathBuilder.GetPath` | Returns the JsonPath location string for a given element ### Equality Helpers @@ -47,10 +37,25 @@ for a given `JsonElement`. ### Dynamic Object Serialization Basic support is provided for serializing to and from dynamic objects through the use of a custom `JsonConverter`. -The `DynamicJsonConverter` converter class is useful for simple scenareos. It is intended as a simple helper for -basic use cases only. +The `DynamicJsonConverter` class is useful for simple scenareos. It is intended as a simple helper for +basic use cases only. A helper methods `JsonHelper.ConvertToDynamic` is provided to simplify the process of +serializing and deserializing dynamic objects. + +#### Example: ConvertToDynamic + +```csharp +var root = JsonDocument.Parse(jsonInput); // jsonInput contains the bookstore example +var element = JsonHelper.ConvertToDynamic( source ); + +var book = element.store.book[0]; +var author = book.author; +var price = book.price; + +Assert.IsTrue( price == 8.95 ); +Assert.IsTrue( author == "Nigel Rees" ); +``` -#### DynamicJsonConverter +#### Example: Serialize To Dynamic ```csharp var serializerOptions = new JsonSerializerOptions @@ -58,7 +63,7 @@ var serializerOptions = new JsonSerializerOptions Converters = {new DynamicJsonConverter()} }; -// jsonInput is a string containing the bookstore json from the previous examples +// jsonInput contains the bookstore example var jobject = JsonSerializer.Deserialize( jsonInput, serializerOptions); Assert.IsTrue( jobject.store.bicycle.color == "red" ); diff --git a/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs index d58e9bf7..09687c88 100644 --- a/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs +++ b/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Hyperbee.Json.Descriptors.Element.Functions; using Hyperbee.Json.Filters; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Element; @@ -8,6 +9,7 @@ public class ElementTypeDescriptor : ITypeDescriptor { private FilterEvaluator _evaluator; private ElementValueAccessor _accessor; + private NodeTypeComparer _comparer; public FunctionRegistry Functions { get; } = new(); @@ -17,6 +19,11 @@ public class ElementTypeDescriptor : ITypeDescriptor public IFilterEvaluator FilterEvaluator => _evaluator ??= new FilterEvaluator( this ); + public INodeTypeComparer Comparer => + _comparer ??= new NodeTypeComparer( Accessor ); + + public bool CanUsePointer => true; + public ElementTypeDescriptor() { Functions.Register( CountElementFunction.Name, () => new CountElementFunction() ); diff --git a/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs b/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs index 6de13d15..3dac8a6c 100644 --- a/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs @@ -43,9 +43,18 @@ internal class ElementValueAccessor : IValueAccessor } [MethodImpl( MethodImplOptions.AggressiveInlining )] - public JsonElement GetElementAt( in JsonElement value, int index ) + public bool TryGetElementAt( in JsonElement value, int index, out JsonElement element ) { - return value[index]; + element = default; + + if ( index < 0 ) // flip negative index to positive + index = value.GetArrayLength() + index; + + if ( index < 0 || index >= value.GetArrayLength() ) // out of bounds + return false; + + element = value[index]; + return true; } [MethodImpl( MethodImplOptions.AggressiveInlining )] @@ -67,7 +76,7 @@ public int GetArrayLength( in JsonElement value ) : 0; } - public bool TryGetChildValue( in JsonElement value, string childSelector, out JsonElement childValue ) + public bool TryGetChildValue( in JsonElement value, string childSelector, SelectorKind selectorKind, out JsonElement childValue ) { switch ( value.ValueKind ) { @@ -77,9 +86,17 @@ public bool TryGetChildValue( in JsonElement value, string childSelector, out Js break; case JsonValueKind.Array: + if ( selectorKind == SelectorKind.Name ) + break; + if ( int.TryParse( childSelector, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index ) ) { - if ( index >= 0 && index < value.GetArrayLength() ) + var arrayLength = value.GetArrayLength(); + + if ( index < 0 ) // flip negative index to positive + index = arrayLength + index; + + if ( index >= 0 && index < arrayLength ) { childValue = value[index]; return true; @@ -163,4 +180,9 @@ public bool TryGetValueFromNode( JsonElement element, out object value ) return true; } + + public bool TryGetFromPointer( in JsonElement element, JsonPathSegment segment, out JsonElement childValue ) + { + return element.TryGetFromJsonPathPointer( segment, out childValue ); + } } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs index c2073206..30c22efe 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs @@ -1,22 +1,25 @@ -using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; - using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Element.Functions; -public class CountElementFunction() : FilterExtensionFunction( argumentCount: 1 ) +public class CountElementFunction() : FilterExtensionFunction( CountMethodInfo, FilterExtensionInfo.MustCompare ) { public const string Name = "count"; - private static readonly Expression CountExpression = Expression.Constant( (Func, float>) Count ); - - protected override Expression GetExtensionExpression( Expression[] arguments ) - { - return Expression.Invoke( CountExpression, arguments[0] ); - } + private static readonly MethodInfo CountMethodInfo = GetMethod( nameof( Count ) ); - public static float Count( IEnumerable elements ) + public static INodeType Count( INodeType input ) { - return elements.Count(); + switch ( input ) + { + case NodesType nodes: + if ( nodes.IsNormalized && !nodes.Any() ) + return new ValueType( 1F ); + return new ValueType( nodes.Count() ); + default: + return new ValueType( 1F ); + } } } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs index 7a40e282..fc2372ce 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs @@ -1,28 +1,42 @@ -using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Element.Functions; -public class LengthElementFunction() : FilterExtensionFunction( argumentCount: 1 ) +public class LengthElementFunction() : FilterExtensionFunction( LengthMethodInfo, FilterExtensionInfo.MustCompare | FilterExtensionInfo.ExpectNormalized ) { public const string Name = "length"; - private static readonly Expression LengthExpression = Expression.Constant( (Func, float>) Length ); + private static readonly MethodInfo LengthMethodInfo = GetMethod( nameof( Length ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Length( INodeType input ) { - return Expression.Invoke( LengthExpression, arguments[0] ); + return input switch + { + NodesType nodes => LengthImpl( nodes.FirstOrDefault() ), + ValueType valueString => new ValueType( valueString.Value.Length ), + Null or Nothing => input, + _ => Constants.Nothing + }; } - public static float Length( IEnumerable elements ) + public static INodeType LengthImpl( object value ) { - var element = elements.FirstOrDefault(); - return element.ValueKind switch + return value switch { - JsonValueKind.String => element.GetString()?.Length ?? 0, - JsonValueKind.Array => element.GetArrayLength(), - JsonValueKind.Object => element.EnumerateObject().Count(), - _ => 0 + string str => new ValueType( str.Length ), + Array array => new ValueType( array.Length ), + System.Collections.ICollection collection => new ValueType( collection.Count ), + System.Collections.IEnumerable enumerable => new ValueType( enumerable.Cast().Count() ), + JsonElement node => node.ValueKind switch + { + JsonValueKind.String => new ValueType( node.GetString()?.Length ?? 0 ), + JsonValueKind.Array => new ValueType( node.EnumerateArray().Count() ), + JsonValueKind.Object => new ValueType( node.EnumerateObject().Count() ), + _ => Constants.Null + }, + _ => Constants.Null }; } } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs index 323a6331..652c9c5f 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs @@ -1,30 +1,39 @@ -using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; +using Hyperbee.Json.Filters; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Element.Functions; -public class MatchElementFunction() : FilterExtensionFunction( argumentCount: 2 ) +public class MatchElementFunction() : FilterExtensionFunction( MatchMethodInfo, FilterExtensionInfo.MustNotCompare ) { public const string Name = "match"; - private static readonly Expression MatchExpression = Expression.Constant( (Func, string, bool>) Match ); + private static readonly MethodInfo MatchMethodInfo = GetMethod( nameof( Match ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Match( INodeType input, INodeType regex ) { - return Expression.Invoke( MatchExpression, arguments[0], arguments[1] ); + return input switch + { + NodesType nodes when regex is ValueType stringValue => + MatchImpl( nodes, stringValue.Value ), + NodesType nodes when regex is NodesType stringValue => + MatchImpl( nodes, stringValue.Value.FirstOrDefault().GetString() ), + _ => Constants.False + }; } - public static bool Match( IEnumerable elements, string regex ) + public static INodeType MatchImpl( NodesType nodes, string regex ) { - var value = elements.FirstOrDefault().GetString(); + var value = nodes.FirstOrDefault(); - if ( value == null ) - { - return false; - } + if ( value.ValueKind != JsonValueKind.String ) + return Constants.False; + + var stringValue = value.GetString() ?? string.Empty; - var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - return regexPattern.IsMatch( $"^{value}$" ); + var regexPattern = new Regex( $"^{IRegexp.ConvertToIRegexp( regex )}$" ); + return new ValueType( regexPattern.IsMatch( stringValue ) ); } } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs index 47686c5e..28d4a40b 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs @@ -1,30 +1,39 @@ -using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; +using Hyperbee.Json.Filters; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Element.Functions; -public class SearchElementFunction() : FilterExtensionFunction( argumentCount: 2 ) +public class SearchElementFunction() : FilterExtensionFunction( SearchMethodInfo, FilterExtensionInfo.MustNotCompare ) { public const string Name = "search"; - private static readonly Expression SearchExpression = Expression.Constant( (Func, string, bool>) Search ); + private static readonly MethodInfo SearchMethodInfo = GetMethod( nameof( Search ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Search( INodeType input, INodeType regex ) { - return Expression.Invoke( SearchExpression, arguments[0], arguments[1] ); + return input switch + { + NodesType nodes when regex is ValueType stringValue => + SearchImpl( nodes, stringValue.Value ), + NodesType nodes when regex is NodesType stringValue => + SearchImpl( nodes, stringValue.Value.FirstOrDefault().GetString() ), + _ => Constants.False + }; } - public static bool Search( IEnumerable elements, string regex ) + public static INodeType SearchImpl( NodesType nodes, string regex ) { - var value = elements.FirstOrDefault().GetString(); + var value = nodes.FirstOrDefault(); - if ( value == null ) - { - return false; - } + if ( value.ValueKind != JsonValueKind.String ) + return Constants.False; + + var stringValue = value.GetString() ?? string.Empty; - var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - return regexPattern.IsMatch( value ); + var regexPattern = new Regex( IRegexp.ConvertToIRegexp( regex ) ); + return new ValueType( regexPattern.IsMatch( stringValue ) ); } } diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs index 8e3ce63b..8c04d5c4 100644 --- a/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs @@ -1,42 +1,43 @@ -using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Element.Functions; -public class ValueElementFunction() : FilterExtensionFunction( argumentCount: 1 ) +public class ValueElementFunction() : FilterExtensionFunction( ValueMethodInfo, FilterExtensionInfo.MustCompare ) { public const string Name = "value"; - public static readonly Expression ValueExpression = Expression.Constant( (Func, object>) Value ); + private static readonly MethodInfo ValueMethodInfo = GetMethod( nameof( Value ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Value( INodeType arg ) { - return Expression.Invoke( ValueExpression, arguments[0] ); - } + if ( arg.Kind != NodeTypeKind.NodeList ) + throw new NotSupportedException( $"Function {Name} does not support kind {arg.Kind}" ); - public static object Value( IEnumerable elements ) - { - var element = elements.FirstOrDefault(); + var nodeArray = ((NodesType) arg).ToArray(); + + if ( nodeArray.Length != 1 ) + return Constants.Nothing; + + var node = nodeArray.FirstOrDefault(); - return element.ValueKind switch + return node.ValueKind switch { - JsonValueKind.Number => element.GetSingle(), - JsonValueKind.String => element.GetString(), - JsonValueKind.Object => IsNotEmpty( element ), - JsonValueKind.Array => IsNotEmpty( element ), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Null => false, - JsonValueKind.Undefined => false, - _ => false + JsonValueKind.Number => new ValueType( node.GetSingle() ), + JsonValueKind.String => new ValueType( node.GetString() ), + JsonValueKind.Object or JsonValueKind.Array => new ValueType( IsNotEmpty( node ) ), + JsonValueKind.True => Constants.True, + JsonValueKind.False or JsonValueKind.Null or JsonValueKind.Undefined => Constants.False, + _ => Constants.False }; - static bool IsNotEmpty( JsonElement element ) + static bool IsNotEmpty( JsonElement node ) { - return element.ValueKind switch + return node.ValueKind switch { - JsonValueKind.Array => element.EnumerateArray().Any(), - JsonValueKind.Object => element.EnumerateObject().Any(), + JsonValueKind.Array => node.EnumerateArray().Any(), + JsonValueKind.Object => node.EnumerateObject().Any(), _ => false }; } diff --git a/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs index dcffa35a..6ce777d2 100644 --- a/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs +++ b/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs @@ -1,5 +1,6 @@ using Hyperbee.Json.Filters; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors; @@ -15,6 +16,9 @@ public interface ITypeDescriptor : ITypeDescriptor public IValueAccessor Accessor { get; } public IFilterEvaluator FilterEvaluator { get; } + public INodeTypeComparer Comparer { get; } + bool CanUsePointer { get; } + public void Deconstruct( out IValueAccessor valueAccessor, out IFilterEvaluator filterEvaluator ) { valueAccessor = Accessor; diff --git a/src/Hyperbee.Json/Descriptors/IValueAccessor.cs b/src/Hyperbee.Json/Descriptors/IValueAccessor.cs index 2898343f..c390baaa 100644 --- a/src/Hyperbee.Json/Descriptors/IValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/IValueAccessor.cs @@ -3,11 +3,12 @@ public interface IValueAccessor { IEnumerable<(TNode, string, SelectorKind)> EnumerateChildren( TNode value, bool includeValues = true ); - TNode GetElementAt( in TNode value, int index ); + bool TryGetElementAt( in TNode value, int index, out TNode element ); NodeKind GetNodeKind( in TNode value ); int GetArrayLength( in TNode value ); - bool TryGetChildValue( in TNode value, string childSelector, out TNode childValue ); + bool TryGetChildValue( in TNode value, string childSelector, SelectorKind selectorKind, out TNode childValue ); bool TryParseNode( ReadOnlySpan item, out TNode value ); bool DeepEquals( TNode left, TNode right ); - bool TryGetValueFromNode( TNode item, out object o ); + bool TryGetValueFromNode( TNode item, out object value ); + bool TryGetFromPointer( in TNode value, JsonPathSegment segment, out TNode childValue ); } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs index 170f06cf..38480824 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs @@ -1,21 +1,25 @@ -using System.Linq.Expressions; +using System.Reflection; using System.Text.Json.Nodes; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Node.Functions; -public class CountNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) +public class CountNodeFunction() : FilterExtensionFunction( CountMethodInfo, FilterExtensionInfo.MustCompare ) { public const string Name = "count"; - private static readonly Expression CountExpression = Expression.Constant( (Func, float>) Count ); + private static readonly MethodInfo CountMethodInfo = GetMethod( nameof( Count ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Count( INodeType arg ) { - return Expression.Invoke( CountExpression, arguments[0] ); - } + if ( arg.Kind != NodeTypeKind.NodeList ) + throw new NotSupportedException( $"Function {Name} must be a node list." ); - public static float Count( IEnumerable nodes ) - { - return nodes.Count(); + var nodes = (NodesType) arg; + + if ( nodes.IsNormalized && !nodes.Any() ) + return new ValueType( 1 ); + + return new ValueType( nodes.Count() ); } } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs index 76341163..0f5792d7 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs @@ -1,29 +1,44 @@ -using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Node.Functions; -public class LengthNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) +public class LengthNodeFunction() : FilterExtensionFunction( LengthMethodInfo, FilterExtensionInfo.MustCompare | FilterExtensionInfo.ExpectNormalized ) { public const string Name = "length"; - private static readonly Expression LengthExpression = Expression.Constant( (Func, float>) Length ); + private static readonly MethodInfo LengthMethodInfo = GetMethod( nameof( Length ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Length( INodeType input ) { - return Expression.Invoke( LengthExpression, arguments[0] ); + return input switch + { + NodesType nodes => LengthImpl( nodes.FirstOrDefault() ), + ValueType valueString => new ValueType( valueString.Value.Length ), + Null or Nothing => input, + _ => Constants.Nothing + }; } - public static float Length( IEnumerable nodes ) + public static INodeType LengthImpl( object value ) { - var node = nodes.FirstOrDefault(); - return node?.GetValueKind() switch + return value switch { - JsonValueKind.String => node.GetValue()?.Length ?? 0, - JsonValueKind.Array => node.AsArray().Count, - JsonValueKind.Object => node.AsObject().Count, - _ => 0 + string str => new ValueType( str.Length ), + Array array => new ValueType( array.Length ), + System.Collections.ICollection collection => new ValueType( collection.Count ), + System.Collections.IEnumerable enumerable => new ValueType( enumerable.Cast().Count() ), + JsonNode node => node.GetValueKind() switch + { + JsonValueKind.String => new ValueType( node.GetValue()?.Length ?? 0 ), + JsonValueKind.Array => new ValueType( node.AsArray().Count ), + JsonValueKind.Object => new ValueType( node.AsObject().Count ), + _ => Constants.Nothing + }, + _ => Constants.Nothing }; } + } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs index 6b259720..443d9fcb 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs @@ -1,30 +1,41 @@ -using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; +using Hyperbee.Json.Filters; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Node.Functions; -public class MatchNodeFunction() : FilterExtensionFunction( argumentCount: 2 ) +public class MatchNodeFunction() : FilterExtensionFunction( MatchMethodInfo, FilterExtensionInfo.MustNotCompare ) { public const string Name = "match"; - private static readonly Expression MatchExpression = Expression.Constant( (Func, string, bool>) Match ); + private static readonly MethodInfo MatchMethodInfo = GetMethod( nameof( Match ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Match( INodeType input, INodeType regex ) { - return Expression.Invoke( MatchExpression, arguments[0], arguments[1] ); + return input switch + { + NodesType nodes when regex is ValueType stringValue => + MatchImpl( nodes, stringValue.Value ), + NodesType nodes when regex is NodesType stringValue => + MatchImpl( nodes, stringValue.Value.FirstOrDefault()?.GetValue() ), + _ => Constants.False + }; } - public static bool Match( IEnumerable nodes, string regex ) + private static INodeType MatchImpl( NodesType nodes, string regex ) { - var value = nodes.FirstOrDefault()?.GetValue(); + var value = nodes.FirstOrDefault(); - if ( value == null ) - { - return false; - } + if ( value?.GetValueKind() != JsonValueKind.String ) + return Constants.False; - var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - return regexPattern.IsMatch( $"^{value}$" ); + var stringValue = value.GetValue(); + + var regexPattern = new Regex( $"^{IRegexp.ConvertToIRegexp( regex )}$" ); + return new ValueType( regexPattern.IsMatch( stringValue ) ); } } + diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/SearchNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/SearchNodeFunction.cs index 8f1f4e1a..40f26776 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/SearchNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/SearchNodeFunction.cs @@ -1,30 +1,38 @@ -using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; +using Hyperbee.Json.Filters; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Node.Functions; -public class SearchNodeFunction() : FilterExtensionFunction( argumentCount: 2 ) +public class SearchNodeFunction() : FilterExtensionFunction( SearchMethodInfo, FilterExtensionInfo.MustNotCompare ) { public const string Name = "search"; - private static readonly Expression SearchExpression = Expression.Constant( (Func, string, bool>) Search ); + private static readonly MethodInfo SearchMethodInfo = GetMethod( nameof( Search ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Search( INodeType input, INodeType regex ) { - return Expression.Invoke( SearchExpression, arguments[0], arguments[1] ); + return input switch + { + NodesType nodes when regex is ValueType stringValue => + SearchImpl( nodes, stringValue.Value ), + NodesType nodes when regex is NodesType stringValue => + SearchImpl( nodes, stringValue.Value.FirstOrDefault()?.GetValue() ), + _ => Constants.False + }; } - public static bool Search( IEnumerable nodes, string regex ) + public static INodeType SearchImpl( NodesType nodes, string regex ) { - var value = nodes.FirstOrDefault()?.GetValue(); + var value = nodes.FirstOrDefault(); - if ( value == null ) - { - return false; - } + if ( value?.GetValueKind() != JsonValueKind.String ) + return Constants.False; - var regexPattern = new Regex( regex.Trim( '\"', '\'' ) ); - return regexPattern.IsMatch( value ); + var regexPattern = new Regex( IRegexp.ConvertToIRegexp( regex ) ); + return new ValueType( regexPattern.IsMatch( value.GetValue() ) ); } } diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/ValueNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/ValueNodeFunction.cs index f31f8fe4..36ae42f6 100644 --- a/src/Hyperbee.Json/Descriptors/Node/Functions/ValueNodeFunction.cs +++ b/src/Hyperbee.Json/Descriptors/Node/Functions/ValueNodeFunction.cs @@ -1,36 +1,36 @@ -using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; -using Hyperbee.Json.Extensions; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Node.Functions; -public class ValueNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) +public class ValueNodeFunction() : FilterExtensionFunction( ValueMethodInfo, FilterExtensionInfo.MustCompare ) { public const string Name = "value"; - public static readonly Expression ValueExpression = Expression.Constant( (Func, object>) Value ); + private static readonly MethodInfo ValueMethodInfo = GetMethod( nameof( Value ) ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + public static INodeType Value( INodeType arg ) { - return Expression.Invoke( ValueExpression, arguments[0] ); - } + if ( arg.Kind != NodeTypeKind.NodeList ) + throw new NotSupportedException( $"Function {Name} does not support kind {arg.Kind}" ); - public static object Value( IEnumerable nodes ) - { - var node = nodes.FirstOrDefault(); + var nodeArray = ((NodesType) arg).ToArray(); + + if ( nodeArray.Length != 1 ) + return Constants.Nothing; + + var node = nodeArray.FirstOrDefault(); return node?.GetValueKind() switch { - JsonValueKind.Number => node.GetNumber(), - JsonValueKind.String => node.GetValue(), - JsonValueKind.Object => IsNotEmpty( node ), - JsonValueKind.Array => IsNotEmpty( node ), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Null => false, - JsonValueKind.Undefined => false, - _ => false + JsonValueKind.Number => new ValueType( node.GetValue() ), + JsonValueKind.String => new ValueType( node.GetValue() ), + JsonValueKind.Object or JsonValueKind.Array => new ValueType( IsNotEmpty( node ) ), + JsonValueKind.True => Constants.True, + JsonValueKind.False or JsonValueKind.Null or JsonValueKind.Undefined => Constants.False, + _ => Constants.False }; static bool IsNotEmpty( JsonNode node ) @@ -43,5 +43,4 @@ static bool IsNotEmpty( JsonNode node ) }; } } - } diff --git a/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs index d73674ee..bdfd97b4 100644 --- a/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs +++ b/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs @@ -1,6 +1,7 @@ using System.Text.Json.Nodes; using Hyperbee.Json.Descriptors.Node.Functions; using Hyperbee.Json.Filters; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Descriptors.Node; @@ -8,6 +9,7 @@ public class NodeTypeDescriptor : ITypeDescriptor { private FilterEvaluator _evaluator; private NodeValueAccessor _accessor; + private NodeTypeComparer _comparer; public FunctionRegistry Functions { get; } = new(); @@ -17,6 +19,11 @@ public class NodeTypeDescriptor : ITypeDescriptor public IFilterEvaluator FilterEvaluator => _evaluator ??= new FilterEvaluator( this ); + public INodeTypeComparer Comparer => + _comparer ??= new NodeTypeComparer( Accessor ); + + public bool CanUsePointer => true; + public NodeTypeDescriptor() { Functions.Register( CountNodeFunction.Name, () => new CountNodeFunction() ); diff --git a/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs b/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs index 9b50425f..852b0163 100644 --- a/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs +++ b/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; namespace Hyperbee.Json.Descriptors.Node; @@ -13,7 +14,6 @@ internal class NodeValueAccessor : IValueAccessor { case JsonArray arrayValue: for ( var index = arrayValue.Count - 1; index >= 0; index-- ) - { var child = arrayValue[index]; @@ -40,9 +40,19 @@ internal class NodeValueAccessor : IValueAccessor } [MethodImpl( MethodImplOptions.AggressiveInlining )] - public JsonNode GetElementAt( in JsonNode value, int index ) + public bool TryGetElementAt( in JsonNode value, int index, out JsonNode element ) { - return value[index]; + var array = (JsonArray) value; + element = null; + + if ( index < 0 ) // flip negative index to positive + index = array.Count + index; + + if ( index < 0 || index >= array.Count ) // out of bounds + return false; + + element = value[index]; + return true; } [MethodImpl( MethodImplOptions.AggressiveInlining )] @@ -65,7 +75,7 @@ public int GetArrayLength( in JsonNode value ) return 0; } - public bool TryGetChildValue( in JsonNode value, string childSelector, out JsonNode childValue ) + public bool TryGetChildValue( in JsonNode value, string childSelector, SelectorKind selectorKind, out JsonNode childValue ) { switch ( value ) { @@ -78,8 +88,14 @@ public bool TryGetChildValue( in JsonNode value, string childSelector, out JsonN } case JsonArray valueArray: { + if ( selectorKind == SelectorKind.Name ) + break; + if ( int.TryParse( childSelector, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index ) ) { + if ( index < 0 ) // flip negative index to positive + index = valueArray.Count + index; + if ( index >= 0 && index < valueArray.Count ) { childValue = value[index]; @@ -161,4 +177,9 @@ public bool TryGetValueFromNode( JsonNode node, out object value ) return true; } + + public bool TryGetFromPointer( in JsonNode node, JsonPathSegment segment, out JsonNode childValue ) + { + return node.TryGetFromJsonPathPointer( segment, out childValue ); + } } diff --git a/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs b/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs index 5bfad4e7..7d504c9f 100644 --- a/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs +++ b/src/Hyperbee.Json/Dynamic/DynamicJsonElement.cs @@ -1,6 +1,5 @@ using System.Dynamic; using System.Text.Json; -using Hyperbee.Json.Extensions; namespace Hyperbee.Json.Dynamic; @@ -11,9 +10,9 @@ public class DynamicJsonElement : DynamicObject public static implicit operator double( DynamicJsonElement proxy ) => proxy.Value.GetDouble(); public static implicit operator decimal( DynamicJsonElement proxy ) => proxy.Value.GetDecimal(); - public static implicit operator short( DynamicJsonElement proxy ) => proxy.Value.GetNumberAsInt16(); - public static implicit operator int( DynamicJsonElement proxy ) => proxy.Value.GetNumberAsInt32(); - public static implicit operator long( DynamicJsonElement proxy ) => proxy.Value.GetNumberAsInt64(); + public static implicit operator short( DynamicJsonElement proxy ) => GetNumberAsInt16( proxy.Value ); + public static implicit operator int( DynamicJsonElement proxy ) => GetNumberAsInt32( proxy.Value ); + public static implicit operator long( DynamicJsonElement proxy ) => GetNumberAsInt64( proxy.Value ); public static implicit operator bool( DynamicJsonElement proxy ) => proxy.Value.GetBoolean(); public static implicit operator byte( DynamicJsonElement proxy ) => proxy.Value.GetByte(); public static implicit operator sbyte( DynamicJsonElement proxy ) => proxy.Value.GetSByte(); @@ -78,4 +77,30 @@ public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, result = null; return false; } + + // Value extensions + + private static short GetNumberAsInt16( JsonElement value ) + { + if ( value.TryGetInt16( out var number ) ) + return number; + + return (short) value.GetDouble(); // for cases where the number contains fractional digits + } + + private static int GetNumberAsInt32( JsonElement value ) + { + if ( value.TryGetInt32( out var number ) ) + return number; + + return (int) value.GetDouble(); // for cases where the number contains fractional digits + } + + private static long GetNumberAsInt64( JsonElement value ) + { + if ( value.TryGetInt64( out var number ) ) + return number; + + return (long) value.GetDouble(); // for cases where the number contains fractional digits + } } diff --git a/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs b/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs index 536fc29d..b5cc3319 100644 --- a/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonElementExtensions.cs @@ -1,87 +1,14 @@ -using System.Buffers; -using System.Text.Json; -using System.Text.Json.Nodes; -using Hyperbee.Json.Dynamic; +using System.Text.Json; namespace Hyperbee.Json.Extensions; public static class JsonElementExtensions { - // To operations - - public static dynamic ToDynamic( this JsonElement value, string path = null ) => new DynamicJsonElement( ref value, path ); - public static dynamic ToDynamic( this JsonDocument value ) => ToDynamic( value.RootElement, "$" ); - - public static JsonNode ToJsonNode( this JsonDocument document ) - { - return ToJsonNode( document.RootElement ); - } - - public static JsonNode ToJsonNode( this JsonElement element ) - { - return element.ValueKind switch - { - JsonValueKind.Object => JsonObject.Create( element ), - JsonValueKind.Array => JsonArray.Create( element ), - _ => JsonValue.Create( element ) - }; - } - - public static T ToObject( this JsonElement value, JsonSerializerOptions options = null ) - where T : new() - { - var bufferWriter = new ArrayBufferWriter(); - using var writer = new Utf8JsonWriter( bufferWriter ); - - value.WriteTo( writer ); - writer.Flush(); - - var reader = new Utf8JsonReader( bufferWriter.WrittenSpan ); - return JsonSerializer.Deserialize( ref reader, options ); - } - // Deep Equals/Compare extensions - public static bool DeepEquals( this JsonElement elmA, string strB, JsonDocumentOptions options = default ) - { - if ( strB == null ) - return false; - - var comparer = new JsonElementDeepEqualityComparer( options.MaxDepth ); - using var docB = JsonDocument.Parse( strB, options ); - - return comparer.Equals( elmA, docB.RootElement ); - } - public static bool DeepEquals( this JsonElement elmA, JsonElement elmB, JsonDocumentOptions options = default ) { var comparer = new JsonElementDeepEqualityComparer( options.MaxDepth ); return comparer.Equals( elmA, elmB ); } - - // Value extensions - - public static short GetNumberAsInt16( this JsonElement value ) - { - if ( value.TryGetInt16( out var number ) ) - return number; - - return (short) value.GetDouble(); // for cases where the number contains fractional digits - } - - public static int GetNumberAsInt32( this JsonElement value ) - { - if ( value.TryGetInt32( out var number ) ) - return number; - - return (int) value.GetDouble(); // for cases where the number contains fractional digits - } - - public static long GetNumberAsInt64( this JsonElement value ) - { - if ( value.TryGetInt64( out var number ) ) - return number; - - return (long) value.GetDouble(); // for cases where the number contains fractional digits - } } diff --git a/src/Hyperbee.Json/Extensions/JsonHelper.cs b/src/Hyperbee.Json/Extensions/JsonHelper.cs new file mode 100644 index 00000000..1de2809e --- /dev/null +++ b/src/Hyperbee.Json/Extensions/JsonHelper.cs @@ -0,0 +1,14 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Hyperbee.Json.Dynamic; + +namespace Hyperbee.Json.Extensions; + +public static class JsonHelper +{ + // conversion + + public static dynamic ConvertToDynamic( JsonNode value ) => new DynamicJsonNode( ref value ); + public static dynamic ConvertToDynamic( JsonElement value, string path = null ) => new DynamicJsonElement( ref value, path ); + public static dynamic ConvertToDynamic( JsonDocument value ) => ConvertToDynamic( value.RootElement, "$" ); +} diff --git a/src/Hyperbee.Json/Extensions/JsonNodeExtensions.cs b/src/Hyperbee.Json/Extensions/JsonNodeExtensions.cs index 141f3322..d77bf283 100644 --- a/src/Hyperbee.Json/Extensions/JsonNodeExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonNodeExtensions.cs @@ -1,33 +1,13 @@ -using System.Buffers; -using System.Numerics; -using System.Text.Json; +using System.Numerics; using System.Text.Json.Nodes; -using Hyperbee.Json.Dynamic; namespace Hyperbee.Json.Extensions; public static class JsonNodeExtensions { - // To operations - - public static dynamic ToDynamic( this JsonNode value ) => new DynamicJsonNode( ref value ); - - public static T ToObject( this JsonNode value, JsonSerializerOptions options = null ) - where T : new() - { - var bufferWriter = new ArrayBufferWriter(); - using var writer = new Utf8JsonWriter( bufferWriter ); - - value.WriteTo( writer ); - writer.Flush(); - - var reader = new Utf8JsonReader( bufferWriter.WrittenSpan ); - return JsonSerializer.Deserialize( ref reader, options ); - } - // Value extensions - public static T GetNumber( this JsonNode value ) + internal static T GetNumber( this JsonNode value ) where T : struct, IComparable, IFormattable, IConvertible, IComparable, IEquatable, INumber { var source = value.AsValue(); diff --git a/src/Hyperbee.Json/Extensions/JsonPathHelper.cs b/src/Hyperbee.Json/Extensions/JsonPathHelper.cs deleted file mode 100644 index d896634e..00000000 --- a/src/Hyperbee.Json/Extensions/JsonPathHelper.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text; - -namespace Hyperbee.Json.Extensions; - -public static class JsonPathHelper -{ - // conversion - - public static ReadOnlySpan NormalizePath( ReadOnlySpan path ) - { - var segments = JsonPathQueryParser.ParseNoCache( path ); - - var builder = new StringBuilder(); - - foreach ( var token in segments.AsEnumerable() ) - { - builder.Append( '[' ); - - foreach ( var selector in token.Selectors ) - { - switch ( selector.SelectorKind ) - { - case SelectorKind.Root: - builder.Append( "'$'" ); - break; - case SelectorKind.Dot: - case SelectorKind.Name: - builder.Append( $"'{selector.Value}'" ); - break; - case SelectorKind.Wildcard: - builder.Append( '*' ); - break; - case SelectorKind.Descendant: - builder.Append( ".." ); - break; - case SelectorKind.Slice: - case SelectorKind.Filter: - case SelectorKind.Index: - builder.Append( selector.Value ); - break; - - case SelectorKind.Undefined: - default: - throw new NotSupportedException( $"Unsupported {nameof( SelectorKind )}." ); - } - } - - builder.Append( ']' ); - } - - return builder.ToString(); - } -} diff --git a/src/Hyperbee.Json/Extensions/JsonPathPointerExtensions.cs b/src/Hyperbee.Json/Extensions/JsonPathPointerExtensions.cs index 7e897173..a088c5a3 100644 --- a/src/Hyperbee.Json/Extensions/JsonPathPointerExtensions.cs +++ b/src/Hyperbee.Json/Extensions/JsonPathPointerExtensions.cs @@ -4,241 +4,138 @@ namespace Hyperbee.Json.Extensions; // DISTINCT from JsonPath these extensions are intended to facilitate 'diving' for Json Properties using -// absolute singular paths. similar to JsonPointer but using JsonPath notation. +// normalized paths. a normalized path is an absolute path that references a single element. +// similar to JsonPointer but using JsonPath notation. // -// syntax supports singular paths; dotted notation, quoted names, and simple bracketed array accessors only. +// syntax supports absolute paths; dotted notation, quoted names, and simple bracketed array accessors only. // // Json path style wildcard '*', '..', and '[a,b]' multi-result selector notations are NOT supported. -// -// examples: -// prop1.prop2 -// prop1[0] -// 'prop.2' -// prop1[0].prop2 -// prop1['prop.2'] -// prop1.'prop.2'[0].prop3 public static class JsonPathPointerExtensions { public static JsonElement FromJsonPathPointer( this JsonElement jsonElement, ReadOnlySpan pointer ) { - if ( IsNullOrUndefined( jsonElement ) || pointer.IsEmpty ) - return default; - - var splitter = new JsonPathPointerSplitter( pointer ); - - while ( splitter.TryMoveNext( out var name ) ) - { - if ( jsonElement.ValueKind == JsonValueKind.Array && int.TryParse( name, out var index ) ) - { - jsonElement = jsonElement.EnumerateArray().ElementAtOrDefault( index ); - continue; - } + var query = JsonPathQueryParser.Parse( pointer ); + var segment = query.Segments.Next; // skip the root segment - jsonElement = jsonElement.TryGetProperty( name!, out var value ) ? value : default; - - if ( IsNullOrUndefined( jsonElement ) ) - return default; - } - - return jsonElement; - - static bool IsNullOrUndefined( JsonElement value ) => value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined; + return TryGetFromJsonPathPointer( jsonElement, segment, out var value ) ? value : default; } - public static JsonNode FromJsonPathPointer( this JsonNode jsonNode, ReadOnlySpan pointer ) + internal static bool TryGetFromJsonPathPointer( this JsonElement jsonElement, JsonPathSegment segment, out JsonElement value ) { - if ( jsonNode == null || pointer.IsEmpty ) - return default; + if ( !segment.IsNormalized ) + throw new NotSupportedException( "Unsupported JsonPath pointer query format." ); - var splitter = new JsonPathPointerSplitter( pointer ); + var current = jsonElement; + value = default; - while ( splitter.TryMoveNext( out var name ) ) + while ( !segment.IsFinal ) { - if ( jsonNode is JsonArray valueArray && int.TryParse( name, out var index ) ) - { - jsonNode = valueArray[index]; + var (selectorValue, selectorKind) = segment.Selectors[0]; - if ( jsonNode == null ) - return default; + switch ( selectorKind ) + { + case SelectorKind.Name: + { + if ( current.ValueKind != JsonValueKind.Object ) + return false; - continue; - } + if ( !current.TryGetProperty( selectorValue, out var child ) ) + return false; - jsonNode = jsonNode.AsObject().TryGetPropertyValue( name!.ToString(), out var value ) ? value : default; + current = child; + break; + } - if ( jsonNode == null ) - return default; - } + case SelectorKind.Index: + { + if ( current.ValueKind != JsonValueKind.Array ) + return false; - return jsonNode; - } + var length = current.GetArrayLength(); + var index = int.Parse( selectorValue ); - private ref struct JsonPathPointerSplitter //TODO Support escaping of \' and bracket counting in literals. Add to unit tests. - { - // zero allocation helper that splits a json path in to parts + if ( index < 0 ) + index = length + index; - // this splitter only works on simple property 'keys' it does not work - // with complex selectors ( '..', '*', '[a,b,c]' ). + if ( index < 0 || index >= length ) + return false; - private ReadOnlySpan _span; - private Scanner _scanner; + current = current[index]; + break; + } - private enum Scanner - { - Default, - Quoted, - Bracket, - Trailing - } + default: + throw new NotSupportedException( $"Unsupported {nameof( SelectorKind )}." ); + } - private enum SpanAction - { - ReadNext, - TruncateLeadingCharacter, - YieldIdentifier + segment = segment.Next; } - private enum BracketContent - { - Undefined, - Quoted, - Number - } + value = current; + return true; + } - internal JsonPathPointerSplitter( ReadOnlySpan span ) - { - if ( span.StartsWith( "$." ) ) - span = span[2..]; - else if ( span.StartsWith( "$" ) ) - span = span[1..]; + public static JsonNode FromJsonPathPointer( this JsonNode jsonNode, ReadOnlySpan pointer ) + { + var query = JsonPathQueryParser.Parse( pointer ); + var segment = query.Segments.Next; // skip the root segment - _span = span; - _scanner = Scanner.Default; - } + return TryGetFromJsonPathPointer( jsonNode, segment, out var value ) ? value : default; + } - private void TakeIdentifier( int i, out ReadOnlySpan identifier ) - { - identifier = i > 0 ? _span[..i].Trim( '\'' ) : default; - _span = _span[Math.Min( i + 1, _span.Length )..]; - } + public static bool TryGetFromJsonPathPointer( this JsonNode jsonNode, JsonPathSegment segment, out JsonNode value ) + { + if ( !segment.IsNormalized ) + throw new NotSupportedException( "Unsupported JsonPath pointer query format." ); - // ReSharper disable once RedundantAssignment - private void TakeLeadingCharacter( ref int i ) - { - _span = _span[1..]; - i = 0; - } + var current = jsonNode; + value = default; - public bool TryMoveNext( out ReadOnlySpan identifier ) + while ( !segment.IsFinal ) { - identifier = default; - var i = 0; + var (selectorValue, selectorKind) = segment.Selectors[0]; - var bracketContent = BracketContent.Undefined; - - do + switch ( selectorKind ) { - if ( _span.IsEmpty || i >= _span.Length ) - return false; - - var c = _span[i]; - var action = SpanAction.ReadNext; - - switch ( _scanner ) - { - case Scanner.Default: - switch ( c ) - { - case '\'': - _scanner = Scanner.Quoted; - break; - case '[': - _scanner = Scanner.Bracket; - action = SpanAction.YieldIdentifier; - break; - case '.': - action = SpanAction.YieldIdentifier; - break; - case ' ': - case '\t': - case ']': - case '$' when i > 0: - throw new JsonException( $"Invalid character '{c}' at pos {i}." ); - default: - if ( i + 1 == _span.Length ) // take if at the end - { - i++; // capture the final character - action = SpanAction.YieldIdentifier; - } - - break; - } + case SelectorKind.Name: + { + if ( current is not JsonObject jsonObject ) + return false; - break; - case Scanner.Quoted: - switch ( c ) - { - case '\'': - _scanner = Scanner.Trailing; - action = SpanAction.YieldIdentifier; - break; - } + if ( !jsonObject.TryGetPropertyValue( selectorValue, out var child ) ) + return false; + current = child; break; - case Scanner.Bracket: - switch ( c ) - { - case ']': - _scanner = Scanner.Trailing; - action = SpanAction.YieldIdentifier; - break; - case var _ when bracketContent == BracketContent.Undefined: - if ( c == '\'' ) - bracketContent = BracketContent.Quoted; - else if ( char.IsNumber( c ) ) - bracketContent = BracketContent.Number; - else - throw new JsonException( $"Invalid character '{c}' in bracket at pos {i}." ); - break; - case var _ when bracketContent == BracketContent.Number && !char.IsNumber( c ): - throw new JsonException( $"Invalid non-numeric {c}' in bracket at pos {i}." ); - } + } - break; - case Scanner.Trailing: - switch ( c ) - { - case '[': - _scanner = Scanner.Bracket; - break; - case '.': - _scanner = Scanner.Default; - break; - default: - throw new JsonException( $"Invalid character '{c}' after identifier at pos {i}." ); - } - - action = SpanAction.TruncateLeadingCharacter; - break; - } + case SelectorKind.Index: + { + if ( current is not JsonArray jsonArray ) + return false; - switch ( action ) - { - case SpanAction.ReadNext: - i++; - break; - case SpanAction.TruncateLeadingCharacter: - TakeLeadingCharacter( ref i ); - break; - case SpanAction.YieldIdentifier: - TakeIdentifier( i, out identifier ); + var length = jsonArray.Count; + var index = int.Parse( selectorValue ); + + if ( index < 0 ) + index = length + index; + + if ( index < 0 || index >= length ) + return false; + + current = jsonArray[index]; break; - } + } - } while ( identifier.IsEmpty ); + default: + throw new NotSupportedException( $"Unsupported {nameof( SelectorKind )}." ); + } - return true; + segment = segment.Next; } + + value = current; + return true; } } diff --git a/src/Hyperbee.Json/Filters/FilterEvaluator.cs b/src/Hyperbee.Json/Filters/FilterEvaluator.cs index dc089bfe..146b6c4f 100644 --- a/src/Hyperbee.Json/Filters/FilterEvaluator.cs +++ b/src/Hyperbee.Json/Filters/FilterEvaluator.cs @@ -7,7 +7,7 @@ namespace Hyperbee.Json.Filters; public sealed class FilterEvaluator : IFilterEvaluator { - private static readonly ConcurrentDictionary> Compiled = new(); + private static readonly ConcurrentDictionary, bool>> Compiled = new(); private readonly ITypeDescriptor _typeDescriptor; @@ -16,21 +16,27 @@ public FilterEvaluator( ITypeDescriptor typeDescriptor ) _typeDescriptor = typeDescriptor; } - public object Evaluate( string filter, TNode current, TNode root ) + public bool Evaluate( string filter, TNode current, TNode root ) { + // Feature: split type descriptor into design/parse and runtime. (functions and json parsing are design time) var compiled = Compiled.GetOrAdd( filter, _ => FilterParser.Compile( filter, _typeDescriptor ) ); try { - return compiled( current, root ); + var runtimeContext = new FilterRuntimeContext( current, root, _typeDescriptor ); + return compiled( runtimeContext ); } catch ( RuntimeBinderException ) { - return null; // missing members should act falsy + return false; // missing members should act falsy + } + catch ( NotSupportedException ) + { + throw; } catch ( Exception ex ) { - throw new FilterEvaluatorException( "Error compiling JsonPath expression.", ex ); + throw new FilterException( "Error compiling filter expression.", ex ); } } } diff --git a/src/Hyperbee.Json/Filters/FilterEvaluatorException.cs b/src/Hyperbee.Json/Filters/FilterEvaluatorException.cs deleted file mode 100644 index 7ba399e2..00000000 --- a/src/Hyperbee.Json/Filters/FilterEvaluatorException.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Hyperbee.Json.Filters; - -[Serializable] -public class FilterEvaluatorException : Exception -{ - public FilterEvaluatorException() - : base( "JsonPath filter evaluator exception." ) - { - } - - public FilterEvaluatorException( string message ) - : base( message ) - { - } - - public FilterEvaluatorException( string message, Exception innerException ) - : base( message, innerException ) - { - } -} diff --git a/src/Hyperbee.Json/Filters/FilterException.cs b/src/Hyperbee.Json/Filters/FilterException.cs new file mode 100644 index 00000000..97de57da --- /dev/null +++ b/src/Hyperbee.Json/Filters/FilterException.cs @@ -0,0 +1,20 @@ +namespace Hyperbee.Json.Filters; + +[Serializable] +public class FilterException : Exception +{ + public FilterException() + : base( "JsonPath filter evaluator exception." ) + { + } + + public FilterException( string message ) + : base( message ) + { + } + + public FilterException( string message, Exception innerException ) + : base( message, innerException ) + { + } +} diff --git a/src/Hyperbee.Json/Filters/IFilterEvaluator.cs b/src/Hyperbee.Json/Filters/IFilterEvaluator.cs index 15a15999..a2ca7dec 100644 --- a/src/Hyperbee.Json/Filters/IFilterEvaluator.cs +++ b/src/Hyperbee.Json/Filters/IFilterEvaluator.cs @@ -3,5 +3,5 @@ namespace Hyperbee.Json.Filters; public interface IFilterEvaluator { - public object Evaluate( string filter, TNode current, TNode root ); + public bool Evaluate( string filter, TNode current, TNode root ); } diff --git a/src/Hyperbee.Json/Filters/IRegexp.cs b/src/Hyperbee.Json/Filters/IRegexp.cs new file mode 100644 index 00000000..26c66468 --- /dev/null +++ b/src/Hyperbee.Json/Filters/IRegexp.cs @@ -0,0 +1,115 @@ +namespace Hyperbee.Json.Filters; + +public static class IRegexp +{ + public static string ConvertToIRegexp( ReadOnlySpan pattern ) + { + // RFC-9535 States that regular expressions must conform to the I-Regexp format (RFC-9485)​. + // + // This requirement impacts DotNet regex for the dot( . ) character and treatment of Surrogate Pairs. + // + // I-Regexp, addresses the expectation for the dot (.) character in regular expressions: + // The dot( . ) character should match any character except newline characters, including + // surrogate pairs, which are treated as single characters in the context of matching. + // + // Surrogate pairs are used to represent characters outside the BMP (Basic Multilingual Plane) + // in UTF-16 encoding. They consist of a high surrogate (D800-DBFF) and a low surrogate (DC00-DFFF), + // which are combined to represent a single character. DotNet does not handle surrogate pairs nicely. + // + // Further, DotNet regex does not match dot( . ) on `\r`, which is an expectation of the RFC-9535 + // compliance test suite. + // + // To address this, we need to rewrite the regex pattern to match the dot( . ) character as expected. + + // stackalloc a span to track positions for replacement + + if ( pattern.IsEmpty ) + return string.Empty; + + var patternSize = pattern.Length; + Span dotPositions = patternSize > 256 + ? new bool[patternSize] + : stackalloc bool[patternSize]; + + var inCharacterClass = false; + var dotCount = 0; + + for ( var i = 0; i < pattern.Length; i++ ) + { + var currentChar = pattern[i]; + + switch ( currentChar ) + { + case '\\': + i++; + break; + case '[': + inCharacterClass = true; + break; + case ']' when inCharacterClass: + inCharacterClass = false; + break; + case '.' when !inCharacterClass: + dotPositions[i] = true; + dotCount++; + break; + } + } + + if ( dotCount == 0 ) + return pattern.ToString(); + + /* + * Regex Rewrite Explanation: + * + * 1. Non-Capturing Group `(?: ... )` + * - The entire pattern is wrapped in a non-capturing group to group the regex parts together + * without capturing the matched text. + * + * 2. Negative Lookahead `(?! ... )` + * - `(?[\r\n])`: Match any character that is not a carriage return (`\r`) or newline (`\n`). + * + * 3. Surrogate Pair `\p{Cs}\p{Cs}` + * - `\p{Cs}`: Matches any character in the "Cs" (surrogate) Unicode category. + * - `\p{Cs}\p{Cs}`: Matches a surrogate pair, which consists of two surrogate characters in sequence. + * + * Overall Pattern: + * - The pattern matches either: + * 1. Any character that is not a newline (`\r` or `\n`), or + * 2. A surrogate pair (two surrogate characters in sequence). + * + * This ensures that the regex matches any character except newline and carriage return characters, + * while correctly handling surrogate pairs which are necessary for certain Unicode characters. + * + * Pattern: + * (?: + * (?[^\r\n]) # Match any character except \r and \n + * | + * \p{Cs}\p{Cs} # Match a surrogate pair (two surrogates in sequence) + * ) + */ + var replacement = @"(?:[^\r\n]|\p{Cs}\p{Cs})".AsSpan(); // (?:(?![\r\n])\P{Cs}|\p{Cs}\p{Cs}) + + var newSize = pattern.Length + dotCount * (replacement.Length - 1); // '.' is 1 char, so extra (pattern-length - 1) chars per '.' + Span buffer = newSize > 512 + ? new char[newSize] + : stackalloc char[newSize]; + + var bufferIndex = 0; + + for ( var i = 0; i < pattern.Length; i++ ) + { + if ( dotPositions[i] ) + { + replacement.CopyTo( buffer[bufferIndex..] ); + bufferIndex += replacement.Length; + } + else + { + buffer[bufferIndex++] = pattern[i]; + } + } + + return new string( buffer[..bufferIndex] ); + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/ExpressionInfo.cs b/src/Hyperbee.Json/Filters/Parser/ExpressionInfo.cs new file mode 100644 index 00000000..0d494ad5 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/ExpressionInfo.cs @@ -0,0 +1,7 @@ +namespace Hyperbee.Json.Filters.Parser; + +internal class ExpressionInfo +{ + public ExpressionKind Kind { get; set; } + public FilterExtensionInfo FunctionInfo { get; set; } +} diff --git a/src/Hyperbee.Json/Filters/Parser/ExpressionKind.cs b/src/Hyperbee.Json/Filters/Parser/ExpressionKind.cs new file mode 100644 index 00000000..9231d05d --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/ExpressionKind.cs @@ -0,0 +1,13 @@ +namespace Hyperbee.Json.Filters.Parser; + +internal enum ExpressionKind +{ + Unspecified, + Function, + Json, + Literal, + Not, + Paren, + Select, + Merged +} diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/ComparerExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/ComparerExpressionFactory.cs deleted file mode 100644 index e296e135..00000000 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/ComparerExpressionFactory.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System.Diagnostics; -using System.Linq.Expressions; -using Hyperbee.Json.Descriptors; - -namespace Hyperbee.Json.Filters.Parser.Expressions; - -public static class ComparerExpressionFactory -{ - // ReSharper disable once StaticMemberInGenericType - private static readonly ConstantExpression CreateComparandExpression; - - static ComparerExpressionFactory() - { - // Pre-compile the delegate to call the Comparand constructor - - var accessorParam = Expression.Parameter( typeof( IValueAccessor ), "accessor" ); - var valueParam = Expression.Parameter( typeof( object ), "value" ); - - var constructorInfo = typeof( Comparand ).GetConstructor( [typeof( IValueAccessor ), typeof( object )] ); - var newExpression = Expression.New( constructorInfo!, accessorParam, valueParam ); - - var creator = Expression.Lambda, object, Comparand>>( - newExpression, accessorParam, valueParam ).Compile(); - - CreateComparandExpression = Expression.Constant( creator ); - } - - public static Expression GetComparand( IValueAccessor accessor, Expression expression ) - { - // Handles Not operator since it maybe not have a left side. - if ( expression == null ) - return null; - - // Create an expression representing the instance of the accessor - var accessorExpression = Expression.Constant( accessor ); - - // Use the compiled delegate to create an expression to call the Comparand constructor - return Expression.Invoke( CreateComparandExpression, accessorExpression, - Expression.Convert( expression, typeof( object ) ) ); - } - - [DebuggerDisplay( "Value = {Value}" )] - public readonly struct Comparand( IValueAccessor accessor, object value ) : IComparable, IEquatable - { - private const float Tolerance = 1e-6F; // Define a tolerance for float comparisons - - private IValueAccessor Accessor { get; } = accessor; - - private object Value { get; } = value; - - public int CompareTo( Comparand other ) => Compare( this, other ); - public bool Equals( Comparand other ) => Compare( this, other ) == 0; - public override bool Equals( object obj ) => obj is Comparand other && Equals( other ); - - public static bool operator ==( Comparand left, Comparand right ) => Compare( left, right ) == 0; - public static bool operator !=( Comparand left, Comparand right ) => Compare( left, right ) != 0; - public static bool operator <( Comparand left, Comparand right ) => Compare( left, right, lessThan: true ) < 0; - public static bool operator >( Comparand left, Comparand right ) => Compare( left, right ) > 0; - public static bool operator <=( Comparand left, Comparand right ) => Compare( left, right, lessThan: true ) <= 0; - public static bool operator >=( Comparand left, Comparand right ) => Compare( left, right ) >= 0; - - public override int GetHashCode() - { - if ( Value == null ) - return 0; - - var valueHash = Value switch - { - IConvertible convertible => convertible.GetHashCode(), - IEnumerable enumerable => enumerable.GetHashCode(), - _ => Value.GetHashCode() - }; - - return HashCode.Combine( Value.GetType().GetHashCode(), valueHash ); - } - - /* - * Comparison Rules (according to JSONPath RFC 9535): - * - * 1. Compare Value to Value: - * - Two values are equal if they are of the same type and have the same value. - * - For float comparisons, use a tolerance to handle precision issues. - * - Comparisons between different types yield false. - * - * 2. Compare Node to Node: - * - Since a Node is essentially an enumerable with a single item, compare the single items directly. - * - Apply the same value comparison rules to the single items. - * - * 3. Compare NodeList to NodeList: - * - Two NodeLists are equal if they are sequence equal. - * - Sequence equality should consider deep equality of Node items. - * - Return 0 if sequences are equal. - * - Return -1 if the left sequence is less. - * - Return 1 if the left sequence is greater. - * - * 4. Compare NodeList to Value: - * - A NodeList is equal to a value if any node in the NodeList matches the value. - * - Return 0 if any node matches the value. - * - Return -1 if the value is less than all nodes. - * - Return 1 if the value is greater than all nodes. - * - * 5. Compare Value to NodeList: - * - Similar to the above, true if the value is found in the NodeList. - * - * 6. Compare Node to NodeList and vice versa: - * - Since Node is a single item enumerable, treat it similarly to Value in comparison to NodeList. - * - * 7. Truthiness Rules: - * - Falsy values: null, false, 0, "", NaN. - * - Truthy values: Anything not falsy, including non-empty strings, non-zero numbers, true, arrays, and objects. - * - Truthiness is generally not used for comparison operators (==, <) in filter expressions. - * - Type mismatches (e.g., string vs. number) result in false for equality (==) and true for inequality (!=). - * - * Order of Operations: - * - Check if both are NodeLists. - * - Check if one is a NodeList and the other is a Value. - * - Compare directly if both are Values. - */ - private static int Compare( Comparand left, Comparand right, bool lessThan = false ) - { - if ( left.Value is IEnumerable leftEnumerable && right.Value is IEnumerable rightEnumerable ) - { - return CompareEnumerables( left.Accessor, leftEnumerable, rightEnumerable ); - } - - if ( left.Value is IEnumerable leftEnumerable1 ) - { - var compare = CompareEnumerableToValue( left.Accessor, leftEnumerable1, right.Value, out var nodeCount ); - return NormalizeResult( compare, nodeCount, lessThan ); - } - - if ( right.Value is IEnumerable rightEnumerable1 ) - { - var compare = CompareEnumerableToValue( left.Accessor, rightEnumerable1, left.Value, out var nodeCount ); - return NormalizeResult( compare, nodeCount, lessThan ); - } - - return CompareValues( left.Value, right.Value ); - - static int NormalizeResult( int compare, int nodeCount, bool lessThan ) - { - // When comparing a NodeList to a Value, '<' and '>' type operators only have meaning when the - // NodeList has a single node. - // - // 1. When there is a single node, the comparison is based on the unwrapped node value. - // This results in a meaningful value to value comparison for equality, and greater-than and - // less-than operations. - // - // 2. When there is more than on node, or an empty node list, equality is based on finding the - // value in the set of nodes. The result is true if the value is found in the set, and false - // otherwise. - // - // In this case, the result is not meaningful for greater-than and less-than operations, since - // the comparison is based on the set of nodes, and not on two single values. - // - // However, the comparison result will still be used in the context of a greater-than or less-than - // operation, which will yield indeterminate results based on the left or right order of operands. - // To handle this, we need to normalize the result of the comparison. In this case, we want to - // normalize the result so that greater-than and less-than always return false, regardless of the - // left or right order of the comparands. - - if ( lessThan && nodeCount != 1 ) // Test for an empty or multi-node set - { - // invert the comparison result to make sure less-than and greater-than return false - return -compare; - } - - return compare; - } - } - - private static int CompareEnumerables( IValueAccessor accessor, IEnumerable left, IEnumerable right ) - { - using var leftEnumerator = left.GetEnumerator(); - using var rightEnumerator = right.GetEnumerator(); - - while ( leftEnumerator.MoveNext() ) - { - if ( !rightEnumerator.MoveNext() ) - return 1; // Left has more elements, so it is greater - - if ( !accessor.DeepEquals( leftEnumerator.Current, rightEnumerator.Current ) ) - return -1; // Elements are not deeply equal - } - - if ( rightEnumerator.MoveNext() ) - return -1; // Right has more elements, so left is less - - return 0; // Sequences are equal - } - - private static int CompareEnumerableToValue( IValueAccessor accessor, IEnumerable enumeration, object value, out int nodeCount ) - { - nodeCount = 0; - var lastCompare = -1; - - foreach ( var item in enumeration ) - { - nodeCount++; - - if ( !accessor.TryGetValueFromNode( item, out var itemValue ) ) - continue; // Skip if value cannot be extracted - - lastCompare = CompareValues( itemValue, value ); - - if ( lastCompare == 0 ) - return 0; // Return 0 if any node matches the value - } - - if ( nodeCount == 0 ) - return -1; // Return -1 if the list is empty (no nodes match the value) - - return nodeCount != 1 ? -1 : lastCompare; // Return the last comparison if there is only one node - } - - private static int CompareValues( object left, object right ) - { - if ( left == null && right == null ) - { - return 0; - } - - if ( left?.GetType() != right?.GetType() ) - { - return -1; - } - - if ( left is string leftString && right is string rightString ) - { - return string.Compare( leftString, rightString, StringComparison.Ordinal ); - } - - if ( left is bool leftBool && right is bool rightBool ) - { - return leftBool.CompareTo( rightBool ); - } - - if ( left is float leftFloat && right is float rightFloat ) - { - return Math.Abs( leftFloat - rightFloat ) < Tolerance ? 0 : leftFloat.CompareTo( rightFloat ); - } - - return Comparer.Default.Compare( left, right ); - } - } -} diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/FunctionExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/FunctionExpressionFactory.cs index 9b927088..8d8cfc62 100644 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/FunctionExpressionFactory.cs +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/FunctionExpressionFactory.cs @@ -4,13 +4,20 @@ namespace Hyperbee.Json.Filters.Parser.Expressions; internal class FunctionExpressionFactory : IExpressionFactory { - public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + public static bool TryGetExpression( ref ParserState state, out Expression expression, ref ExpressionInfo expressionInfo, FilterParserContext parserContext ) { - if ( context.Descriptor.Functions.TryGetCreator( state.Item.ToString(), out var functionCreator ) ) + if ( parserContext.Descriptor.Functions.TryGetCreator( state.Item.ToString(), out var functionCreator ) ) { - expression = functionCreator() - .GetExpression( ref state, context ); // will recurse for each function argument. + if ( state.TrailingWhitespace ) + throw new NotSupportedException( "Whitespace is not allowed after a function name." ); + var function = functionCreator(); + + expression = function + .GetExpression( ref state, parserContext ); // will recurse for each function argument. + + expressionInfo.Kind = ExpressionKind.Function; + expressionInfo.FunctionInfo = function.FunctionInfo; return true; } diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/IExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/IExpressionFactory.cs index 6f13ac97..f7535ad1 100644 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/IExpressionFactory.cs +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/IExpressionFactory.cs @@ -4,5 +4,5 @@ namespace Hyperbee.Json.Filters.Parser.Expressions; internal interface IExpressionFactory { - static abstract bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ); + static abstract bool TryGetExpression( ref ParserState state, out Expression expression, ref ExpressionInfo expressionInfo, FilterParserContext parserContext ); } diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/JsonExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/JsonExpressionFactory.cs index 9584ce16..846587e0 100644 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/JsonExpressionFactory.cs +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/JsonExpressionFactory.cs @@ -1,14 +1,16 @@ using System.Linq.Expressions; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Filters.Parser.Expressions; internal class JsonExpressionFactory : IExpressionFactory { - public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + public static bool TryGetExpression( ref ParserState state, out Expression expression, ref ExpressionInfo expressionInfo, FilterParserContext parserContext ) { - if ( context.Descriptor.Accessor.TryParseNode( state.Item.ToString(), out var node ) ) + if ( parserContext.Descriptor.Accessor.TryParseNode( state.Item.ToString(), out var node ) ) { - expression = Expression.Constant( new[] { node } ); + expression = Expression.Constant( new NodesType( [node], isNormalized: true ) ); + expressionInfo.Kind = ExpressionKind.Json; return true; } diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs index ac037998..b3f1c538 100644 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/LiteralExpressionFactory.cs @@ -1,40 +1,50 @@ using System.Linq.Expressions; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Filters.Parser.Expressions; internal class LiteralExpressionFactory : IExpressionFactory { - public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + public static bool TryGetExpression( ref ParserState state, out Expression expression, ref ExpressionInfo expressionInfo, FilterParserContext parserContext ) { - expression = GetLiteralExpression( state.Item, context ); - return expression != null; + expression = GetLiteralExpression( state.Item ); + + if ( expression == null ) + return false; + + expressionInfo.Kind = ExpressionKind.Literal; + return true; + } - private static ConstantExpression GetLiteralExpression( ReadOnlySpan item, FilterContext context ) + private static ConstantExpression GetLiteralExpression( ReadOnlySpan item ) { // Check for known literals (true, false, null) first if ( item.Equals( "true", StringComparison.OrdinalIgnoreCase ) ) - return Expression.Constant( true ); + return Expression.Constant( Constants.True ); if ( item.Equals( "false", StringComparison.OrdinalIgnoreCase ) ) - return Expression.Constant( false ); + return Expression.Constant( Constants.False ); if ( item.Equals( "null", StringComparison.OrdinalIgnoreCase ) ) - return Expression.Constant( null ); + return Expression.Constant( Constants.Null ); // Check for quoted strings if ( item.Length >= 2 && (item[0] == '"' && item[^1] == '"' || item[0] == '\'' && item[^1] == '\'') ) - return Expression.Constant( item[1..^1].ToString() ); // remove quotes + return Expression.Constant( new ValueType( item[1..^1].ToString() ) ); // remove quotes // Check for numbers // // The current design treats all numbers are floats since we don't // know what's in the data or the other side of the operator yet. + if ( item.Length > 0 && item[^1] == '.' ) // incomplete floating-point number. we can parse it but the RFC doesn't like it. + throw new NotSupportedException( $"Incomplete floating-point number `{item.ToString()}`" ); + return float.TryParse( item, out float result ) - ? Expression.Constant( result ) + ? Expression.Constant( new ValueType( result ) ) : null; } } diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/NotExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/NotExpressionFactory.cs index 55afe51f..72594ee5 100644 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/NotExpressionFactory.cs +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/NotExpressionFactory.cs @@ -4,9 +4,14 @@ namespace Hyperbee.Json.Filters.Parser.Expressions; internal class NotExpressionFactory : IExpressionFactory { - public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + public static bool TryGetExpression( ref ParserState state, out Expression expression, ref ExpressionInfo expressionInfo, FilterParserContext parserContext ) { expression = null; - return state.Operator == Operator.Not; + + if ( state.Operator != Operator.Not ) + return false; + + expressionInfo.Kind = ExpressionKind.Not; + return true; } } diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/ParenExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/ParenExpressionFactory.cs index b6f8de67..ea6e671d 100644 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/ParenExpressionFactory.cs +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/ParenExpressionFactory.cs @@ -4,12 +4,17 @@ namespace Hyperbee.Json.Filters.Parser.Expressions; internal class ParenExpressionFactory : IExpressionFactory { - public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + public static bool TryGetExpression( ref ParserState state, out Expression expression, ref ExpressionInfo expressionInfo, FilterParserContext parserContext ) { if ( state.Operator == Operator.OpenParen && state.Item.IsEmpty ) { - var localState = state with { Terminal = FilterParser.EndArg }; - expression = FilterParser.Parse( ref localState, context ); // will recurse. + var localState = state with + { + Terminal = FilterParser.ArgClose + }; + + expression = FilterParser.Parse( ref localState, parserContext ); // will recurse. + expressionInfo.Kind = ExpressionKind.Paren; return true; } diff --git a/src/Hyperbee.Json/Filters/Parser/Expressions/SelectExpressionFactory.cs b/src/Hyperbee.Json/Filters/Parser/Expressions/SelectExpressionFactory.cs index 195545d5..7220ee9a 100644 --- a/src/Hyperbee.Json/Filters/Parser/Expressions/SelectExpressionFactory.cs +++ b/src/Hyperbee.Json/Filters/Parser/Expressions/SelectExpressionFactory.cs @@ -1,20 +1,26 @@ using System.Linq.Expressions; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Filters.Parser.Expressions; internal class SelectExpressionFactory : IExpressionFactory { - public static bool TryGetExpression( ref ParserState state, out Expression expression, FilterContext context ) + public static bool TryGetExpression( ref ParserState state, out Expression expression, ref ExpressionInfo itemContext, FilterParserContext parserContext ) { - expression = ExpressionHelper.GetExpression( state.Item, context ); - return expression != null; + expression = ExpressionHelper.GetExpression( state.Item, state.IsArgument, parserContext ); + + if ( expression == null ) + return false; + + itemContext.Kind = ExpressionKind.Select; + return true; } static class ExpressionHelper { - private static readonly Expression SelectExpression = Expression.Constant( (Func>) Select ); + private static readonly Expression SelectExpression = Expression.Constant( (Func, INodeType>) Select ); - public static Expression GetExpression( ReadOnlySpan item, FilterContext context ) + public static Expression GetExpression( ReadOnlySpan item, bool allowDotWhitespace, FilterParserContext parserContext ) { if ( item.IsEmpty ) return null; @@ -22,17 +28,22 @@ public static Expression GetExpression( ReadOnlySpan item, FilterContext Select( TNode current, TNode root, string query ) + private static INodeType Select( string query, bool allowDotWhitespace, FilterRuntimeContext runtimeContext ) { - return JsonPath.SelectInternal( current, root, query ); + var compileQuery = JsonPathQueryParser.Parse( query, allowDotWhitespace ); + + // Current becomes root + return query[0] == '$' + ? new NodesType( JsonPath.SelectInternal( runtimeContext.Root, runtimeContext.Root, compileQuery ), compileQuery.Normalized ) + : new NodesType( JsonPath.SelectInternal( runtimeContext.Current, runtimeContext.Root, compileQuery ), compileQuery.Normalized ); } + } } diff --git a/src/Hyperbee.Json/Filters/Parser/FilterContext.cs b/src/Hyperbee.Json/Filters/Parser/FilterContext.cs deleted file mode 100644 index e7d4ca8f..00000000 --- a/src/Hyperbee.Json/Filters/Parser/FilterContext.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Linq.Expressions; -using Hyperbee.Json.Descriptors; - -namespace Hyperbee.Json.Filters.Parser; - -internal record FilterContext -{ - public FilterContext( ITypeDescriptor descriptor ) - { - Descriptor = descriptor; - } - - public ParameterExpression Current { get; init; } = Expression.Parameter( typeof( TNode ), "current" ); - public ParameterExpression Root { get; } = Expression.Parameter( typeof( TNode ), "root" ); - public ITypeDescriptor Descriptor { get; } -} diff --git a/src/Hyperbee.Json/Filters/Parser/FilterExtensionFunction.cs b/src/Hyperbee.Json/Filters/Parser/FilterExtensionFunction.cs index ab549e01..51efee57 100644 --- a/src/Hyperbee.Json/Filters/Parser/FilterExtensionFunction.cs +++ b/src/Hyperbee.Json/Filters/Parser/FilterExtensionFunction.cs @@ -1,21 +1,29 @@ using System.Linq.Expressions; +using System.Reflection; +using Hyperbee.Json.Filters.Values; namespace Hyperbee.Json.Filters.Parser; public abstract class FilterExtensionFunction { private readonly int _argumentCount; + private readonly MethodInfo _methodInfo; + private MethodInfo _throwIfNotNormalizedMethodInfo; - protected FilterExtensionFunction( int argumentCount ) + public FilterExtensionInfo FunctionInfo { get; } + + protected FilterExtensionFunction( MethodInfo methodInfo, FilterExtensionInfo filterInfo ) { - _argumentCount = argumentCount; - } + _argumentCount = methodInfo.GetParameters().Length; + _methodInfo = methodInfo; - protected abstract Expression GetExtensionExpression( Expression[] arguments ); + FunctionInfo = filterInfo; + } - internal Expression GetExpression( ref ParserState state, FilterContext context ) + internal Expression GetExpression( ref ParserState state, FilterParserContext parserContext ) { var arguments = new Expression[_argumentCount]; + var expectNormalized = FunctionInfo.HasFlag( FilterExtensionInfo.ExpectNormalized ); for ( var i = 0; i < _argumentCount; i++ ) { @@ -23,16 +31,42 @@ internal Expression GetExpression( ref ParserState state, FilterContext.Parse( ref localState, context ); + if ( localState.EndOfBuffer ) + throw new NotSupportedException( $"Invalid arguments for filter: \"{state.Buffer}\"." ); + + var argument = FilterParser.Parse( ref localState, parserContext ); - arguments[i] = argument; + // Create expression that throws if not normalized. + if ( expectNormalized ) + { + _throwIfNotNormalizedMethodInfo ??= GetMethod( nameof( ThrowIfNotNormalized ) ) + .MakeGenericMethod( typeof( TNode ) ); + + arguments[i] = Expression.Call( _throwIfNotNormalizedMethodInfo, + Expression.Constant( _methodInfo.Name ), + Expression.Convert( argument, typeof( INodeType ) ) ); + } + else + { + arguments[i] = Expression.Convert( argument, typeof( INodeType ) ); + } } - return GetExtensionExpression( arguments ); + return Expression.Call( _methodInfo, arguments ); } -} + public static INodeType ThrowIfNotNormalized( string methodName, INodeType node ) + { + if ( node is NodesType { IsNormalized: false } ) + throw new NotSupportedException( $"Function {methodName} does not support non-singular arguments." ); + + return node; + } + + protected static MethodInfo GetMethod( string methodName ) => typeof( T ).GetMethod( methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ); +} diff --git a/src/Hyperbee.Json/Filters/Parser/FilterExtensionInfo.cs b/src/Hyperbee.Json/Filters/Parser/FilterExtensionInfo.cs new file mode 100644 index 00000000..394a6f9e --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/FilterExtensionInfo.cs @@ -0,0 +1,9 @@ +namespace Hyperbee.Json.Filters.Parser; + +[Flags] +public enum FilterExtensionInfo +{ + MustCompare = 0x01, + MustNotCompare = 0x02, + ExpectNormalized = 0x10, +} diff --git a/src/Hyperbee.Json/Filters/Parser/FilterParser.cs b/src/Hyperbee.Json/Filters/Parser/FilterParser.cs index 246b2c5a..4e0c0ba8 100644 --- a/src/Hyperbee.Json/Filters/Parser/FilterParser.cs +++ b/src/Hyperbee.Json/Filters/Parser/FilterParser.cs @@ -9,8 +9,8 @@ #endregion +using System.Diagnostics; using System.Linq.Expressions; -using System.Reflection; using Hyperbee.Json.Descriptors; using Hyperbee.Json.Filters.Parser.Expressions; @@ -18,40 +18,43 @@ namespace Hyperbee.Json.Filters.Parser; public abstract class FilterParser { - public const char EndLine = '\n'; - public const char EndArg = ')'; - public const char ArgSeparator = ','; + public const char EndLine = '\0'; // use null instead of newline + public const char ArgClose = ')'; + public const char ArgComma = ','; } public class FilterParser : FilterParser { - public static Func Compile( ReadOnlySpan filter, ITypeDescriptor descriptor ) + public static Func, bool> Compile( ReadOnlySpan filter, ITypeDescriptor descriptor ) { - var context = new FilterContext( descriptor ); + var context = new FilterParserContext( descriptor ); var expression = Parse( filter, context ); - return Expression.Lambda>( expression, context.Current, context.Root ).Compile(); + return Expression.Lambda, bool>>( expression, context.RuntimeContext ).Compile(); } - internal static Expression Parse( ReadOnlySpan filter, FilterContext context ) + internal static Expression Parse( ReadOnlySpan filter, FilterParserContext parserContext ) { + filter = filter.Trim(); // remove leading and trailing whitespace to simplify parsing + var pos = 0; - var state = new ParserState( filter, [], ref pos, Operator.Nop, EndLine ); + var parenDepth = 0; + var state = new ParserState( filter, [], ref pos, ref parenDepth, Operator.NonOperator, EndLine ); - var expression = Parse( ref state, context ); + var expression = Parse( ref state, parserContext ); return FilterTruthyExpression.IsTruthyExpression( expression ); } - internal static Expression Parse( ref ParserState state, FilterContext context ) // recursion entrypoint + internal static Expression Parse( ref ParserState state, FilterParserContext parserContext ) // recursion entrypoint { // validate input - if ( context == null ) - throw new ArgumentNullException( nameof( context ) ); + if ( parserContext == null ) + throw new ArgumentNullException( nameof( parserContext ) ); - if ( state.EndOfBuffer || state.IsTerminal ) - throw new ArgumentException( $"Invalid filter: \"{state.Buffer}\"", nameof( state ) ); + if ( state.EndOfBuffer ) + throw new NotSupportedException( $"Invalid filter: \"{state.Buffer}\"." ); // parse the expression var items = new List(); @@ -59,52 +62,55 @@ internal static Expression Parse( ref ParserState state, FilterContext co do { MoveNext( ref state ); - items.Add( GetExprItem( ref state, context ) ); + items.Add( GetExprItem( ref state, parserContext ) ); // will recurse for nested expressions } while ( state.IsParsing ); - // advance to next character for recursive calls. - if ( !state.EndOfBuffer && state.IsTerminal ) - state.Pos++; + // check for paren mismatch + if ( state.EndOfBuffer && state.ParenDepth != 0 ) + throw new NotSupportedException( $"Unbalanced parenthesis in filter: \"{state.Buffer}\"." ); // merge the expressions var baseItem = items[0]; var index = 1; - return Merge( baseItem, ref index, items, context.Descriptor ); + return Merge( in state, baseItem, ref index, items, parserContext ); } - private static ExprItem GetExprItem( ref ParserState state, FilterContext context ) + + private static ExprItem GetExprItem( ref ParserState state, FilterParserContext parserContext ) { - if ( NotExpressionFactory.TryGetExpression( ref state, out var expression, context ) ) - return ExprItem( ref state, expression ); + var expressionInfo = new ExpressionInfo(); + + if ( NotExpressionFactory.TryGetExpression( ref state, out var expression, ref expressionInfo, parserContext ) ) + return ExprItem( ref state, expression, expressionInfo ); - if ( ParenExpressionFactory.TryGetExpression( ref state, out expression, context ) ) // will recurse. - return ExprItem( ref state, expression ); + if ( ParenExpressionFactory.TryGetExpression( ref state, out expression, ref expressionInfo, parserContext ) ) // will recurse. + return ExprItem( ref state, expression, expressionInfo ); - if ( SelectExpressionFactory.TryGetExpression( ref state, out expression, context ) ) - return ExprItem( ref state, expression ); + if ( SelectExpressionFactory.TryGetExpression( ref state, out expression, ref expressionInfo, parserContext ) ) + return ExprItem( ref state, expression, expressionInfo ); - if ( FunctionExpressionFactory.TryGetExpression( ref state, out expression, context ) ) // may recurse for each function argument. - return ExprItem( ref state, expression ); + if ( FunctionExpressionFactory.TryGetExpression( ref state, out expression, ref expressionInfo, parserContext ) ) // may recurse for each function argument. + return ExprItem( ref state, expression, expressionInfo ); - if ( LiteralExpressionFactory.TryGetExpression( ref state, out expression, context ) ) - return ExprItem( ref state, expression ); + if ( LiteralExpressionFactory.TryGetExpression( ref state, out expression, ref expressionInfo, parserContext ) ) + return ExprItem( ref state, expression, expressionInfo ); - if ( JsonExpressionFactory.TryGetExpression( ref state, out expression, context ) ) - return ExprItem( ref state, expression ); + if ( JsonExpressionFactory.TryGetExpression( ref state, out expression, ref expressionInfo, parserContext ) ) + return ExprItem( ref state, expression, expressionInfo ); throw new NotSupportedException( $"Unsupported literal: {state.Buffer.ToString()}" ); // Helper method to create an expression item - static ExprItem ExprItem( ref ParserState state, Expression expression ) + static ExprItem ExprItem( ref ParserState state, Expression expression, ExpressionInfo expressionInfo ) { - UpdateOperator( ref state ); - return new ExprItem( expression, state.Operator ); + MoveNextOperator( ref state ); // will set state.Operator + return new ExprItem( expression, state.Operator, expressionInfo ); } } - private static void MoveNext( ref ParserState state ) + private static void MoveNext( ref ParserState state ) // move to the next item { char? quote = null; @@ -115,7 +121,7 @@ private static void MoveNext( ref ParserState state ) // check for end of buffer if ( state.EndOfBuffer ) { - state.Operator = Operator.Nop; + state.Operator = Operator.NonOperator; state.Item = []; return; } @@ -126,37 +132,59 @@ private static void MoveNext( ref ParserState state ) while ( true ) { - itemEnd = state.Pos; // assign before the call to NextCharacter + itemEnd = state.Pos; // store before calling NextCharacter - NextCharacter( ref state, out var nextChar, ref quote ); + NextCharacter( ref state, out var nextChar, ref quote ); // will advance state.Pos - if ( IsFinished( state.Pos - itemStart, nextChar, state.Operator, state.Terminal ) ) + if ( IsFinished( in state, nextChar ) ) + { break; + } - if ( !state.EndOfBuffer && !state.IsTerminal ) - continue; - - itemEnd = state.Pos; // fall-through: include the terminal character - break; + if ( state.EndOfBuffer ) + { + itemEnd = state.Pos; // include the final character + break; + } } state.SetItem( itemStart, itemEnd ); + return; // Helper method to determine if item parsing is finished - static bool IsFinished( int count, char ch, Operator op, char terminal ) + static bool IsFinished( in ParserState state, char ch ) { - // order of operations matters here - if ( count == 0 && ch == EndArg ) + // order of operations matters + + if ( state.BracketDepth != 0 ) return false; - if ( op != Operator.Nop && op != Operator.ClosedParen ) + if ( state.Operator.IsNonOperator() == false ) return true; - if ( ch == terminal || ch == EndArg || ch == EndLine ) - return true; + return ch == state.Terminal; // terminal character [ '\0' or ',' or ')' ] + } + } + + private static void MoveNextOperator( ref ParserState state ) // move to the next operator + { + if ( state.Operator.IsLogical() || state.Operator.IsComparison() ) + { + return; + } + + if ( !state.IsParsing ) + { + state.Operator = Operator.NonOperator; + return; + } + + char? quoteChar = null; - return false; + while ( !(state.Operator.IsLogical() || state.Operator.IsComparison()) && !state.EndOfBuffer ) + { + NextCharacter( ref state, out _, ref quoteChar ); } } @@ -164,6 +192,21 @@ private static void NextCharacter( ref ParserState state, out char nextChar, ref { nextChar = state.Buffer[state.Pos++]; + // Handle escape characters within quotes + if ( quoteChar.HasValue ) + { + if ( nextChar == '\\' && state.Pos < state.Buffer.Length ) + { + nextChar = state.Buffer[state.Pos++]; + } + else if ( nextChar == quoteChar && (state.Pos <= 1 || state.Buffer[state.Pos - 2] != '\\') ) + { + quoteChar = null; // Exiting a quoted string + } + return; + } + + // Normal character handling switch ( nextChar ) { case '&' when Next( ref state, '&' ): @@ -194,20 +237,30 @@ private static void NextCharacter( ref ParserState state, out char nextChar, ref state.Operator = Operator.Not; break; case '(': + state.ParenDepth++; state.Operator = Operator.OpenParen; break; case ')': + state.ParenDepth--; state.Operator = Operator.ClosedParen; break; - case ' ' or '\t' when quoteChar == null: - state.Operator = Operator.Nop; + case ' ' or '\t' or '\r' or '\n': + state.Operator = Operator.Whitespace; + break; + case '[': + state.BracketDepth++; + state.Operator = Operator.Bracket; break; - case '\'' or '\"' when state.Pos > 0 && state.Previous != '\\': - quoteChar = quoteChar == null ? nextChar : null; - state.Operator = Operator.Nop; + case ']': + state.BracketDepth--; + state.Operator = Operator.Bracket; + break; + case '\'' or '\"': + quoteChar = nextChar; // Entering a quoted string + state.Operator = Operator.Quotes; break; default: - state.Operator = Operator.Nop; + state.Operator = Operator.Token; break; } @@ -216,7 +269,7 @@ private static void NextCharacter( ref ParserState state, out char nextChar, ref // Helper method to check if the next character is the expected character static bool Next( ref ParserState state, char expected ) { - if ( state.EndOfBuffer || state.Current != expected ) + if ( state.EndOfBuffer || state.Buffer[state.Pos] != expected ) return false; state.Pos++; @@ -224,167 +277,132 @@ static bool Next( ref ParserState state, char expected ) } } - private static void UpdateOperator( ref ParserState state ) + private static Expression Merge( in ParserState state, ExprItem left, ref int index, List items, FilterParserContext parserContext, bool mergeOneOnly = false ) { - if ( !IsParenOrNop( state.Operator ) ) - return; - - if ( state.EndOfBuffer ) - { - state.Operator = Operator.Nop; - return; - } - - if ( state.IsTerminal ) + if ( items.Count == 1 ) { - state.Operator = Operator.ClosedParen; - return; + ThrowIfInvalidComparison( in state, left, null ); // single item, no recursion } - - char? quoteChar = null; - var startPos = state.Pos; - - while ( IsParenOrNop( state.Operator ) && !state.EndOfBuffer ) + else { - NextCharacter( ref state, out _, ref quoteChar ); - } + while ( index < items.Count ) + { + var right = items[index++]; - if ( IsParen( state.Operator ) && state.Pos > startPos ) - { - state.Pos--; - } + while ( !CanMergeItems( left, right ) ) + { + Merge( in state, right, ref index, items, parserContext, mergeOneOnly: true ); // recursive call - right becomes left + } - return; + ThrowIfInvalidComparison( in state, left, right ); - // Helper method to determine if an operator is a parenthesis or a no-op - static bool IsParenOrNop( Operator op ) => op is Operator.OpenParen or Operator.ClosedParen or Operator.Nop; - static bool IsParen( Operator op ) => op is Operator.OpenParen or Operator.ClosedParen; - } + MergeItems( left, right, parserContext ); - private static Expression Merge( ExprItem current, ref int index, List items, ITypeDescriptor descriptor, bool mergeOneOnly = false ) - { - while ( index < items.Count ) - { - var next = items[index++]; - - while ( !CanMergeItems( current, next ) ) - { - Merge( next, ref index, items, descriptor, mergeOneOnly: true ); // recursive call + if ( mergeOneOnly ) + return left.Expression; } - - MergeItems( current, next, descriptor ); - - if ( mergeOneOnly ) - return current.Expression; } - return current.Expression; + return left.Expression; // Helper method to determine if two items can be merged static bool CanMergeItems( ExprItem left, ExprItem right ) { // "Not" can never be a right side operator - return right.Operator != Operator.Not && GetPriority( left.Operator ) >= GetPriority( right.Operator ); + return right.Operator != Operator.Not && GetPrecedence( left.Operator ) >= GetPrecedence( right.Operator ); } // Helper method to get the priority of an operator - static int GetPriority( Operator type ) + static int GetPrecedence( Operator type ) { - return type switch + return type switch // higher number means greater precedence { Operator.Not => 1, - Operator.And or - Operator.Or => 2, + Operator.Or => 2, + Operator.And => 3, Operator.Equals or Operator.NotEquals or Operator.GreaterThan or Operator.GreaterThanOrEqual or Operator.LessThan or - Operator.LessThanOrEqual => 3, + Operator.LessThanOrEqual => 4, _ => 0, }; } } - private static void MergeItems( ExprItem left, ExprItem right, ITypeDescriptor descriptor ) + private static void MergeItems( ExprItem left, ExprItem right, FilterParserContext parserContext ) { - switch ( left.Operator ) + left.Expression = NodeTypeComparerBinderExpression.BindComparerExpression( parserContext, left.Expression ); + right.Expression = NodeTypeComparerBinderExpression.BindComparerExpression( parserContext, right.Expression ); + + left.Expression = left.Operator switch { - case Operator.Equals: - left.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, left.Expression ); - right.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, right.Expression ); + Operator.Equals => NodeTypeExpression.Equal( left.Expression, right.Expression ), + Operator.NotEquals => NodeTypeExpression.NotEqual( left.Expression, right.Expression ), + Operator.GreaterThan => NodeTypeExpression.GreaterThan( left.Expression, right.Expression ), + Operator.GreaterThanOrEqual => NodeTypeExpression.GreaterThanOrEqual( left.Expression, right.Expression ), + Operator.LessThan => NodeTypeExpression.LessThan( left.Expression, right.Expression ), + Operator.LessThanOrEqual => NodeTypeExpression.LessThanOrEqual( left.Expression, right.Expression ), + Operator.And => NodeTypeExpression.And( left.Expression, right.Expression ), + Operator.Or => NodeTypeExpression.Or( left.Expression, right.Expression ), + Operator.Not => NodeTypeExpression.Not( right.Expression ), + _ => throw new InvalidOperationException( $"Invalid operator {left.Operator}" ) + }; + + left.Expression = FilterTruthyExpression.ConvertBoolToValueTypeExpression( left.Expression ); - left.Expression = Expression.Equal( left.Expression, right.Expression ); - break; - case Operator.NotEquals: - left.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, left.Expression ); - right.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, right.Expression ); + left.Operator = right.Operator; + left.ExpressionInfo.Kind = ExpressionKind.Merged; + } - left.Expression = Expression.NotEqual( left.Expression, right.Expression ); - break; - case Operator.GreaterThan: - left.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, left.Expression ); - right.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, right.Expression ); + // Throw helpers - left.Expression = Expression.GreaterThan( left.Expression, right.Expression ); - break; - case Operator.GreaterThanOrEqual: - left.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, left.Expression ); - right.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, right.Expression ); + private static void ThrowIfInvalidComparison( in ParserState state, ExprItem left, ExprItem right ) + { + ThrowIfConstantIsNotCompared( in state, left, right ); + ThrowIfFunctionInvalidCompare( in state, left ); + } - left.Expression = Expression.GreaterThanOrEqual( left.Expression, right.Expression ); - break; - case Operator.LessThan: - left.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, left.Expression ); - right.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, right.Expression ); + private static void ThrowIfFunctionInvalidCompare( in ParserState state, ExprItem item ) + { + if ( state.IsArgument ) + return; - left.Expression = Expression.LessThan( left.Expression, right.Expression ); - break; - case Operator.LessThanOrEqual: - left.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, left.Expression ); - right.Expression = ComparerExpressionFactory.GetComparand( descriptor.Accessor, right.Expression ); + if ( item.ExpressionInfo.Kind != ExpressionKind.Function ) + return; - left.Expression = Expression.LessThanOrEqual( left.Expression, right.Expression ); - break; - case Operator.And: - left.Expression = Expression.AndAlso( - FilterTruthyExpression.IsTruthyExpression( left.Expression! ), - FilterTruthyExpression.IsTruthyExpression( right.Expression ) - ); - break; - case Operator.Or: - left.Expression = Expression.OrElse( - FilterTruthyExpression.IsTruthyExpression( left.Expression! ), - FilterTruthyExpression.IsTruthyExpression( right.Expression ) - ); - break; - case Operator.Not: - left.Expression = Expression.Not( - FilterTruthyExpression.IsTruthyExpression( right.Expression ) - ); - break; - case Operator.Nop: - case Operator.OpenParen: - case Operator.ClosedParen: - default: - left.Expression = left.Expression; - break; + if ( (item.ExpressionInfo.FunctionInfo & FilterExtensionInfo.MustCompare) == FilterExtensionInfo.MustCompare && + !item.Operator.IsComparison() ) + { + throw new NotSupportedException( $"Function must compare: {state.Buffer.ToString()}." ); } - // Wrap left expression in a try-catch block to handle exceptions - left.Expression = left.Expression == null - ? left.Expression - : Expression.TryCatch( - left.Expression, - Expression.Catch( typeof( NotSupportedException ), Expression.Rethrow( left.Expression.Type ) ), - Expression.Catch( typeof( Exception ), Expression.Constant( false ) ) - ); + if ( (item.ExpressionInfo.FunctionInfo & FilterExtensionInfo.MustNotCompare) == FilterExtensionInfo.MustNotCompare && + item.Operator.IsComparison() ) + { + throw new NotSupportedException( $"Function must not compare: {state.Buffer.ToString()}." ); + } + } - left.Operator = right.Operator; + private static void ThrowIfConstantIsNotCompared( in ParserState state, ExprItem left, ExprItem right ) + { + if ( state.IsArgument ) + return; + + if ( left.ExpressionInfo.Kind == ExpressionKind.Literal && !left.Operator.IsComparison() ) + throw new NotSupportedException( $"Unsupported literal without comparison: {state.Buffer.ToString()}." ); + + if ( right != null && right.ExpressionInfo.Kind == ExpressionKind.Literal && !left.Operator.IsComparison() ) + throw new NotSupportedException( $"Unsupported literal without comparison: {state.Buffer.ToString()}." ); } - private class ExprItem( Expression expression, Operator op ) + // ExprItem + + [DebuggerDisplay( "Operator = {Operator}, {ExpressionInfo.Kind}" )] + private class ExprItem( Expression expression, Operator op, ExpressionInfo expressionInfo ) { + public ExpressionInfo ExpressionInfo { get; } = expressionInfo; public Expression Expression { get; set; } = expression; public Operator Operator { get; set; } = op; } diff --git a/src/Hyperbee.Json/Filters/Parser/FilterParserContext.cs b/src/Hyperbee.Json/Filters/Parser/FilterParserContext.cs new file mode 100644 index 00000000..286cdb58 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/FilterParserContext.cs @@ -0,0 +1,12 @@ +using System.Linq.Expressions; +using Hyperbee.Json.Descriptors; + +namespace Hyperbee.Json.Filters.Parser; + +internal record FilterParserContext( ITypeDescriptor Descriptor ) +{ + public ParameterExpression RuntimeContext { get; init; } = Expression.Parameter( typeof( FilterRuntimeContext ), "runtimeContext" ); +} + + +public record FilterRuntimeContext( TNode Current, TNode Root, ITypeDescriptor Descriptor ); diff --git a/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs b/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs index 778159a0..1e2820d7 100644 --- a/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs +++ b/src/Hyperbee.Json/Filters/Parser/FilterTruthyExpression.cs @@ -1,30 +1,41 @@ using System.Collections; using System.Linq.Expressions; using System.Reflection; +using Hyperbee.Json.Filters.Values; -namespace Hyperbee.Json.Filters.Parser +namespace Hyperbee.Json.Filters.Parser; + +public static class FilterTruthyExpression { - public static class FilterTruthyExpression - { - private static readonly MethodInfo IsTruthyMethodInfo = typeof( FilterTruthyExpression ).GetMethod( nameof( IsTruthy ) ); + private static readonly MethodInfo IsTruthyMethodInfo = typeof( FilterTruthyExpression ).GetMethod( nameof( IsTruthy ) ); + private static readonly MethodInfo ConvertBoolToValueTypeMethodInfo = typeof( FilterTruthyExpression ).GetMethod( nameof( ConvertBoolToValueType ) ); - public static Expression IsTruthyExpression( Expression expression ) => - expression.Type == typeof( bool ) - ? expression - : Expression.Call( IsTruthyMethodInfo, expression ); + public static Expression IsTruthyExpression( Expression expression ) => + expression.Type == typeof( bool ) + ? expression + : Expression.Call( IsTruthyMethodInfo, expression ); - public static bool IsTruthy( object value ) + public static Expression ConvertBoolToValueTypeExpression( Expression expression ) => + Expression.Call( ConvertBoolToValueTypeMethodInfo, expression ); + + public static bool IsTruthy( object value ) + { + var truthy = value switch { - return value switch - { - null => false, - bool boolValue => boolValue, - string str => !string.IsNullOrEmpty( str ) && !str.Equals( "false", StringComparison.OrdinalIgnoreCase ), - Array array => array.Length > 0, - IEnumerable enumerable => enumerable.Cast().Any(), - IConvertible convertible => Convert.ToBoolean( convertible ), - _ => true - }; - } + Nothing => false, + Null => false, + ValueType valueBool => valueBool.Value, + ValueType floatValue => floatValue.Value != 0, + ValueType valueString => !string.IsNullOrEmpty( valueString.Value ) && !valueString.Value.Equals( "false", StringComparison.OrdinalIgnoreCase ), + IEnumerable enumerable => enumerable.Cast().Any(), // NodesType + _ => true + }; + + return truthy; + } + + public static INodeType ConvertBoolToValueType( bool value ) + { + return value ? Constants.True : Constants.False; } } diff --git a/src/Hyperbee.Json/Filters/Parser/NodeTypeComparerBinderExpression.cs b/src/Hyperbee.Json/Filters/Parser/NodeTypeComparerBinderExpression.cs new file mode 100644 index 00000000..b6ca6328 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/NodeTypeComparerBinderExpression.cs @@ -0,0 +1,25 @@ +using System.Linq.Expressions; +using Hyperbee.Json.Filters.Values; + +namespace Hyperbee.Json.Filters.Parser; + +public static class NodeTypeComparerBinderExpression +{ + private static readonly Expression BindComparerExpressionConst = Expression.Constant( (Func, INodeType, INodeType>) BindComparer ); + internal static Expression BindComparerExpression( FilterParserContext parserContext, Expression expression ) + { + if ( expression == null ) + return null; + + var parserContextExp = Expression.Constant( parserContext ); + + return Expression.Invoke( BindComparerExpressionConst, parserContextExp, + Expression.Convert( expression, typeof( INodeType ) ) ); + } + + internal static INodeType BindComparer( FilterParserContext parserContext, INodeType item ) + { + item.Comparer = parserContext.Descriptor.Comparer; + return item; + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/NodeTypeExpression.cs b/src/Hyperbee.Json/Filters/Parser/NodeTypeExpression.cs new file mode 100644 index 00000000..8b969121 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Parser/NodeTypeExpression.cs @@ -0,0 +1,64 @@ +using System.Linq.Expressions; +using System.Reflection; +using Hyperbee.Json.Filters.Values; + +namespace Hyperbee.Json.Filters.Parser; + +public static class NodeTypeExpression +{ + private static readonly MethodInfo AreEqualMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( AreEqual ) ); + private static readonly MethodInfo AreNotEqualMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( AreNotEqual ) ); + private static readonly MethodInfo IsLessThanMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( IsLessThan ) ); + private static readonly MethodInfo IsLessThanOrEqualMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( IsLessThanOrEqual ) ); + private static readonly MethodInfo IsGreaterThanMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( IsGreaterThan ) ); + private static readonly MethodInfo IsGreaterThanOrEqualMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( IsGreaterThanOrEqual ) ); + + private static readonly MethodInfo AndAlsoMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( AndAlso ) ); + private static readonly MethodInfo OrElseMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( OrElse ) ); + private static readonly MethodInfo NotMethodInfo = typeof( NodeTypeExpression ).GetMethod( nameof( NotBoolean ) ); + + public static Expression Equal( Expression left, Expression right ) => Expression.Call( AreEqualMethodInfo, left, right ); + public static Expression NotEqual( Expression left, Expression right ) => Expression.Call( AreNotEqualMethodInfo, left, right ); + public static Expression LessThan( Expression left, Expression right ) => Expression.Call( IsLessThanMethodInfo, left, right ); + public static Expression LessThanOrEqual( Expression left, Expression right ) => Expression.Call( IsLessThanOrEqualMethodInfo, left, right ); + public static Expression GreaterThan( Expression left, Expression right ) => Expression.Call( IsGreaterThanMethodInfo, left, right ); + public static Expression GreaterThanOrEqual( Expression left, Expression right ) => Expression.Call( IsGreaterThanOrEqualMethodInfo, left, right ); + + // Binary operators + public static Expression And( Expression left, Expression right ) => Expression.Call( AndAlsoMethodInfo, left, right ); + public static Expression Or( Expression left, Expression right ) => Expression.Call( OrElseMethodInfo, left, right ); + public static Expression Not( Expression expression ) => Expression.Call( NotMethodInfo, expression ); + + public static bool AreEqual( INodeType left, INodeType right ) => left.Comparer.Compare( left, right, Operator.Equals ) == 0; + public static bool AreNotEqual( INodeType left, INodeType right ) => left.Comparer.Compare( left, right, Operator.NotEquals ) != 0; + public static bool IsLessThan( INodeType left, INodeType right ) => left.Comparer.Compare( left, right, Operator.LessThan ) < 0; + public static bool IsLessThanOrEqual( INodeType left, INodeType right ) => left.Comparer.Compare( left, right, Operator.LessThanOrEqual ) <= 0; + public static bool IsGreaterThan( INodeType left, INodeType right ) => left.Comparer.Compare( left, right, Operator.GreaterThan ) > 0; + public static bool IsGreaterThanOrEqual( INodeType left, INodeType right ) => left.Comparer.Compare( left, right, Operator.GreaterThanOrEqual ) >= 0; + + public static bool AndAlso( INodeType left, INodeType right ) + { + if ( left is ValueType leftBoolValue && right is ValueType rightBoolValue ) + return leftBoolValue.Value && rightBoolValue.Value; + + return left.Comparer.Exists( left ) && + right.Comparer.Exists( right ); + } + + public static bool OrElse( INodeType left, INodeType right ) + { + if ( left is ValueType leftBoolValue && right is ValueType rightBoolValue ) + return leftBoolValue.Value || rightBoolValue.Value; + + return left.Comparer.Exists( left ) || + right.Comparer.Exists( right ); + } + + public static bool NotBoolean( INodeType value ) + { + if ( value is ValueType { Value: false } ) + return true; + + return !value.Comparer.Exists( value ); + } +} diff --git a/src/Hyperbee.Json/Filters/Parser/Operator.cs b/src/Hyperbee.Json/Filters/Parser/Operator.cs index 14164f09..acbbea67 100644 --- a/src/Hyperbee.Json/Filters/Parser/Operator.cs +++ b/src/Hyperbee.Json/Filters/Parser/Operator.cs @@ -1,17 +1,54 @@ namespace Hyperbee.Json.Filters.Parser; +[Flags] public enum Operator { - Nop = 0, // used to represent an unassigned token - OpenParen, - ClosedParen, - Not, - Equals, - NotEquals, - LessThan, - LessThanOrEqual, - GreaterThan, - GreaterThanOrEqual, - Or, - And + None = 0x0, + + // Flags + NonOperator = 0x1, // 0001 + Comparison = 0x2, // 0010 + Logical = 0x4, // 0100 + Parenthesis = 0x8, // 1000 + + // Parenthesis Operators + OpenParen = 0x10 | Parenthesis, + ClosedParen = 0x20 | Parenthesis, + + // Logical Operators + Not = 0x30 | Logical, + Or = 0x40 | Logical, + And = 0x50 | Logical, + + // Comparison Operators + Equals = 0x60 | Comparison, + NotEquals = 0x70 | Comparison, + LessThan = 0x80 | Comparison, + LessThanOrEqual = 0x90 | Comparison, + GreaterThan = 0xA0 | Comparison, + GreaterThanOrEqual = 0xB0 | Comparison, + + // Specific non-operators + Whitespace = 0xC0 | NonOperator, + Quotes = 0xD0 | NonOperator, + Token = 0xE0 | NonOperator, + Bracket = 0xF0 | NonOperator, +} + +public static class OperatorExtensions +{ + public static bool IsNonOperator( this Operator op ) + { + return (op & Operator.NonOperator) == Operator.NonOperator; + } + + public static bool IsComparison( this Operator op ) + { + return (op & Operator.Comparison) == Operator.Comparison; + } + + public static bool IsLogical( this Operator op ) + { + return (op & Operator.Logical) == Operator.Logical; + } } diff --git a/src/Hyperbee.Json/Filters/Parser/ParserState.cs b/src/Hyperbee.Json/Filters/Parser/ParserState.cs index e90d673f..5dd2c720 100644 --- a/src/Hyperbee.Json/Filters/Parser/ParserState.cs +++ b/src/Hyperbee.Json/Filters/Parser/ParserState.cs @@ -1,30 +1,44 @@ -namespace Hyperbee.Json.Filters.Parser; +using System.Diagnostics; +namespace Hyperbee.Json.Filters.Parser; + +[DebuggerDisplay( "{Buffer.ToString()}, Item = {Item.ToString()}, Operator = {Operator}, Pos = {Pos.ToString()}" )] public ref struct ParserState { public ReadOnlySpan Buffer { get; } public ReadOnlySpan Item { get; internal set; } + public bool TrailingWhitespace { get; internal set; } + public bool IsArgument { get; internal set; } + public int BracketDepth { get; internal set; } + public ref int ParenDepth; + public Operator Operator { get; set; } public char Terminal { get; init; } public readonly ref int Pos; - internal ParserState( ReadOnlySpan buffer, ReadOnlySpan item, ref int pos, Operator tokenType, char terminal ) + internal ParserState( ReadOnlySpan buffer, ReadOnlySpan item, ref int pos, ref int parenDepth, Operator tokenType, char terminal ) { Buffer = buffer; Item = item; Operator = tokenType; Terminal = terminal; Pos = ref pos; + ParenDepth = ref parenDepth; } public readonly bool EndOfBuffer => Pos >= Buffer.Length; - public readonly bool IsParsing => Pos < Buffer.Length && Buffer[Pos] != Terminal; - public readonly bool IsTerminal => Buffer[Pos] == Terminal; + public readonly bool IsParsing => Pos < Buffer.Length && Previous != Terminal; public readonly char Current => Buffer[Pos]; public readonly char Previous => Buffer[Pos - 1]; - internal void SetItem( int itemStart, int itemEnd ) => Item = Buffer[itemStart..itemEnd].TrimEnd(); + internal void SetItem( int itemStart, int itemEnd ) + { + var item = Buffer[itemStart..itemEnd]; + TrailingWhitespace = !item.IsEmpty && char.IsWhiteSpace( item[^1] ); + + Item = item.TrimEnd(); + } } diff --git a/src/Hyperbee.Json/Filters/Values/Constants.cs b/src/Hyperbee.Json/Filters/Values/Constants.cs new file mode 100644 index 00000000..31f4b581 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Values/Constants.cs @@ -0,0 +1,10 @@ +namespace Hyperbee.Json.Filters.Values; + +public static class Constants +{ + public static ValueType True { get; } = new( true ); + public static ValueType False { get; } = new( false ); + + public static Null Null { get; } = new(); + public static Nothing Nothing { get; } = new(); +} diff --git a/src/Hyperbee.Json/Filters/Values/INodeType.cs b/src/Hyperbee.Json/Filters/Values/INodeType.cs new file mode 100644 index 00000000..a7fea226 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Values/INodeType.cs @@ -0,0 +1,8 @@ +namespace Hyperbee.Json.Filters.Values; + +public interface INodeType +{ + public NodeTypeKind Kind { get; } + + public INodeTypeComparer Comparer { get; set; } +} diff --git a/src/Hyperbee.Json/Filters/Values/NodeTypeComparer.cs b/src/Hyperbee.Json/Filters/Values/NodeTypeComparer.cs new file mode 100644 index 00000000..47103f4b --- /dev/null +++ b/src/Hyperbee.Json/Filters/Values/NodeTypeComparer.cs @@ -0,0 +1,236 @@ +using Hyperbee.Json.Descriptors; +using Hyperbee.Json.Filters.Parser; + +namespace Hyperbee.Json.Filters.Values; + +public interface INodeTypeComparer +{ + public int Compare( INodeType left, INodeType right, Operator operation ); + + public bool Exists( INodeType node ); +} + +public class NodeTypeComparer( IValueAccessor accessor ) : INodeTypeComparer +{ + private const float Tolerance = 1e-6F; // Define a tolerance for float comparisons + + /* + * Comparison Rules (according to JSONPath RFC 9535): + * + * 1. Compare Value to Value: + * - Two values are equal if they are of the same type and have the same value. + * - For float comparisons, use a tolerance to handle precision issues. + * - Comparisons between different types yield false. + * + * 2. Compare Node to Node: + * - Since a Node is essentially an enumerable with a single item, compare the single items directly. + * - Apply the same value comparison rules to the single items. + * + * 3. Compare NodeList to NodeList: + * - Two NodeLists are equal if they are sequence equal. + * - Sequence equality should consider deep equality of Node items. + * - Return 0 if sequences are equal. + * - Return -1 if the left sequence is less. + * - Return 1 if the left sequence is greater. + * + * 4. Compare NodeList to Value: + * - A NodeList is equal to a value if any node in the NodeList matches the value. + * - Return 0 if any node matches the value. + * - Return -1 if the value is less than all nodes. + * - Return 1 if the value is greater than all nodes. + * + * 5. Compare Value to NodeList: + * - Similar to the above, true if the value is found in the NodeList. + * + * 6. Compare Node to NodeList and vice versa: + * - Since Node is a single item enumerable, treat it similarly to Value in comparison to NodeList. + * + * 7. Truthiness Rules: + * - Falsy values: null, false, 0, "", NaN. + * - Truthy values: Anything not falsy, including non-empty strings, non-zero numbers, true, arrays, and objects. + * - Truthiness is generally not used for comparison operators (==, <) in filter expressions. + * - Type mismatches (e.g., string vs. number) result in false for equality (==) and true for inequality (!=). + * + * Order of Operations: + * - Check if both are NodeLists. + * - Check if one is a NodeList and the other is a Value. + * - Compare directly if both are Values. + */ + public int Compare( INodeType left, INodeType right, Operator operation ) + { + ThrowIfNotNormalized( left ); + ThrowIfNotNormalized( right ); + + if ( left is NodesType leftEnumerable && right is NodesType rightEnumerable ) + { + return CompareEnumerables( leftEnumerable, rightEnumerable ); + } + + if ( left is NodesType leftEnumerable1 ) + { + var compare = CompareEnumerableToValue( leftEnumerable1, right, out var typeMismatch, out var nodeCount ); + return AdjustResult( compare, nodeCount, operation, typeMismatch ); + } + + if ( right is NodesType rightEnumerable1 ) + { + var compare = CompareEnumerableToValue( rightEnumerable1, left, out var typeMismatch, out var nodeCount ); + return AdjustResult( compare, nodeCount, operation, typeMismatch ); + } + + return CompareValues( left, right, out _ ); + + static int AdjustResult( int compare, int nodeCount, Operator operation, bool typeMismatch ) + { + // When comparing a NodeList to a Value, '<' and '>' type operators only have meaning when the + // NodeList has a single node. + // + // 1. When there is a single node, the comparison is based on the unwrapped node value. + // This results in a meaningful value to value comparison for equality, and greater-than and + // less-than operations (if the values are the same type). + // + // 2. When there is more than one node, or an empty node list, equality is based on finding the + // value in the set of nodes. The result is true if the value is found in the set, and false + // otherwise. + // + // In this case, the result is not meaningful for greater-than and less-than operations, since + // the comparison is based on the set of nodes, and not on two single values. + // + // However, the comparison result will still be used in the context of a greater-than or less-than + // operation, which will yield indeterminate results based on the left or right order of operands. + // To handle this, we need to normalize the result of the comparison. In this case, we want to + // normalize the result so that greater-than and less-than always return false, regardless of the + // left or right order of the comparands. + + return (nodeCount != 1 || typeMismatch) switch // Test for a non-single value set, or a type comparison mismatch + { + true when (operation == Operator.LessThan || operation == Operator.LessThanOrEqual) => compare < 0 ? -compare : compare, + true when (operation == Operator.GreaterThan || operation == Operator.GreaterThanOrEqual) => compare > 0 ? -compare : compare, + _ => compare + }; + } + + static void ThrowIfNotNormalized( INodeType nodeType ) + { + if ( nodeType is NodesType { IsNormalized: false } ) + throw new NotSupportedException( "Unsupported non-single query." ); + } + } + + public bool Exists( INodeType node ) + { + return node switch + { + ValueType boolValue => boolValue.Value, + ValueType floatValue => floatValue.Value != 0, + ValueType stringValue => !string.IsNullOrEmpty( stringValue.Value ), + NodesType nodes => nodes.Any(), + _ => false + }; + } + + private int CompareEnumerables( IEnumerable left, IEnumerable right ) + { + using var leftEnumerator = left.GetEnumerator(); + using var rightEnumerator = right.GetEnumerator(); + + while ( leftEnumerator.MoveNext() ) + { + if ( !rightEnumerator.MoveNext() ) + return 1; // Left has more elements, so it is greater + + // if the values can be extracted, compare the values directly + if ( TryGetValueType( accessor, leftEnumerator.Current, out var leftItemValue ) && + TryGetValueType( accessor, rightEnumerator.Current, out var rightItemValue ) ) + return CompareValues( leftItemValue, rightItemValue, out _ ); + + if ( !accessor.DeepEquals( leftEnumerator.Current, rightEnumerator.Current ) ) + return -1; // Elements are not deeply equal + } + + if ( rightEnumerator.MoveNext() ) + return -1; // Right has more elements, so left is less + + return 0; // Sequences are equal + } + + private int CompareEnumerableToValue( IEnumerable enumeration, INodeType value, out bool typeMismatch, out int nodeCount ) + { + nodeCount = 0; + typeMismatch = false; + var lastCompare = -1; + + foreach ( var item in enumeration ) + { + nodeCount++; + + if ( !TryGetValueType( accessor, item, out var itemValue ) ) + continue; // Skip if value cannot be extracted + + lastCompare = CompareValues( itemValue, value, out typeMismatch ); + + if ( lastCompare == 0 ) + return 0; // Return 0 if any node matches the value + } + + if ( nodeCount == 0 ) + { + if ( value is Nothing ) // Considered equal + return 0; + + return -1; + } + + return nodeCount != 1 ? -1 : lastCompare; // Return the last comparison if there is only one node + + } + + private static int CompareValues( INodeType left, INodeType right, out bool typeMismatch ) + { + typeMismatch = false; + + if ( left is Null or Nothing && right is Null or Nothing ) + { + return 0; + } + + if ( left?.GetType() != right?.GetType() ) + { + typeMismatch = true; // Type mismatch: important for non-equality comparisons + return -1; + } + + return left switch + { + ValueType leftStringValue when right is ValueType rightStringValue => + string.Compare( leftStringValue.Value, rightStringValue.Value, StringComparison.Ordinal ), + + ValueType leftBoolValue when right is ValueType rightBoolValue => + leftBoolValue.Value.CompareTo( rightBoolValue.Value ), + + ValueType leftFloatValue when right is ValueType rightFloatValue => + Math.Abs( leftFloatValue.Value - rightFloatValue.Value ) < Tolerance ? 0 : leftFloatValue.Value.CompareTo( rightFloatValue.Value ), + + _ => Comparer.Default.Compare( left, right ) + }; + } + + private static bool TryGetValueType( IValueAccessor accessor, TNode node, out INodeType nodeType ) + { + if ( accessor.TryGetValueFromNode( node, out var itemValue ) ) + { + nodeType = itemValue switch + { + string itemString => new ValueType( itemString ), + bool itemBool => new ValueType( itemBool ), + float itemFloat => new ValueType( itemFloat ), + null => Constants.Null, + _ => throw new NotSupportedException( "Unsupported value type." ) + }; + return true; + } + + nodeType = null; + return false; + } +} diff --git a/src/Hyperbee.Json/Filters/Values/NodeTypeKind.cs b/src/Hyperbee.Json/Filters/Values/NodeTypeKind.cs new file mode 100644 index 00000000..5e401893 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Values/NodeTypeKind.cs @@ -0,0 +1,10 @@ +namespace Hyperbee.Json.Filters.Values; + +public enum NodeTypeKind +{ + Null, + Nothing, + Value, + Node, + NodeList +} diff --git a/src/Hyperbee.Json/Filters/Values/NodesType.cs b/src/Hyperbee.Json/Filters/Values/NodesType.cs new file mode 100644 index 00000000..76cb6bc0 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Values/NodesType.cs @@ -0,0 +1,17 @@ +using System.Collections; + +namespace Hyperbee.Json.Filters.Values; + +public struct NodesType( IEnumerable value, bool isNormalized ) : INodeType, IEnumerable +{ + public readonly bool IsNormalized => isNormalized; + public readonly NodeTypeKind Kind => NodeTypeKind.NodeList; + + public INodeTypeComparer Comparer { get; set; } + + public IEnumerable Value { get; } = value; + + public readonly IEnumerator GetEnumerator() => Value.GetEnumerator(); + + readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/Hyperbee.Json/Filters/Values/Nothing.cs b/src/Hyperbee.Json/Filters/Values/Nothing.cs new file mode 100644 index 00000000..79a22880 --- /dev/null +++ b/src/Hyperbee.Json/Filters/Values/Nothing.cs @@ -0,0 +1,7 @@ +namespace Hyperbee.Json.Filters.Values; + +public struct Nothing : INodeType +{ + public readonly NodeTypeKind Kind => NodeTypeKind.Nothing; + public INodeTypeComparer Comparer { get; set; } +} diff --git a/src/Hyperbee.Json/Filters/Values/Null.cs b/src/Hyperbee.Json/Filters/Values/Null.cs new file mode 100644 index 00000000..72a0942f --- /dev/null +++ b/src/Hyperbee.Json/Filters/Values/Null.cs @@ -0,0 +1,7 @@ +namespace Hyperbee.Json.Filters.Values; + +public struct Null : INodeType +{ + public readonly NodeTypeKind Kind => NodeTypeKind.Null; + public INodeTypeComparer Comparer { get; set; } +} diff --git a/src/Hyperbee.Json/Filters/Values/ValueType.cs b/src/Hyperbee.Json/Filters/Values/ValueType.cs new file mode 100644 index 00000000..8cba9a7d --- /dev/null +++ b/src/Hyperbee.Json/Filters/Values/ValueType.cs @@ -0,0 +1,10 @@ +namespace Hyperbee.Json.Filters.Values; + +public struct ValueType( T value ) : INodeType +{ + public readonly NodeTypeKind Kind => NodeTypeKind.Value; + + public INodeTypeComparer Comparer { get; set; } + + public T Value { get; } = value; +} diff --git a/src/Hyperbee.Json/Internal/SpanBuilder.cs b/src/Hyperbee.Json/Internal/SpanBuilder.cs new file mode 100644 index 00000000..24c423d4 --- /dev/null +++ b/src/Hyperbee.Json/Internal/SpanBuilder.cs @@ -0,0 +1,76 @@ + +using System.Runtime.CompilerServices; + +namespace Hyperbee.Json.Internal; + +using System; +using System.Buffers; + +internal ref struct SpanBuilder // use in a try finally with an explicit Dispose +{ + private char[] _buffer; + private Span _chars; + private int _pos; + + public SpanBuilder( int initialCapacity ) + { + _buffer = ArrayPool.Shared.Rent( initialCapacity ); + _chars = _buffer; + _pos = 0; + } + + public readonly bool IsEmpty => _pos == 0; + + public void Append( char value ) + { + if ( _pos >= _chars.Length ) + Grow(); + + _chars[_pos++] = value; + } + + public void Append( ReadOnlySpan value ) + { + if ( _pos + value.Length > _chars.Length ) + Grow( value.Length ); + + value.CopyTo( _chars[_pos..] ); + _pos += value.Length; + } + + public void Clear() => _pos = 0; + + public readonly ReadOnlySpan AsSpan() => _chars[.._pos]; + + private void Grow( int additionalCapacity = 0 ) + { + var newCapacity = Math.Max( _chars.Length * 2, _chars.Length + additionalCapacity ); + var newArray = ArrayPool.Shared.Rent( newCapacity ); + _chars.CopyTo( newArray ); + + ArrayPool.Shared.Return( _buffer ); + _buffer = newArray; + _chars = newArray; + } + + public override string ToString() + { + var value = _chars[.._pos].ToString(); + Dispose(); + return value; + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public void Dispose() + { + var array = _buffer; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + + if ( array != null ) + ArrayPool.Shared.Return( array ); + + _buffer = null; + _chars = default; + _pos = 0; + } +} diff --git a/src/Hyperbee.Json/Internal/SpanHelper.cs b/src/Hyperbee.Json/Internal/SpanHelper.cs new file mode 100644 index 00000000..af3cf370 --- /dev/null +++ b/src/Hyperbee.Json/Internal/SpanHelper.cs @@ -0,0 +1,132 @@ +namespace Hyperbee.Json.Internal; + +internal enum SpanUnescapeOptions +{ + Single, + SingleThenUnquote, + Mixed +} + +internal static class SpanHelper +{ + internal static void Unescape( ReadOnlySpan span, ref SpanBuilder builder, SpanUnescapeOptions options ) + { + if ( options == SpanUnescapeOptions.Single || options == SpanUnescapeOptions.SingleThenUnquote ) + { + if ( span.Length < 2 || span[0] != '\'' && span[0] != '"' || span[^1] != '\'' && span[^1] != '"' ) + throw new ArgumentException( "Quoted strings must start and end with a quote." ); + + if ( options == SpanUnescapeOptions.SingleThenUnquote ) + UnescapeQuotedString( span[1..^1], span[0], ref builder ); // unquote and unescape + else + UnescapeQuotedString( span, span[0], ref builder ); // unquote + } + else + { + // Scan for, and unescape, quoted strings + for ( var i = 0; i < span.Length; i++ ) + { + var current = span[i]; + + if ( current == '\'' || current == '"' ) + { + builder.Append( current ); + + var endQuotePos = UnescapeQuotedString( span[++i..], current, ref builder ); // unescape + + if ( endQuotePos == -1 ) // we expect a closing quote + throw new ArgumentException( "Closing quote not found in quoted string." ); + + i += endQuotePos; + + builder.Append( current ); + } + else + { + builder.Append( current ); + } + } + } + } + + private static int UnescapeQuotedString( ReadOnlySpan span, char quoteChar, ref SpanBuilder builder ) + { + for ( var i = 0; i < span.Length; i++ ) + { + if ( span[i] == quoteChar ) + { + // return after the closing quote + return i; + } + + if ( span[i] == '\\' && i + 1 < span.Length ) + { + i++; + switch ( span[i] ) + { + case '\'': + builder.Append( '\'' ); + break; + case '"': + builder.Append( '"' ); + break; + case '\\': + builder.Append( '\\' ); + break; + case '/': + builder.Append( '/' ); + break; + case 'b': + builder.Append( '\b' ); + break; + case 'f': + builder.Append( '\f' ); + break; + case 'n': + builder.Append( '\n' ); + break; + case 'r': + builder.Append( '\r' ); + break; + case 't': + builder.Append( '\t' ); + break; + case 'u' when i + 4 < span.Length: + builder.Append( ConvertHexToChar( span.Slice( i + 1, 4 ) ) ); + i += 4; + break; + default: + throw new ArgumentException( $"Invalid escape sequence `\\{span[i]}` in quoted string." ); + } + } + else + { + builder.Append( span[i] ); + } + } + + return -1; // no closing quote + + static char ConvertHexToChar( ReadOnlySpan hexSpan ) + { + if ( hexSpan.Length != 4 ) + { + throw new ArgumentException( "Hex span must be exactly 4 characters long." ); + } + + var value = 0; + for ( var i = 0; i < hexSpan.Length; i++ ) + { + value = (value << 4) + hexSpan[i] switch + { + >= '0' and <= '9' => hexSpan[i] - '0', + >= 'a' and <= 'f' => hexSpan[i] - 'a' + 10, + >= 'A' and <= 'F' => hexSpan[i] - 'A' + 10, + _ => throw new ArgumentException( "Invalid hex digit." ) + }; + } + + return (char) value; + } + } +} diff --git a/src/Hyperbee.Json/JsonPath.cs b/src/Hyperbee.Json/JsonPath.cs index 66645e78..7e56066f 100644 --- a/src/Hyperbee.Json/JsonPath.cs +++ b/src/Hyperbee.Json/JsonPath.cs @@ -33,7 +33,6 @@ #endregion using System.Diagnostics; -using System.Globalization; using System.Runtime.CompilerServices; using Hyperbee.Json.Descriptors; @@ -57,23 +56,38 @@ internal enum NodeFlags public static IEnumerable Select( in TNode value, string query, NodeProcessorDelegate processor = null ) { - return EnumerateMatches( value, value, query, processor ); + var compiledQuery = JsonPathQueryParser.Parse( query ); + return EnumerateMatches( value, value, compiledQuery, processor ); } - internal static IEnumerable SelectInternal( in TNode value, TNode root, string query, NodeProcessorDelegate processor = null ) + internal static IEnumerable SelectInternal( in TNode value, in TNode root, JsonPathQuery compiledQuery, NodeProcessorDelegate processor = null ) { - return EnumerateMatches( value, root, query, processor ); + // entry point for filter recursive calls + + // explicitly allow dot whitespace for function arguments. This is annoying + // because the RFC ABNF does not allow whitespace in the query for dot member + // notation, but it does allow it in the filter for function arguments. + + return EnumerateMatches( value, root, compiledQuery, processor ); } - private static IEnumerable EnumerateMatches( in TNode value, in TNode root, string query, NodeProcessorDelegate processor = null ) + private static IEnumerable EnumerateMatches( in TNode value, in TNode root, JsonPathQuery compiledQuery, NodeProcessorDelegate processor = null ) { - if ( string.IsNullOrWhiteSpace( query ) ) // invalid per the RFC ABNF + if ( string.IsNullOrWhiteSpace( compiledQuery.Query ) ) // invalid per the RFC ABNF return []; // Consensus: return empty array for empty query - if ( query == "$" || query == "@" ) // quick out for everything + if ( compiledQuery.Query == "$" || compiledQuery.Query == "@" ) // quick out for everything return [value]; - var segmentNext = JsonPathQueryParser.Parse( query ).Next; // The first segment is always the root; skip it + var segmentNext = compiledQuery.Segments.Next; // The first segment is always the root; skip it + + if ( Descriptor.CanUsePointer && compiledQuery.Normalized ) // we can fast path this + { + if ( Descriptor.Accessor.TryGetFromPointer( in value, segmentNext, out var result ) ) + return [result]; + + return []; + } return EnumerateMatches( root, new NodeArgs( default, value, default, segmentNext, NodeFlags.Default ), processor ); } @@ -119,159 +133,159 @@ private static IEnumerable EnumerateMatches( TNode root, NodeArgs args, N if ( nodeKind == NodeKind.Value ) continue; - // try to access object or array using name or index + // singular selector if ( segmentCurrent.IsSingular ) { if ( nodeKind == NodeKind.Object && selectorKind == SelectorKind.Index ) continue; // don't allow indexing in to objects - if ( accessor.TryGetChildValue( value, selector, out var childValue ) ) - { + // try to access object or array using name or index + if ( accessor.TryGetChildValue( value, selector, selectorKind, out var childValue ) ) stack.Push( value, childValue, selector, segmentNext ); - } continue; } - // wildcard + // group selector - if ( selectorKind == SelectorKind.Wildcard ) + for ( var i = 0; i < segmentCurrent.Selectors.Length; i++ ) // using 'for' for performance { - foreach ( var (childValue, childKey, childKind) in accessor.EnumerateChildren( value ) ) - { - // optimization: quicker return for final - // - // the parser will work without this check, but we would be forcing it - // to push and pop values onto the stack that we know will not be used. - if ( segmentNext.IsFinal ) - { - // theoretically, we should yield here, but we can't because we need to - // preserve the order of the results as per the RFC. so we push the - // value onto the stack without prepending the childKey or childKind - // to set up for an immediate return on the next iteration. - //Push( stack, value, childValue, childKey, segmentNext ); - stack.Push( value, childValue, childKey, segmentNext ); - continue; - } - - stack.Push( parent, value, childKey, segmentNext.Prepend( childKey, childKind ) ); // (Name | Index) - } - - continue; - } - - // descendant + if ( i > 0 ) // we already have the first selector + (selector, selectorKind) = segmentCurrent.Selectors[i]; - if ( selectorKind == SelectorKind.Descendant ) - { - foreach ( var (childValue, childKey, _) in accessor.EnumerateChildren( value, includeValues: false ) ) // child arrays or objects only + switch ( selectorKind ) { - stack.Push( value, childValue, childKey, segmentCurrent ); // Descendant - } + // descendant + case SelectorKind.Descendant: + { + foreach ( var (childValue, childKey, _) in accessor.EnumerateChildren( value, includeValues: false ) ) // child arrays or objects only + { + stack.Push( value, childValue, childKey, segmentCurrent ); // Descendant + } - // Union Processing After Descent: If a union operator follows a descent operator, - // either directly or after intermediary selectors, it should only process simple values. + // Union Processing After Descent: If a union operator immediately follows a + // descendant operator, the union should only process simple values. This is + // to prevent duplication of complex objects that would result from both the + // current node and the union processing the same items. - stack.Push( parent, value, null, segmentNext, NodeFlags.AfterDescent ); // process the current value - continue; - } + stack.Push( parent, value, null, segmentNext, NodeFlags.AfterDescent ); // process the current value + continue; + } - // group + // wildcard + case SelectorKind.Wildcard: + { + foreach ( var (childValue, childKey, childKind) in accessor.EnumerateChildren( value ) ) + { + // optimization: quicker return for final + // + // the parser will work without this check, but we would be forcing it + // to push and pop values onto the stack that we know will not be used. + if ( segmentNext.IsFinal ) + { + // we could just yield here, but we can't because we want to preserve + // the order of the results as per the RFC. so we push the current + // value onto the stack without prepending the childKey or childKind + // to set up for an immediate return on the next iteration. + //Push( stack, value, childValue, childKey, segmentNext ); + stack.Push( value, childValue, childKey, segmentNext ); + continue; + } + + stack.Push( parent, value, childKey, segmentNext.Prepend( childKey, childKind ) ); // (Name | Index) + } - for ( var i = 0; i < segmentCurrent.Selectors.Length; i++ ) // use 'for' for performance - { - if ( i != 0 ) - (selector, selectorKind) = segmentCurrent.Selectors[i]; + continue; + } - // [?exp] + // [?exp] + case SelectorKind.Filter: + { + foreach ( var (childValue, childKey, childKind) in accessor.EnumerateChildren( value ) ) + { + if ( !filterEvaluator.Evaluate( selector[1..], childValue, root ) ) // remove the leading '?' character + continue; - if ( selectorKind == SelectorKind.Filter ) - { - foreach ( var (childValue, childKey, childKind) in accessor.EnumerateChildren( value ) ) - { - var result = filterEvaluator.Evaluate( selector[1..], childValue, root ); // remove leading '?' + // optimization: quicker return for tail values + if ( segmentNext.IsFinal ) + { + stack.Push( value, childValue, childKey, segmentNext ); + continue; + } - if ( !Truthy( result ) ) - continue; + stack.Push( parent, value, childKey, segmentNext.Prepend( childKey, childKind ) ); // (Name | Index) + } - // optimization: quicker return for tail values - if ( segmentNext.IsFinal ) - { - stack.Push( value, childValue, childKey, segmentNext ); continue; } - stack.Push( parent, value, childKey, segmentNext.Prepend( childKey, childKind ) ); // (Name | Index) - } - - continue; - } + // Array: [#,#,...] + case SelectorKind.Index: + { + if ( nodeKind != NodeKind.Array ) + continue; - // array [name1,name2,...] or [#,#,...] or [start:end:step] + if ( accessor.TryGetElementAt( value, int.Parse( selector ), out var childValue ) ) + stack.Push( value, childValue, selector, segmentNext ); + continue; + } - if ( nodeKind == NodeKind.Array ) - { - // [#,#,...] + // Array: [start:end:step] Python slice syntax + case SelectorKind.Slice: + { + if ( nodeKind != NodeKind.Array ) + continue; - if ( selectorKind == SelectorKind.Index ) - { - stack.Push( value, accessor.GetElementAt( value, int.Parse( selector ) ), selector, segmentNext ); - continue; - } + foreach ( var index in EnumerateSlice( value, selector, accessor ) ) + { + if ( accessor.TryGetElementAt( value, index, out var childValue ) ) + stack.Push( value, childValue, index.ToString(), segmentNext ); + } - // [start:end:step] Python slice syntax + continue; + } - if ( selectorKind == SelectorKind.Slice ) - { - foreach ( var index in EnumerateSlice( value, selector, accessor ) ) + // Array: [name1,name2,...] Names over array + case SelectorKind.Name when nodeKind == NodeKind.Array: { - stack.Push( value, accessor.GetElementAt( value, index ), index.ToString(), segmentNext ); - } - continue; - } + var indexSegment = segmentNext.Prepend( selector, SelectorKind.Name ); + var length = accessor.GetArrayLength( value ); - // [name1,name2,...] + for ( var index = length - 1; index >= 0; index-- ) + { + if ( !accessor.TryGetElementAt( value, index, out var childValue ) ) + continue; - var indexSegment = segmentNext.Prepend( selector, SelectorKind.Name ); - var length = accessor.GetArrayLength( value ); + if ( flags == NodeFlags.AfterDescent && accessor.GetNodeKind( childValue ) != NodeKind.Value ) + continue; - for ( var index = length - 1; index >= 0; index-- ) - { - var childValue = accessor.GetElementAt( value, index ); + stack.Push( value, childValue, index.ToString(), indexSegment ); + } - if ( flags == NodeFlags.AfterDescent && accessor.GetNodeKind( childValue ) != NodeKind.Value ) continue; + } - stack.Push( value, childValue, index.ToString(), indexSegment ); - } - - continue; - } + // Object: [name1,name2,...] Names over object + case SelectorKind.Name when nodeKind == NodeKind.Object: + { + if ( accessor.TryGetChildValue( value, selector, selectorKind, out var childValue ) ) + stack.Push( value, childValue, selector, segmentNext ); - // object [name1,name2,...] + continue; + } - if ( nodeKind == NodeKind.Object ) - { - if ( selectorKind == SelectorKind.Slice || selectorKind == SelectorKind.Index ) - continue; + default: + { + throw new NotSupportedException( $"Unsupported {nameof( SelectorKind )}." ); + } - if ( accessor.TryGetChildValue( value, selector, out var childValue ) ) - { - stack.Push( value, childValue, selector, segmentNext ); - } - } - } + } // end switch + } // end for group selector } while ( stack.TryPop( out args ) ); } - [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static bool Truthy( object value ) - { - return value is not null and not IConvertible || Convert.ToBoolean( value, CultureInfo.InvariantCulture ); - } - private static IEnumerable EnumerateSlice( TNode value, string sliceExpr, IValueAccessor accessor ) { var length = accessor.GetArrayLength( value ); @@ -298,11 +312,13 @@ private static IEnumerable EnumerateSlice( TNode value, string sliceExpr, I } } - [DebuggerDisplay( "Parent = {Parent}, Value = {Value}, First = ({Segment?.Selectors?[0]}), IsSingular = {Segment?.IsSingular}, Count = {Segment?.Selectors?.Length}" )] + [DebuggerDisplay( "Parent = {Parent}, Value = {Value}, {Segment}" )] private record struct NodeArgs( TNode Parent, TNode Value, string Key, JsonPathSegment Segment, NodeFlags Flags ); + [DebuggerDisplay( "{_stack}" )] private sealed class NodeArgsStack( int capacity = 16 ) { + [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] private readonly Stack _stack = new( capacity ); [MethodImpl( MethodImplOptions.AggressiveInlining )] diff --git a/src/Hyperbee.Json/JsonPathQueryParser.cs b/src/Hyperbee.Json/JsonPathQueryParser.cs index 255cceac..79f3c6f5 100644 --- a/src/Hyperbee.Json/JsonPathQueryParser.cs +++ b/src/Hyperbee.Json/JsonPathQueryParser.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; -using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; +using Hyperbee.Json.Internal; namespace Hyperbee.Json; @@ -12,239 +13,242 @@ public enum SelectorKind Singular = 0x1, Group = 0x2, - // dot notation - Root = 0x4 | Singular, - Dot = 0x8 | Singular, - - // union notation + // selectors + Root = 0x8 | Singular, Name = 0x10 | Singular, Index = 0x20 | Singular, Slice = 0x40 | Group, Filter = 0x80 | Group, - - // Wildcard = 0x100 | Group, Descendant = 0x200 | Group } internal static class JsonPathQueryParser { - private static readonly ConcurrentDictionary JsonPathTokens = new(); + private static readonly ConcurrentDictionary JsonPathQueries = new(); private enum State { + Undefined, + Whitespace, Start, DotChild, - UnionStart, - UnionElementQuoted, - UnionElementQuotedFinal, - UnionElement, - UnionNextElement, - UnionFinal, + UnionItem, + UnionNext, + Finish, Final } - private static string GetSelector( State state, ReadOnlySpan buffer, int start, int stop ) - { - var adjust = state == State.Final ? 0 : 1; // non-final states have already advanced to the next character, so we need to subtract 1 - var length = stop - start - adjust; - return length <= 0 ? null : buffer.Slice( start, length ).Trim().ToString(); - } - - private static void InsertToken( ICollection tokens, SelectorDescriptor selector ) - { - if ( selector?.Value == null ) - return; - - InsertToken( tokens, [selector] ); - } - - private static void InsertToken( ICollection tokens, SelectorDescriptor[] selectors ) + internal static JsonPathQuery Parse( ReadOnlySpan query, bool allowDotWhitespace = false ) { - if ( selectors == null || selectors.Length == 0 ) - return; - - tokens.Add( new JsonPathSegment( selectors ) ); + return Parse( query.ToString(), allowDotWhitespace ); } - internal static JsonPathSegment Parse( string query ) + internal static JsonPathQuery Parse( string query, bool allowDotWhitespace = false ) { - return JsonPathTokens.GetOrAdd( query, x => TokenFactory( x.AsSpan() ) ); + return JsonPathQueries.GetOrAdd( query, x => QueryFactory( x.AsSpan(), allowDotWhitespace ) ); } - internal static JsonPathSegment ParseNoCache( ReadOnlySpan query ) + internal static JsonPathQuery ParseNoCache( ReadOnlySpan query, bool allowDotWhitespace = false ) { - return TokenFactory( query ); + return QueryFactory( query, allowDotWhitespace ); } - private static JsonPathSegment TokenFactory( ReadOnlySpan query ) + private static JsonPathQuery QueryFactory( ReadOnlySpan query, bool allowDotWhitespace = false ) { - // transform jsonpath patterns like "$.store.book[*]..author" to an array of tokens [ $, store, book, *, .., author ] - - var tokens = new List(); - - query = query.TrimEnd(); // remove trailing whitespace to simplify parsing + // RFC - query cannot start or end with whitespace + if ( !query.IsEmpty && (char.IsWhiteSpace( query[0] ) || char.IsWhiteSpace( query[^1] )) ) + throw new NotSupportedException( "Query cannot start or end with whitespace." ); var i = 0; var n = query.Length; var selectorStart = 0; + var inQuotes = false; + var inFilter = false; + var quoteChar = '\''; + bool escaped = false; var bracketDepth = 0; var parenDepth = 0; - var literalDelimiter = '\''; + + char[] whitespaceTerminators = []; + var whiteSpaceReplay = true; + + var tokens = new List(); var selectors = new List(); var state = State.Start; + State returnState = State.Undefined; do { - var c = query[i++]; + // read next character + char c; + + if ( i < n ) + { + c = query[i++]; + } + else // end of input + { + if ( state != State.Whitespace ) // whitespace is a sub-state, allow it to exit + state = State.Finish; + c = '\0'; // Set char to null terminator to signal end of input + } + // process character + ReadOnlySpan selectorSpan; SelectorKind selectorKind; - string selectorValue; switch ( state ) { case State.Start: switch ( c ) { - case ' ': - case '\t': - break; case '@': // Technically invalid, but allows `@` to work on sub queries without changing tokenizer case '$': - if ( i < n && query[i] != '.' && query[i] != '[' ) - throw new NotSupportedException( "Invalid character after `$`." ); if ( query[^1] == '.' && query[^2] == '.' ) throw new NotSupportedException( "`..` cannot be the last segment." ); - state = State.DotChild; + InsertToken( tokens, new SelectorDescriptor { SelectorKind = SelectorKind.Root, Value = c.ToString() } ); + + whitespaceTerminators = ['.', '[']; + state = State.Whitespace; + returnState = State.DotChild; break; + default: - throw new NotSupportedException( "`$` expected." ); + throw new NotSupportedException( $"Invalid character `{c}` at pos {i - 1}." ); } break; - case State.DotChild: + case State.Whitespace: switch ( c ) { - case '[': - state = State.UnionStart; + case ' ': + case '\t': + case '\n': + case '\r': + break; + default: - selectorValue = GetSelector( state, query, selectorStart, i ); - selectorKind = selectorValue switch + if ( c != '\0' && whitespaceTerminators.Length > 0 && !whitespaceTerminators.Contains( c ) ) + throw new NotSupportedException( $"Invalid character `{c}` at pos {i - 1}." ); + + whitespaceTerminators = []; + state = returnState; // transition back to the appropriate state + selectorStart = i; // start of the next selector + + if ( whiteSpaceReplay ) + i--; // replay character + + whiteSpaceReplay = true; + + break; + } + + break; + + case State.DotChild: + switch ( c ) + { + case '[': // end-of-child + selectorSpan = GetSelectorSpan( state, query, selectorStart, i ); + selectorKind = selectorSpan switch { - "$" when tokens.Count != 0 => throw new NotSupportedException( $"Invalid use of root `$` at pos {i - 1}." ), - "$" => SelectorKind.Root, + "$" => throw new NotSupportedException( $"Invalid use of root `$` at pos {i - 1}." ), + "@" => throw new NotSupportedException( $"Invalid use of local root `$` at pos {i - 1}." ), "*" => SelectorKind.Wildcard, - _ => SelectorKind.Dot + _ => SelectorKind.Name }; - InsertToken( tokens, new SelectorDescriptor { SelectorKind = selectorKind, Value = selectorValue } ); + if ( selectorKind == SelectorKind.Name && !selectorSpan.IsEmpty ) + { + ThrowIfQuoted( selectorSpan ); + ThrowIfInvalidUnquotedName( selectorSpan ); + } + InsertToken( tokens, GetSelectorDescriptor( selectorKind, selectorSpan ) ); + + state = State.Whitespace; + whiteSpaceReplay = false; + returnState = State.UnionItem; + bracketDepth = 1; + i--; // replay character break; - case '.': + case '.': // end-of-child if ( i == n ) throw new NotSupportedException( $"Missing character after `.` at pos {i - 1}." ); - selectorValue = GetSelector( state, query, selectorStart, i ); - selectorKind = selectorValue switch + selectorSpan = GetSelectorSpan( state, query, selectorStart, i ); + selectorKind = selectorSpan switch { - "$" when tokens.Count != 0 => throw new NotSupportedException( $"Invalid use of root `$` at pos {i - 1}." ), - "$" => SelectorKind.Root, + "$" => throw new NotSupportedException( $"Invalid use of root `$` at pos {i - 1}." ), + "@" => throw new NotSupportedException( $"Invalid use of local root `$` at pos {i - 1}." ), "*" => SelectorKind.Wildcard, - _ => SelectorKind.Dot + _ => SelectorKind.Name }; - InsertToken( tokens, new SelectorDescriptor { SelectorKind = selectorKind, Value = selectorValue } ); - - if ( i <= n && query[i] == '.' ) + if ( selectorKind == SelectorKind.Name && !selectorSpan.IsEmpty ) // can be null after a union { - InsertToken( tokens, new SelectorDescriptor { SelectorKind = SelectorKind.Descendant, Value = ".." } ); - - i++; + ThrowIfQuoted( selectorSpan ); + ThrowIfInvalidUnquotedName( selectorSpan ); } - selectorStart = i; - break; - case ' ': - case '\t': - throw new NotSupportedException( $"Invalid whitespace in object notation at pos {i - 1}." ); - } + InsertToken( tokens, GetSelectorDescriptor( selectorKind, selectorSpan ) ); - break; + if ( i < n && query[i] == '.' ) // peek next character + { + InsertToken( tokens, GetSelectorDescriptor( SelectorKind.Descendant, ".." ) ); + i++; // advance past second `.` + } - case State.UnionStart: - switch ( c ) - { - case ' ': - case '\t': - break; - case '*': - state = State.UnionFinal; - InsertToken( tokens, new SelectorDescriptor { SelectorKind = SelectorKind.Wildcard, Value = "*" } ); + selectorStart = i; break; - case '.': - if ( i > n || query[i] != '.' ) - throw new NotSupportedException( $"Invalid `.` in bracket expression at pos {i - 1}." ); - state = State.UnionFinal; - InsertToken( tokens, new SelectorDescriptor { SelectorKind = SelectorKind.Descendant, Value = ".." } ); - i++; - break; case '\'': case '"': - state = State.UnionElementQuoted; - literalDelimiter = c; - selectorStart = i - 1; - bracketDepth = 1; - break; - default: - state = State.UnionElement; - i--; // replay character - selectorStart = i; - bracketDepth = 1; + throw new NotSupportedException( $"Quoted member names are not allowed in dot notation at pos {i - 1}." ); + case ' ': + case '\t': + case '\n': + case '\r': + if ( !allowDotWhitespace ) // filter dot notation allows whitespace, query dot notation does not + throw new NotSupportedException( $"Invalid whitespace in object notation at pos {i - 1}." ); break; } break; - case State.UnionElementQuoted: - if ( c == '\\' ) // handle escaping - { - i++; // advance past the escaped character - } - else if ( c == literalDelimiter ) + case State.UnionItem: + + if ( inQuotes ) { - state = State.UnionElementQuotedFinal; + if ( c == '\\' ) // handle escaping + { + escaped = true; + i++; // advance past the escaped character + } + else if ( c == quoteChar ) + { + inQuotes = false; + } + + continue; } - break; - - case State.UnionElementQuotedFinal: switch ( c ) { - case ' ': - case '\t': - break; - case ']': - case ',': - state = State.UnionElement; - i--; // replay character + case '\'': + case '"': + quoteChar = c; + inQuotes = true; break; - default: // invalid characters after end of string - throw new NotSupportedException( $"Invalid bracket literal at pos {i - 1}." ); - } - - break; - case State.UnionElement: - switch ( c ) - { case '[': // handle nested `[` (not called for first bracket) bracketDepth++; break; @@ -256,231 +260,529 @@ private static JsonPathSegment TokenFactory( ReadOnlySpan query ) break; case ',': case ']': - if ( c == ']' && --bracketDepth > 0 ) // handle nested `]` + if ( c == ']' && bracketDepth-- > 1 ) // handle nested `]` + break; + if ( c == ',' && bracketDepth > 1 ) break; if ( parenDepth > 0 ) break; - // get the child item atom - - selectorValue = GetSelector( state, query, selectorStart, i ); + // get the selector + selectorSpan = GetSelectorSpan( state, query, selectorStart, i ); selectorStart = i; - // validate the extracted atom value shape - - if ( string.IsNullOrEmpty( selectorValue ) ) // [] is not valid + if ( selectorSpan.IsEmpty ) // [] is not valid throw new NotSupportedException( "Invalid bracket expression syntax. Bracket expression cannot be empty." ); - selectorKind = GetSelectorKind( selectorValue ); + // validate the selector and get its kind + selectorKind = GetValidSelectorKind( selectorSpan ); - if ( selectorKind == SelectorKind.Undefined ) - throw new NotSupportedException( $"Invalid bracket expression syntax. Unrecognized selector format at pos {i - 1}." ); + // create the selector descriptor + SelectorDescriptor descriptor; - if ( selectorKind == SelectorKind.Name ) + switch ( selectorKind ) { - selectorValue = selectorValue[1..^1]; // remove surrounding quotes - selectorValue = Regex.Unescape( selectorValue ); // unescape selector + case SelectorKind.Undefined: + throw new NotSupportedException( $"Invalid bracket expression syntax. Unrecognized selector format at pos {i - 1}." ); + + case SelectorKind.Name: + ThrowIfInvalidQuotedName( selectorSpan ); + if ( escaped ) + { + var builder = new SpanBuilder( selectorSpan.Length ); + try + { + SpanHelper.Unescape( selectorSpan, ref builder, SpanUnescapeOptions.SingleThenUnquote ); // unescape and then unquote + descriptor = GetSelectorDescriptor( selectorKind, builder, nullable: false ); + escaped = false; + } + finally // ensure builder is disposed + { + builder.Dispose(); + } + } + else + { + descriptor = GetSelectorDescriptor( selectorKind, selectorSpan[1..^1], nullable: false ); // unquote + } + + break; + + case SelectorKind.Filter: + if ( escaped ) + { + var builder = new SpanBuilder( selectorSpan.Length ); + try + { + SpanHelper.Unescape( selectorSpan, ref builder, SpanUnescapeOptions.Mixed ); // unescape one or more strings + descriptor = GetSelectorDescriptor( selectorKind, builder ); + escaped = false; + } + finally // ensure builder is disposed + { + builder.Dispose(); + } + } + else + { + descriptor = GetSelectorDescriptor( selectorKind, selectorSpan ); + } + + break; + + default: + descriptor = GetSelectorDescriptor( selectorKind, selectorSpan ); + break; } - selectors.Insert( 0, new SelectorDescriptor { SelectorKind = selectorKind, Value = selectorValue } ); + selectors.Insert( 0, descriptor ); // continue parsing the union switch ( c ) { case ',': - state = State.UnionNextElement; + whitespaceTerminators = []; + state = State.Whitespace; + returnState = State.UnionNext; break; case ']': - state = State.DotChild; InsertToken( tokens, [.. selectors] ); selectors.Clear(); + + whitespaceTerminators = ['.', '[']; + state = State.Whitespace; + returnState = State.DotChild; break; } break; + + case '?': + if ( !inQuotes ) + inFilter = true; + break; + + case '.': // descent in brackets is illegal except within a filter expr + if ( i < n && query[i] == '.' && !inFilter ) + throw new NotSupportedException( $"Invalid `..` in bracket expression at pos {i - 1}." ); + break; } break; - case State.UnionNextElement: - case State.UnionFinal: + case State.UnionNext: switch ( c ) { - case ' ': - case '\t': - break; case ']': + if ( i < n && query[i] != '.' && query[i] != '[' ) + throw new NotSupportedException( $"Invalid character after `]` at pos {i - 1}." ); state = State.DotChild; selectorStart = i; break; case '\'': case '"': - if ( state != State.UnionNextElement ) - throw new NotSupportedException( $"Invalid bracket syntax at pos {i - 1}." ); - - state = State.UnionElementQuoted; - literalDelimiter = c; - selectorStart = i - 1; + state = State.UnionItem; + quoteChar = c; + selectorStart = i - 1; // capture the quote character + inQuotes = true; + inFilter = false; break; default: - if ( state != State.UnionNextElement ) - throw new NotSupportedException( $"Invalid bracket syntax at pos {i - 1}." ); - - state = State.UnionElement; + state = State.UnionItem; i--; // replay character selectorStart = i; - break; } break; + case State.Finish: + selectorSpan = GetSelectorSpan( state, query, selectorStart, i ); + if ( !selectorSpan.IsEmpty ) + { + var finalKind = selectorSpan switch + { + "*" => SelectorKind.Wildcard, + ".." => SelectorKind.Descendant, + _ => SelectorKind.Name + }; + + if ( finalKind == SelectorKind.Name ) + { + ThrowIfQuoted( selectorSpan ); + ThrowIfInvalidUnquotedName( selectorSpan ); + } + + InsertToken( tokens, GetSelectorDescriptor( finalKind, selectorSpan ) ); + } + + state = State.Final; + break; + default: throw new InvalidOperationException(); } - } while ( i < n ); + } while ( state != State.Final ); + + return BuildJsonPathQuery( query, tokens ); + } - // handle the trailing bits - state = State.Final; + private static JsonPathQuery BuildJsonPathQuery( ReadOnlySpan query, IList segments ) + { + if ( segments == null || segments.Count == 0 ) + return new JsonPathQuery( query.ToString(), JsonPathSegment.Final, false ); - var finalSelector = GetSelector( state, query, selectorStart, i ); + // link the segments - if ( finalSelector != null ) + for ( var index = 0; index < segments.Count; index++ ) { - var finalKind = finalSelector switch - { - "*" => SelectorKind.Wildcard, - ".." => SelectorKind.Descendant, - _ => SelectorKind.Dot - }; + var segment = segments[index]; - InsertToken( tokens, new SelectorDescriptor { SelectorKind = finalKind, Value = finalSelector } ); + segment.Next = index != segments.Count - 1 + ? segments[index + 1] + : JsonPathSegment.Final; } - // return tokenized query as a segment list + var rootSegment = segments.First(); // first segment is the root + var normalized = rootSegment.IsNormalized; - return TokensToSegment( tokens ); + return new JsonPathQuery( query.ToString(), rootSegment, normalized ); } - private static JsonPathSegment TokensToSegment( IList tokens ) + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static SelectorDescriptor GetSelectorDescriptor( SelectorKind selectorKind, ReadOnlySpan selectorSpan, bool nullable = true ) { - if ( tokens == null || tokens.Count == 0 ) - return JsonPathSegment.Final; - - // set the next properties + var selectorValue = selectorSpan.IsEmpty && nullable ? null : selectorSpan.ToString(); + return new SelectorDescriptor { SelectorKind = selectorKind, Value = selectorValue }; + } - for ( var index = 0; index < tokens.Count; index++ ) - { - tokens[index].Next = index != tokens.Count - 1 - ? tokens[index + 1] - : JsonPathSegment.Final; - } + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static SelectorDescriptor GetSelectorDescriptor( SelectorKind selectorKind, in SpanBuilder builder, bool nullable = true ) + { + var selectorValue = builder.IsEmpty && !nullable ? null : builder.ToString(); + return new SelectorDescriptor { SelectorKind = selectorKind, Value = selectorValue }; + } - return tokens.First(); + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static ReadOnlySpan GetSelectorSpan( State state, ReadOnlySpan buffer, int start, int stop ) + { + var adjust = state == State.Finish || state == State.Final ? 0 : 1; // non-final states have already advanced to the next character, so we need to subtract 1 + var length = stop - start - adjust; + return length <= 0 ? [] : buffer.Slice( start, length ).Trim(); } - private static SelectorKind GetSelectorKind( string selector ) + private static SelectorKind GetValidSelectorKind( ReadOnlySpan selector ) { + // selector order matters + + switch ( selector ) + { + case "*": + return SelectorKind.Wildcard; + case "..": + return SelectorKind.Descendant; + } + if ( IsQuoted( selector ) ) return SelectorKind.Name; - if ( IsIndex( selector ) ) + if ( IsIndex( selector, out var isValid, out var reason ) ) + { + if ( !isValid ) // it is an index, but invalid + throw new NotSupportedException( reason ); + return SelectorKind.Index; + } if ( IsFilter( selector ) ) return SelectorKind.Filter; - if ( IsSlice( selector ) ) + if ( IsSlice( selector, out isValid, out reason ) ) + { + if ( !isValid ) // it is a slice, but invalid + throw new NotSupportedException( reason ); + return SelectorKind.Slice; + } - return selector switch - { - "*" => SelectorKind.Wildcard, - ".." => SelectorKind.Descendant, - _ => SelectorKind.Undefined - }; + return SelectorKind.Undefined; } - private static bool IsSlice( ReadOnlySpan input ) + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static void InsertToken( ICollection tokens, SelectorDescriptor selector ) + { + if ( selector?.Value == null ) // ignore null selectors + return; + + InsertToken( tokens, [selector] ); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static void InsertToken( ICollection tokens, SelectorDescriptor[] selectors ) + { + if ( selectors == null || selectors.Length == 0 ) // ignore empty selectors + return; + + tokens.Add( new JsonPathSegment( selectors ) ); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool IsFilter( ReadOnlySpan input ) + { + // Check if the input starts with '?' and is at least two characters long + return input.Length > 1 && input[0] == '?'; + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool IsIndex( ReadOnlySpan input, out bool isValid, out string reason ) + { + return IsValidNumber( input, out isValid, out reason ); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool IsQuoted( ReadOnlySpan input ) + { + return input.Length > 1 && input[0] == '"' && input[^1] == '"' || input[0] == '\'' && input[^1] == '\''; + } + + private static bool IsSlice( ReadOnlySpan input, out bool isValid, out string reason ) { var index = 0; + isValid = true; + reason = string.Empty; + var partCount = 0; - // First part (optional number) - if ( !IsOptionalNumber( input, ref index ) ) - return false; + SkipWhitespace( input, ref index ); - // Optional colon - if ( index < input.Length && input[index] == ':' ) + do { - index++; + // Validate each part (optional number) + if ( !ValidatePart( input, ref index, ref isValid, ref reason ) ) + { + if ( !isValid ) + reason = "Invalid number in slice."; + return partCount > 0; // Return true if at least one colon was found, indicating it was intended as a slice + } - // Second part (optional number) - if ( !IsOptionalNumber( input, ref index ) ) - return false; + partCount++; - // Optional second colon - if ( index < input.Length && input[index] == ':' ) - { - index++; + SkipWhitespace( input, ref index ); - // Third part (optional number) - if ( !IsOptionalNumber( input, ref index ) ) - return false; - } + // Check for optional colon + if ( index >= input.Length || input[index] != ':' ) + break; + + index++; + SkipWhitespace( input, ref index ); + + } while ( partCount < 3 && index < input.Length ); + + if ( index != input.Length ) + { + isValid = false; + reason = "Unexpected characters at the end of slice."; } - var result = index == input.Length; - return result; + return partCount > 0; // Return true if at least one colon was found, indicating it was intended as a slice - static bool IsOptionalNumber( ReadOnlySpan span, ref int idx ) + // Helper method to validate each part of the slice + static bool ValidatePart( ReadOnlySpan span, ref int idx, ref bool isValid, ref string reason ) { + SkipWhitespace( span, ref idx ); + var start = idx; + var length = span.Length; - if ( idx < span.Length && (span[idx] == '-' || span[idx] == '+') ) + if ( idx < length && (span[idx] == '-') ) idx++; - while ( idx < span.Length && char.IsDigit( span[idx] ) ) + while ( idx < length && char.IsDigit( span[idx] ) ) idx++; - var isValid = idx > start || start == idx; - return isValid; // True if there was a number or just an optional sign + // Allow empty + if ( start == idx ) + return true; + + // Check for leading zeros in unsigned or signed numbers + if ( !IsValidNumber( span[start..idx], out isValid, out reason ) ) + return false; + + var isValidNumber = idx > start || start == idx; + + if ( !isValidNumber ) + { + isValid = false; + reason = "Invalid number format."; + } + + return isValidNumber; // True if there was a number or just an optional sign + } + + // Helper method to skip whitespace + [MethodImpl( MethodImplOptions.AggressiveInlining )] + static void SkipWhitespace( ReadOnlySpan span, ref int idx ) + { + var length = span.Length; + while ( idx < length && char.IsWhiteSpace( span[idx] ) ) + idx++; } } - private static bool IsFilter( ReadOnlySpan input ) + private static bool IsValidNumber( ReadOnlySpan input, out bool isValid, out string reason ) { - if ( input.Length < 2 || input[0] != '?' ) + isValid = true; + reason = string.Empty; + + var length = input.Length; + + if ( length == 0 ) + { + isValid = false; + reason = "Input is empty."; return false; + } + + int start = 0; + + // Handle optional leading negative sign + if ( input[0] == '-' ) + { + start = 1; + if ( length == 1 ) + { + isValid = false; + reason = "Invalid negative number."; + return false; + } + } - var start = 1; - var end = input.Length; + // Check for leading zeros + if ( input[start] == '0' && length > (start + 1) ) + { + isValid = false; + reason = "Leading zeros are not allowed."; + return false; + } - if ( input[1] == '(' ) + // Check if all remaining characters are digits + for ( var i = start; i < length; i++ ) { - start = 2; - if ( input[^1] == ')' ) - end--; + char c = input[i]; + + if ( c >= '0' && c <= '9' ) + continue; + + isValid = false; + reason = "Input contains non-digit characters."; + return false; } - var result = start < end; + // Try parse to detect overflow + if ( long.TryParse( input, out _ ) ) + return true; // It's a valid number - return result; + isValid = false; + reason = "Input is too large."; + return false; } - private static bool IsIndex( ReadOnlySpan input ) + private static void ThrowIfQuoted( ReadOnlySpan value ) { - foreach ( var ch in input ) + if ( IsQuoted( value ) ) + throw new NotSupportedException( $"Quoted member names are not allowed in dot notation: {value}" ); + } + + private static void ThrowIfInvalidUnquotedName( ReadOnlySpan name ) + { + if ( name.IsEmpty ) + throw new NotSupportedException( "Selector name cannot be null." ); + + // Validate the first character + if ( !IsValidFirstChar( name[0] ) ) + throw new NotSupportedException( $"Selector name cannot start with `{name[0]}`." ); + + // Validate subsequent characters + for ( int i = 1; i < name.Length; i++ ) { - if ( !char.IsDigit( ch ) ) - return false; + if ( !IsValidSubsequentChar( name[i] ) ) + throw new NotSupportedException( $"Selector name cannot contain `{name[i]}`." ); } - return true; + return; + + static bool IsValidFirstChar( char c ) => char.IsLetter( c ) || c == '_' || c >= 0x80; + static bool IsValidSubsequentChar( char c ) => char.IsLetterOrDigit( c ) || c == '_' || c == '-' || c >= 0x80; } - private static bool IsQuoted( ReadOnlySpan input ) + private static void ThrowIfInvalidQuotedName( ReadOnlySpan name ) { - return (input[0] == '"' && input[^1] == '"') || (input[0] == '\'' && input[^1] == '\''); + if ( name.IsEmpty ) + throw new NotSupportedException( "Selector name cannot be empty." ); + + char quoteChar = name[0]; + if ( name.Length < 2 || (quoteChar != '"' && quoteChar != '\'') || name[^1] != quoteChar ) + throw new NotSupportedException( "Quoted name must start and end with the same quote character, either double or single quote." ); + + for ( int i = 1; i < name.Length - 1; i++ ) + { + if ( name[i] == '\\' ) + { + // Check if it's a valid escape sequence + if ( i + 1 >= name.Length - 1 || !IsValidEscapeChar( name[i + 1], quoteChar ) ) + throw new NotSupportedException( "Invalid escape sequence in quoted name." ); + + if ( name[i + 1] == 'u' ) + { + // Ensure it's a valid Unicode escape sequence (e.g., \u263a) + if ( i + 5 >= name.Length - 1 || !IsValidUnicodeEscapeSequence( name.Slice( i, 6 ) ) ) + throw new NotSupportedException( "Invalid Unicode escape sequence in quoted name." ); + i += 5; // Skip the Unicode escape sequence + } + else + { + i++; // Skip the regular escape character + } + } + else if ( name[i] == quoteChar ) + { + // Unescaped quotes are not allowed inside the quoted name. + throw new NotSupportedException( "Unescaped quote characters are not allowed inside a quoted name." ); + } + else if ( name[i] <= '\u001F' ) + { + // Control characters (U+0000 to U+001F) are not allowed. + throw new NotSupportedException( $"Control character '\\u{(int) name[i]:x4}' is not allowed in a quoted name." ); + } + } + + return; + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + static bool IsValidEscapeChar( char escapeChar, char quoteChar ) + { + return + escapeChar == quoteChar || + escapeChar == '\\' || + escapeChar == '/' || escapeChar == 'b' || + escapeChar == 'f' || escapeChar == 'n' || + escapeChar == 'r' || escapeChar == 't' || + escapeChar == 'u' + ; + } + + static bool IsValidUnicodeEscapeSequence( ReadOnlySpan span ) + { + if ( span.Length != 6 || span[1] != 'u' ) + return false; + + for ( int i = 2; i < 6; i++ ) + { + if ( !char.IsAsciiHexDigit( span[i] ) ) + return false; + } + + return true; + } } } diff --git a/src/Hyperbee.Json/JsonPathSegment.cs b/src/Hyperbee.Json/JsonPathSegment.cs index 5d95c1c4..f78422df 100644 --- a/src/Hyperbee.Json/JsonPathSegment.cs +++ b/src/Hyperbee.Json/JsonPathSegment.cs @@ -2,6 +2,9 @@ namespace Hyperbee.Json; +public record JsonPathQuery( string Query, JsonPathSegment Segments, bool Normalized ); + + [DebuggerDisplay( "{Value}, SelectorKind = {SelectorKind}" )] public record SelectorDescriptor { @@ -63,6 +66,24 @@ public IEnumerable AsEnumerable() } } + public bool IsNormalized + { + get + { + var current = this; + + while ( current != Final ) + { + if ( !current.IsSingular ) + return false; + + current = current.Next; + } + + return true; + } + } + private bool InitIsSingular() { // singular is one selector that is not a group diff --git a/src/Hyperbee.Json/JsonPathSliceSyntaxHelper.cs b/src/Hyperbee.Json/JsonPathSliceSyntaxHelper.cs index ecec7863..c3bb999b 100644 --- a/src/Hyperbee.Json/JsonPathSliceSyntaxHelper.cs +++ b/src/Hyperbee.Json/JsonPathSliceSyntaxHelper.cs @@ -5,6 +5,7 @@ namespace Hyperbee.Json; internal static class JsonPathSliceSyntaxHelper { + // parse slice expression and return normalized bounds public static (int Lower, int Upper, int Step) ParseExpression( ReadOnlySpan sliceExpr, int length, bool reverse = false ) { // parse the slice expression and return normalized bounds @@ -34,7 +35,7 @@ public static (int Lower, int Upper, int Step) ParseExpression( ReadOnlySpan part, int length ) + { + // a little magic for overflow and underflow conditions cause by massive steps. + // just scope the step to length + 1 or -length - 1. + + if ( !part.IsEmpty && long.TryParse( part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n ) ) + { + return n switch + { + > 0 when n > length => length + 1, + < 0 when -n > length => -(length + 1), + _ => (int) n + }; + } + + return 1; + } + static int ParsePart( ReadOnlySpan part, int defaultValue ) { - if ( !part.IsEmpty ) - return int.TryParse( part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n ) ? n : defaultValue; + if ( !part.IsEmpty && int.TryParse( part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n ) ) + return n; return defaultValue; } @@ -86,7 +105,7 @@ private static (int Lower, int Upper, int Step) GetBoundedValues( int start, int private static (int Lower, int Upper, int Step) ReverseBoundedValues( int lower, int upper, int step ) { - step *= -1; + step = -step; // adjust upper for correct reverse iteration // upper may not be lower + (n * step) aligned diff --git a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs b/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs index 1d7b301d..b2df8b6f 100644 --- a/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs +++ b/test/Hyperbee.Json.Benchmark/FilterExpressionParserEvaluator.cs @@ -9,8 +9,8 @@ namespace Hyperbee.Json.Benchmark; public class FilterExpressionParserEvaluator { - private FilterContext _nodeExecutionContext; - private FilterContext _elementExecutionContext; + private FilterParserContext _nodeExecutionParserContext; + private FilterParserContext _elementExecutionParserContext; [Params( "(\"world\" == 'world') && (true || false)" )] public string Filter; @@ -18,20 +18,20 @@ public class FilterExpressionParserEvaluator [GlobalSetup] public void Setup() { - _nodeExecutionContext = new FilterContext( new NodeTypeDescriptor() ); + _nodeExecutionParserContext = new FilterParserContext( new NodeTypeDescriptor() ); - _elementExecutionContext = new FilterContext( new ElementTypeDescriptor() ); + _elementExecutionParserContext = new FilterParserContext( new ElementTypeDescriptor() ); } [Benchmark] public void JsonPathFilterParser_JsonElement() { - FilterParser.Parse( Filter, _elementExecutionContext ); + FilterParser.Parse( Filter, _elementExecutionParserContext ); } [Benchmark] public void JsonPathFilterParser_JsonNode() { - FilterParser.Parse( Filter, _nodeExecutionContext ); + FilterParser.Parse( Filter, _nodeExecutionParserContext ); } } diff --git a/test/Hyperbee.Json.Cts/AssertExtensions.cs b/test/Hyperbee.Json.Cts/AssertExtensions.cs new file mode 100644 index 00000000..4e0f379b --- /dev/null +++ b/test/Hyperbee.Json.Cts/AssertExtensions.cs @@ -0,0 +1,61 @@ +namespace Hyperbee.Json.Cts; + +public static class AssertExtensions +{ + public static void ThrowsAny( Action action ) + where T1 : Exception + where T2 : Exception + { + ThrowsAnyInternal( action, typeof( T1 ), typeof( T2 ) ); + } + + public static void ThrowsAny( Action action ) + where T1 : Exception + where T2 : Exception + where T3 : Exception + { + ThrowsAnyInternal( action, typeof( T1 ), typeof( T2 ), typeof( T3 ) ); + } + + public static void ThrowsAny( Action action ) + where T1 : Exception + where T2 : Exception + where T3 : Exception + where T4 : Exception + { + ThrowsAnyInternal( action, typeof( T1 ), typeof( T2 ), typeof( T3 ), typeof( T4 ) ); + } + + public static void ThrowsAny( Action action ) + where T1 : Exception + where T2 : Exception + where T3 : Exception + where T4 : Exception + where T5 : Exception + { + ThrowsAnyInternal( action, typeof( T1 ), typeof( T2 ), typeof( T3 ), typeof( T4 ), typeof( T5 ) ); + } + + private static void ThrowsAnyInternal( Action action, params Type[] expectedExceptionTypes ) + { + Exception? caughtException = null; + + try + { + action(); + } + catch ( Exception? ex ) + { + caughtException = ex; + } + + if ( caughtException == null ) + { + Assert.Fail( $"No exception was thrown. Expected one of: {string.Join( ", ", expectedExceptionTypes.Select( t => t.Name ) )}" ); + } + else if ( !expectedExceptionTypes.Any( e => e.IsInstanceOfType( caughtException ) ) ) + { + Assert.Fail( $"Exception of type {caughtException.GetType().Name} was thrown, but none of the expected types were: {string.Join( ", ", expectedExceptionTypes.Select( t => t.Name ) )}" ); + } + } +} diff --git a/test/Hyperbee.Json.Cts/Hyperbee.Json.Cts.csproj b/test/Hyperbee.Json.Cts/Hyperbee.Json.Cts.csproj new file mode 100644 index 00000000..5448cc6f --- /dev/null +++ b/test/Hyperbee.Json.Cts/Hyperbee.Json.Cts.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/test/Hyperbee.Json.Cts/TestHelper.cs b/test/Hyperbee.Json.Cts/TestHelper.cs new file mode 100644 index 00000000..d7bfd2c1 --- /dev/null +++ b/test/Hyperbee.Json.Cts/TestHelper.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Nodes; + +namespace Hyperbee.Json.Cts; + +internal static class TestHelper +{ + // Result Helpers + + public static JsonArray ConvertToJsonArraySet( JsonNode jsonNode ) + { + if ( jsonNode is JsonArray jsonArray && jsonArray[0] is JsonArray ) + return jsonArray; + + JsonArray jsonArraySet = new JsonArray( jsonNode ); + + return jsonArraySet; + } + + public static JsonArray ConvertToJsonArray( IEnumerable nodes, bool force = false ) + { + var nodeArray = nodes.ToArray(); + + if ( !force && nodeArray.Length == 1 && nodeArray[0] is JsonArray array ) + return array; + + var jsonArray = new JsonArray(); + + foreach ( var node in nodeArray ) + { + jsonArray.Add( CopyNode( node ) ); + } + + return jsonArray; + + static JsonNode? CopyNode( JsonNode? node ) + { + return node == null ? null : JsonNode.Parse( node.ToJsonString() ); + } + } + + public static bool MatchAny( IEnumerable results, JsonNode expected ) + { + var expectedSet = ConvertToJsonArraySet( expected ); + var compare = ConvertToJsonArray( results ); + return expectedSet.Any( expect => JsonNode.DeepEquals( expect, compare ) ); + } + + public static bool MatchOne( IEnumerable results, JsonNode expected ) + { + var expect = expected as JsonArray; + var compare = ConvertToJsonArray( results, force: true ); + return JsonNode.DeepEquals( expect, compare ); + } +} diff --git a/test/Hyperbee.Json.Cts/Tests/cts-basic-tests.cs b/test/Hyperbee.Json.Cts/Tests/cts-basic-tests.cs new file mode 100644 index 00000000..43a743a7 --- /dev/null +++ b/test/Hyperbee.Json.Cts/Tests/cts-basic-tests.cs @@ -0,0 +1,1068 @@ +// This file was auto generated. + +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Cts.Tests +{ + [TestClass] + public class CtsBasicTest + { + + [TestMethod( @"root (1)" )] + public void Test_root_1() + { + var selector = "$"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + [ + "first", + "second" + ] + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"no leading whitespace (2)" )] + public void Test_no_leading_whitespace_2() + { + var selector = " $"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"no trailing whitespace (3)" )] + public void Test_no_trailing_whitespace_3() + { + var selector = "$ "; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"name shorthand (4)" )] + public void Test_name_shorthand_4() + { + var selector = "$.a"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"name shorthand, extended unicode ☺ (5)" )] + public void Test_name_shorthand__extended_unicode___5() + { + var selector = "$.☺"; + var document = JsonNode.Parse( + """ + { + "☺": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"name shorthand, underscore (6)" )] + public void Test_name_shorthand__underscore_6() + { + var selector = "$._"; + var document = JsonNode.Parse( + """ + { + "_": "A", + "_foo": "B" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"name shorthand, symbol (7)" )] + public void Test_name_shorthand__symbol_7() + { + var selector = "$.&"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"name shorthand, number (8)" )] + public void Test_name_shorthand__number_8() + { + var selector = "$.1"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"name shorthand, absent data (9)" )] + public void Test_name_shorthand__absent_data_9() + { + var selector = "$.c"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"name shorthand, array data (10)" )] + public void Test_name_shorthand__array_data_10() + { + var selector = "$.a"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"wildcard shorthand, object data (11)" )] + public void Test_wildcard_shorthand__object_data_11() + { + var selector = "$.*"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expectOneOf = JsonNode.Parse( + """ + [ + [ + "A", + "B" + ], + [ + "B", + "A" + ] + ] + """ ); + + var match = TestHelper.MatchAny( results, expectOneOf! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"wildcard shorthand, array data (12)" )] + public void Test_wildcard_shorthand__array_data_12() + { + var selector = "$.*"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"wildcard selector, array data (13)" )] + public void Test_wildcard_selector__array_data_13() + { + var selector = "$[*]"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"wildcard shorthand, then name shorthand (14)" )] + public void Test_wildcard_shorthand__then_name_shorthand_14() + { + var selector = "$.*.a"; + var document = JsonNode.Parse( + """ + { + "x": { + "a": "Ax", + "b": "Bx" + }, + "y": { + "a": "Ay", + "b": "By" + } + } + """ ); + var results = document.Select( selector ); + var expectOneOf = JsonNode.Parse( + """ + [ + [ + "Ax", + "Ay" + ], + [ + "Ay", + "Ax" + ] + ] + """ ); + + var match = TestHelper.MatchAny( results, expectOneOf! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors (15)" )] + public void Test_multiple_selectors_15() + { + var selector = "$[0,2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 2 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, space instead of comma (16)" )] + public void Test_multiple_selectors__space_instead_of_comma_16() + { + var selector = "$[0 2]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"multiple selectors, name and index, array data (17)" )] + public void Test_multiple_selectors__name_and_index__array_data_17() + { + var selector = "$['a',1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, name and index, object data (18)" )] + public void Test_multiple_selectors__name_and_index__object_data_18() + { + var selector = "$['a',1]"; + var document = JsonNode.Parse( + """ + { + "a": 1, + "b": 2 + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, index and slice (19)" )] + public void Test_multiple_selectors__index_and_slice_19() + { + var selector = "$[1,5:7]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1, + 5, + 6 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, index and slice, overlapping (20)" )] + public void Test_multiple_selectors__index_and_slice__overlapping_20() + { + var selector = "$[1,0:3]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1, + 0, + 1, + 2 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, duplicate index (21)" )] + public void Test_multiple_selectors__duplicate_index_21() + { + var selector = "$[1,1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, wildcard and index (22)" )] + public void Test_multiple_selectors__wildcard_and_index_22() + { + var selector = "$[*,1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, wildcard and name (23)" )] + public void Test_multiple_selectors__wildcard_and_name_23() + { + var selector = "$[*,'a']"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expectOneOf = JsonNode.Parse( + """ + [ + [ + "A", + "B", + "A" + ], + [ + "B", + "A", + "A" + ] + ] + """ ); + + var match = TestHelper.MatchAny( results, expectOneOf! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, wildcard and slice (24)" )] + public void Test_multiple_selectors__wildcard_and_slice_24() + { + var selector = "$[*,0:2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 0, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, multiple wildcards (25)" )] + public void Test_multiple_selectors__multiple_wildcards_25() + { + var selector = "$[*,*]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 0, + 1, + 2 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"empty segment (26)" )] + public void Test_empty_segment_26() + { + var selector = "$[]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"descendant segment, index (27)" )] + public void Test_descendant_segment__index_27() + { + var selector = "$..[1]"; + var document = JsonNode.Parse( + """ + { + "o": [ + 0, + 1, + [ + 2, + 3 + ] + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1, + 3 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, name shorthand (28)" )] + public void Test_descendant_segment__name_shorthand_28() + { + var selector = "$..a"; + var document = JsonNode.Parse( + """ + { + "o": [ + { + "a": "b" + }, + { + "a": "c" + } + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "b", + "c" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, wildcard shorthand, array data (29)" )] + public void Test_descendant_segment__wildcard_shorthand__array_data_29() + { + var selector = "$..*"; + var document = JsonNode.Parse( + """ + [ + 0, + 1 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, wildcard selector, array data (30)" )] + public void Test_descendant_segment__wildcard_selector__array_data_30() + { + var selector = "$..[*]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, wildcard selector, nested arrays (31)" )] + public void Test_descendant_segment__wildcard_selector__nested_arrays_31() + { + var selector = "$..[*]"; + var document = JsonNode.Parse( + """ + [ + [ + [ + 1 + ] + ], + [ + 2 + ] + ] + """ ); + var results = document.Select( selector ); + var expectOneOf = JsonNode.Parse( + """ + [ + [ + [ + [ + 1 + ] + ], + [ + 2 + ], + [ + 1 + ], + 1, + 2 + ], + [ + [ + [ + 1 + ] + ], + [ + 2 + ], + [ + 1 + ], + 2, + 1 + ] + ] + """ ); + + var match = TestHelper.MatchAny( results, expectOneOf! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, wildcard selector, nested objects (32)" )] + public void Test_descendant_segment__wildcard_selector__nested_objects_32() + { + var selector = "$..[*]"; + var document = JsonNode.Parse( + """ + { + "a": { + "c": { + "e": 1 + } + }, + "b": { + "d": 2 + } + } + """ ); + var results = document.Select( selector ); + var expectOneOf = JsonNode.Parse( + """ + [ + [ + { + "c": { + "e": 1 + } + }, + { + "d": 2 + }, + { + "e": 1 + }, + 1, + 2 + ], + [ + { + "c": { + "e": 1 + } + }, + { + "d": 2 + }, + { + "e": 1 + }, + 2, + 1 + ], + [ + { + "c": { + "e": 1 + } + }, + { + "d": 2 + }, + 2, + { + "e": 1 + }, + 1 + ], + [ + { + "d": 2 + }, + { + "c": { + "e": 1 + } + }, + { + "e": 1 + }, + 1, + 2 + ], + [ + { + "d": 2 + }, + { + "c": { + "e": 1 + } + }, + { + "e": 1 + }, + 2, + 1 + ], + [ + { + "d": 2 + }, + { + "c": { + "e": 1 + } + }, + 2, + { + "e": 1 + }, + 1 + ] + ] + """ ); + + var match = TestHelper.MatchAny( results, expectOneOf! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, wildcard shorthand, object data (33)" )] + public void Test_descendant_segment__wildcard_shorthand__object_data_33() + { + var selector = "$..*"; + var document = JsonNode.Parse( + """ + { + "a": "b" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "b" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, wildcard shorthand, nested data (34)" )] + public void Test_descendant_segment__wildcard_shorthand__nested_data_34() + { + var selector = "$..*"; + var document = JsonNode.Parse( + """ + { + "o": [ + { + "a": "b" + } + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + [ + { + "a": "b" + } + ], + { + "a": "b" + }, + "b" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, multiple selectors (35)" )] + public void Test_descendant_segment__multiple_selectors_35() + { + var selector = "$..['a','d']"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "b", + "e", + "c", + "f" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"descendant segment, object traversal, multiple selectors (36)" )] + public void Test_descendant_segment__object_traversal__multiple_selectors_36() + { + var selector = "$..['a','d']"; + var document = JsonNode.Parse( + """ + { + "x": { + "a": "b", + "d": "e" + }, + "y": { + "a": "c", + "d": "f" + } + } + """ ); + var results = document.Select( selector ); + var expectOneOf = JsonNode.Parse( + """ + [ + [ + "b", + "e", + "c", + "f" + ], + [ + "c", + "f", + "b", + "e" + ] + ] + """ ); + + var match = TestHelper.MatchAny( results, expectOneOf! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"bald descendant segment (37)" )] + public void Test_bald_descendant_segment_37() + { + var selector = "$.."; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + } +} + diff --git a/test/Hyperbee.Json.Cts/Tests/cts-filter-tests.cs b/test/Hyperbee.Json.Cts/Tests/cts-filter-tests.cs new file mode 100644 index 00000000..d3287966 --- /dev/null +++ b/test/Hyperbee.Json.Cts/Tests/cts-filter-tests.cs @@ -0,0 +1,3884 @@ +// This file was auto generated. + +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Cts.Tests +{ + [TestClass] + public class CtsFilterTest + { + + [TestMethod( @"existence, without segments (1)" )] + public void Test_existence__without_segments_1() + { + var selector = "$[?@]"; + var document = JsonNode.Parse( + """ + { + "a": 1, + "b": null + } + """ ); + var results = document.Select( selector ); + var expectOneOf = JsonNode.Parse( + """ + [ + [ + 1, + null + ], + [ + null, + 1 + ] + ] + """ ); + + var match = TestHelper.MatchAny( results, expectOneOf! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"existence (2)" )] + public void Test_existence_2() + { + var selector = "$[?@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"existence, present with null (3)" )] + public void Test_existence__present_with_null_3() + { + var selector = "$[?@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": null, + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": null, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals string, single quotes (4)" )] + public void Test_equals_string__single_quotes_4() + { + var selector = "$[?@.a=='b']"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals numeric string, single quotes (5)" )] + public void Test_equals_numeric_string__single_quotes_5() + { + var selector = "$[?@.a=='1']"; + var document = JsonNode.Parse( + """ + [ + { + "a": "1", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "1", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals string, double quotes (6)" )] + public void Test_equals_string__double_quotes_6() + { + var selector = "$[?@.a==\"b\"]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals numeric string, double quotes (7)" )] + public void Test_equals_numeric_string__double_quotes_7() + { + var selector = "$[?@.a==\"1\"]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "1", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "1", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number (8)" )] + public void Test_equals_number_8() + { + var selector = "$[?@.a==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": 2, + "d": "f" + }, + { + "a": "1", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals null (9)" )] + public void Test_equals_null_9() + { + var selector = "$[?@.a==null]"; + var document = JsonNode.Parse( + """ + [ + { + "a": null, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": null, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals null, absent from data (10)" )] + public void Test_equals_null__absent_from_data_10() + { + var selector = "$[?@.a==null]"; + var document = JsonNode.Parse( + """ + [ + { + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals true (11)" )] + public void Test_equals_true_11() + { + var selector = "$[?@.a==true]"; + var document = JsonNode.Parse( + """ + [ + { + "a": true, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": true, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals false (12)" )] + public void Test_equals_false_12() + { + var selector = "$[?@.a==false]"; + var document = JsonNode.Parse( + """ + [ + { + "a": false, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": false, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals self (13)" )] + public void Test_equals_self_13() + { + var selector = "$[?@==@]"; + var document = JsonNode.Parse( + """ + [ + 1, + null, + true, + { + "a": "b" + }, + [ + false + ] + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1, + null, + true, + { + "a": "b" + }, + [ + false + ] + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"deep equality, arrays (14)" )] + public void Test_deep_equality__arrays_14() + { + var selector = "$[?@.a==@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": false, + "b": [ + 1, + 2 + ] + }, + { + "a": [ + [ + 1, + [ + 2 + ] + ] + ], + "b": [ + [ + 1, + [ + 2 + ] + ] + ] + }, + { + "a": [ + [ + 1, + [ + 2 + ] + ] + ], + "b": [ + [ + [ + 2 + ], + 1 + ] + ] + }, + { + "a": [ + [ + 1, + [ + 2 + ] + ] + ], + "b": [ + [ + 1, + 2 + ] + ] + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": [ + [ + 1, + [ + 2 + ] + ] + ], + "b": [ + [ + 1, + [ + 2 + ] + ] + ] + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"deep equality, objects (15)" )] + public void Test_deep_equality__objects_15() + { + var selector = "$[?@.a==@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": false, + "b": { + "x": 1, + "y": { + "z": 1 + } + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "x": 1, + "y": { + "z": 1 + } + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "y": { + "z": 1 + }, + "x": 1 + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "x": 1 + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "x": 1, + "y": { + "z": 2 + } + } + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "x": 1, + "y": { + "z": 1 + } + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "y": { + "z": 1 + }, + "x": 1 + } + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals string, single quotes (16)" )] + public void Test_not_equals_string__single_quotes_16() + { + var selector = "$[?@.a!='b']"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals numeric string, single quotes (17)" )] + public void Test_not_equals_numeric_string__single_quotes_17() + { + var selector = "$[?@.a!='1']"; + var document = JsonNode.Parse( + """ + [ + { + "a": "1", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals string, single quotes, different type (18)" )] + public void Test_not_equals_string__single_quotes__different_type_18() + { + var selector = "$[?@.a!='b']"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals string, double quotes (19)" )] + public void Test_not_equals_string__double_quotes_19() + { + var selector = "$[?@.a!=\"b\"]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals numeric string, double quotes (20)" )] + public void Test_not_equals_numeric_string__double_quotes_20() + { + var selector = "$[?@.a!=\"1\"]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "1", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals string, double quotes, different types (21)" )] + public void Test_not_equals_string__double_quotes__different_types_21() + { + var selector = "$[?@.a!=\"b\"]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals number (22)" )] + public void Test_not_equals_number_22() + { + var selector = "$[?@.a!=1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "e" + }, + { + "a": 2, + "d": "f" + }, + { + "a": "1", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 2, + "d": "f" + }, + { + "a": "1", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals number, different types (23)" )] + public void Test_not_equals_number__different_types_23() + { + var selector = "$[?@.a!=1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals null (24)" )] + public void Test_not_equals_null_24() + { + var selector = "$[?@.a!=null]"; + var document = JsonNode.Parse( + """ + [ + { + "a": null, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals null, absent from data (25)" )] + public void Test_not_equals_null__absent_from_data_25() + { + var selector = "$[?@.a!=null]"; + var document = JsonNode.Parse( + """ + [ + { + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals true (26)" )] + public void Test_not_equals_true_26() + { + var selector = "$[?@.a!=true]"; + var document = JsonNode.Parse( + """ + [ + { + "a": true, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"not-equals false (27)" )] + public void Test_not_equals_false_27() + { + var selector = "$[?@.a!=false]"; + var document = JsonNode.Parse( + """ + [ + { + "a": false, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"less than string, single quotes (28)" )] + public void Test_less_than_string__single_quotes_28() + { + var selector = "$[?@.a<'c']"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"less than string, double quotes (29)" )] + public void Test_less_than_string__double_quotes_29() + { + var selector = "$[?@.a<\"c\"]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"less than number (30)" )] + public void Test_less_than_number_30() + { + var selector = "$[?@.a<10]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "e" + }, + { + "a": 10, + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": 20, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"less than null (31)" )] + public void Test_less_than_null_31() + { + var selector = "$[?@.a( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"non-singular query in comparison, all children (65)" )] + public void Test_non_singular_query_in_comparison__all_children_65() + { + var selector = "$[?@[*]==0]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"non-singular query in comparison, descendants (66)" )] + public void Test_non_singular_query_in_comparison__descendants_66() + { + var selector = "$[?@..a==0]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"non-singular query in comparison, combined (67)" )] + public void Test_non_singular_query_in_comparison__combined_67() + { + var selector = "$[?@.a[*].a==0]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"nested (68)" )] + public void Test_nested_68() + { + var selector = "$[?@[?@>1]]"; + var document = JsonNode.Parse( + """ + [ + [ + 0 + ], + [ + 0, + 1 + ], + [ + 0, + 1, + 2 + ], + [ + 42 + ] + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + [ + 0, + 1, + 2 + ], + [ + 42 + ] + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"name segment on primitive, selects nothing (69)" )] + public void Test_name_segment_on_primitive__selects_nothing_69() + { + var selector = "$[?@.a == 1]"; + var document = JsonNode.Parse( + """ + { + "a": 1 + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"name segment on array, selects nothing (70)" )] + public void Test_name_segment_on_array__selects_nothing_70() + { + var selector = "$[?@['0'] == 5]"; + var document = JsonNode.Parse( + """ + [ + [ + 5, + 6 + ] + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"index segment on object, selects nothing (71)" )] + public void Test_index_segment_on_object__selects_nothing_71() + { + var selector = "$[?@[0] == 5]"; + var document = JsonNode.Parse( + """ + [ + { + "0": 5 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"relative non-singular query, index, equal (72)" )] + public void Test_relative_non_singular_query__index__equal_72() + { + var selector = "$[?(@[0, 0]==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, index, not equal (73)" )] + public void Test_relative_non_singular_query__index__not_equal_73() + { + var selector = "$[?(@[0, 0]!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, index, less-or-equal (74)" )] + public void Test_relative_non_singular_query__index__less_or_equal_74() + { + var selector = "$[?(@[0, 0]<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, name, equal (75)" )] + public void Test_relative_non_singular_query__name__equal_75() + { + var selector = "$[?(@['a', 'a']==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, name, not equal (76)" )] + public void Test_relative_non_singular_query__name__not_equal_76() + { + var selector = "$[?(@['a', 'a']!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, name, less-or-equal (77)" )] + public void Test_relative_non_singular_query__name__less_or_equal_77() + { + var selector = "$[?(@['a', 'a']<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, combined, equal (78)" )] + public void Test_relative_non_singular_query__combined__equal_78() + { + var selector = "$[?(@[0, '0']==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, combined, not equal (79)" )] + public void Test_relative_non_singular_query__combined__not_equal_79() + { + var selector = "$[?(@[0, '0']!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, combined, less-or-equal (80)" )] + public void Test_relative_non_singular_query__combined__less_or_equal_80() + { + var selector = "$[?(@[0, '0']<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, wildcard, equal (81)" )] + public void Test_relative_non_singular_query__wildcard__equal_81() + { + var selector = "$[?(@.*==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, wildcard, not equal (82)" )] + public void Test_relative_non_singular_query__wildcard__not_equal_82() + { + var selector = "$[?(@.*!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, wildcard, less-or-equal (83)" )] + public void Test_relative_non_singular_query__wildcard__less_or_equal_83() + { + var selector = "$[?(@.*<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, slice, equal (84)" )] + public void Test_relative_non_singular_query__slice__equal_84() + { + var selector = "$[?(@[0:0]==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, slice, not equal (85)" )] + public void Test_relative_non_singular_query__slice__not_equal_85() + { + var selector = "$[?(@[0:0]!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"relative non-singular query, slice, less-or-equal (86)" )] + public void Test_relative_non_singular_query__slice__less_or_equal_86() + { + var selector = "$[?(@[0:0]<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, index, equal (87)" )] + public void Test_absolute_non_singular_query__index__equal_87() + { + var selector = "$[?($[0, 0]==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, index, not equal (88)" )] + public void Test_absolute_non_singular_query__index__not_equal_88() + { + var selector = "$[?($[0, 0]!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, index, less-or-equal (89)" )] + public void Test_absolute_non_singular_query__index__less_or_equal_89() + { + var selector = "$[?($[0, 0]<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, name, equal (90)" )] + public void Test_absolute_non_singular_query__name__equal_90() + { + var selector = "$[?($['a', 'a']==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, name, not equal (91)" )] + public void Test_absolute_non_singular_query__name__not_equal_91() + { + var selector = "$[?($['a', 'a']!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, name, less-or-equal (92)" )] + public void Test_absolute_non_singular_query__name__less_or_equal_92() + { + var selector = "$[?($['a', 'a']<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, combined, equal (93)" )] + public void Test_absolute_non_singular_query__combined__equal_93() + { + var selector = "$[?($[0, '0']==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, combined, not equal (94)" )] + public void Test_absolute_non_singular_query__combined__not_equal_94() + { + var selector = "$[?($[0, '0']!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, combined, less-or-equal (95)" )] + public void Test_absolute_non_singular_query__combined__less_or_equal_95() + { + var selector = "$[?($[0, '0']<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, wildcard, equal (96)" )] + public void Test_absolute_non_singular_query__wildcard__equal_96() + { + var selector = "$[?($.*==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, wildcard, not equal (97)" )] + public void Test_absolute_non_singular_query__wildcard__not_equal_97() + { + var selector = "$[?($.*!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, wildcard, less-or-equal (98)" )] + public void Test_absolute_non_singular_query__wildcard__less_or_equal_98() + { + var selector = "$[?($.*<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, slice, equal (99)" )] + public void Test_absolute_non_singular_query__slice__equal_99() + { + var selector = "$[?($[0:0]==42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, slice, not equal (100)" )] + public void Test_absolute_non_singular_query__slice__not_equal_100() + { + var selector = "$[?($[0:0]!=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"absolute non-singular query, slice, less-or-equal (101)" )] + public void Test_absolute_non_singular_query__slice__less_or_equal_101() + { + var selector = "$[?($[0:0]<=42)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"multiple selectors (102)" )] + public void Test_multiple_selectors_102() + { + var selector = "$[?@.a,?@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, comparison (103)" )] + public void Test_multiple_selectors__comparison_103() + { + var selector = "$[?@.a=='b',?@.b=='x']"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, overlapping (104)" )] + public void Test_multiple_selectors__overlapping_104() + { + var selector = "$[?@.a,?@.d]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, filter and index (105)" )] + public void Test_multiple_selectors__filter_and_index_105() + { + var selector = "$[?@.a,1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, filter and wildcard (106)" )] + public void Test_multiple_selectors__filter_and_wildcard_106() + { + var selector = "$[?@.a,*]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, filter and slice (107)" )] + public void Test_multiple_selectors__filter_and_slice_107() + { + var selector = "$[?@.a,1:]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + }, + { + "g": "h" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + }, + { + "g": "h" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"multiple selectors, comparison filter, index and slice (108)" )] + public void Test_multiple_selectors__comparison_filter__index_and_slice_108() + { + var selector = "$[1, ?@.a=='b', 1:]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "b": "c", + "d": "f" + }, + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, zero and negative zero (109)" )] + public void Test_equals_number__zero_and_negative_zero_109() + { + var selector = "$[?@.a==-0]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 0, + "d": "e" + }, + { + "a": 0.1, + "d": "f" + }, + { + "a": "0", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 0, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, with and without decimal fraction (110)" )] + public void Test_equals_number__with_and_without_decimal_fraction_110() + { + var selector = "$[?@.a==1.0]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "e" + }, + { + "a": 2, + "d": "f" + }, + { + "a": "1", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, exponent (111)" )] + public void Test_equals_number__exponent_111() + { + var selector = "$[?@.a==1e2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 100, + "d": "e" + }, + { + "a": 100.1, + "d": "f" + }, + { + "a": "100", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 100, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, positive exponent (112)" )] + public void Test_equals_number__positive_exponent_112() + { + var selector = "$[?@.a==1e+2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 100, + "d": "e" + }, + { + "a": 100.1, + "d": "f" + }, + { + "a": "100", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 100, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, negative exponent (113)" )] + public void Test_equals_number__negative_exponent_113() + { + var selector = "$[?@.a==1e-2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 0.01, + "d": "e" + }, + { + "a": 0.02, + "d": "f" + }, + { + "a": "0.01", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 0.01, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, decimal fraction (114)" )] + public void Test_equals_number__decimal_fraction_114() + { + var selector = "$[?@.a==1.1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1.1, + "d": "e" + }, + { + "a": 1, + "d": "f" + }, + { + "a": "1.1", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1.1, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, decimal fraction, no fractional digit (115)" )] + public void Test_equals_number__decimal_fraction__no_fractional_digit_115() + { + var selector = "$[?@.a==1.]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"equals number, decimal fraction, exponent (116)" )] + public void Test_equals_number__decimal_fraction__exponent_116() + { + var selector = "$[?@.a==1.1e2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 110, + "d": "e" + }, + { + "a": 110.1, + "d": "f" + }, + { + "a": "110", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 110, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, decimal fraction, positive exponent (117)" )] + public void Test_equals_number__decimal_fraction__positive_exponent_117() + { + var selector = "$[?@.a==1.1e+2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 110, + "d": "e" + }, + { + "a": 110.1, + "d": "f" + }, + { + "a": "110", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 110, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals number, decimal fraction, negative exponent (118)" )] + public void Test_equals_number__decimal_fraction__negative_exponent_118() + { + var selector = "$[?@.a==1.1e-2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 0.011, + "d": "e" + }, + { + "a": 0.012, + "d": "f" + }, + { + "a": "0.011", + "d": "g" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 0.011, + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals, special nothing (119)" )] + public void Test_equals__special_nothing_119() + { + var selector = "$.values[?length(@.a) == value($..c)]"; + var document = JsonNode.Parse( + """ + { + "c": "cd", + "values": [ + { + "a": "ab" + }, + { + "c": "d" + }, + { + "a": null + } + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "c": "d" + }, + { + "a": null + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals, empty node list and empty node list (120)" )] + public void Test_equals__empty_node_list_and_empty_node_list_120() + { + var selector = "$[?@.a == @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "c": 3 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"equals, empty node list and special nothing (121)" )] + public void Test_equals__empty_node_list_and_special_nothing_121() + { + var selector = "$[?@.a == length(@.b)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"object data (122)" )] + public void Test_object_data_122() + { + var selector = "$[?@<3]"; + var document = JsonNode.Parse( + """ + { + "a": 1, + "b": 2, + "c": 3 + } + """ ); + var results = document.Select( selector ); + var expectOneOf = JsonNode.Parse( + """ + [ + [ + 1, + 2 + ], + [ + 2, + 1 + ] + ] + """ ); + + var match = TestHelper.MatchAny( results, expectOneOf! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"and binds more tightly than or (123)" )] + public void Test_and_binds_more_tightly_than_or_123() + { + var selector = "$[?@.a || @.b && @.c]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2, + "c": 3 + }, + { + "c": 3 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"left to right evaluation (124)" )] + public void Test_left_to_right_evaluation_124() + { + var selector = "$[?@.a && @.b || @.c]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 1, + "c": 3 + }, + { + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 1, + "c": 3 + }, + { + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"group terms, left (125)" )] + public void Test_group_terms__left_125() + { + var selector = "$[?(@.a || @.b) && @.c]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "c": 3 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"group terms, right (126)" )] + public void Test_group_terms__right_126() + { + var selector = "$[?@.a && (@.b || @.c)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 2 + }, + { + "b": 2 + }, + { + "c": 2 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 2 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"string literal, single quote in double quotes (127)" )] + public void Test_string_literal__single_quote_in_double_quotes_127() + { + var selector = "$[?@ == \"quoted' literal\"]"; + var document = JsonNode.Parse( + """ + [ + "quoted' literal", + "a", + "quoted\\' literal" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "quoted' literal" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"string literal, double quote in single quotes (128)" )] + public void Test_string_literal__double_quote_in_single_quotes_128() + { + var selector = "$[?@ == 'quoted\" literal']"; + var document = JsonNode.Parse( + """ + [ + "quoted\" literal", + "a", + "quoted\\\" literal", + "'quoted\" literal'" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "quoted\" literal" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"string literal, escaped single quote in single quotes (129)" )] + public void Test_string_literal__escaped_single_quote_in_single_quotes_129() + { + var selector = "$[?@ == 'quoted\\' literal']"; + var document = JsonNode.Parse( + """ + [ + "quoted' literal", + "a", + "quoted\\' literal", + "'quoted\" literal'" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "quoted' literal" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"string literal, escaped double quote in double quotes (130)" )] + public void Test_string_literal__escaped_double_quote_in_double_quotes_130() + { + var selector = "$[?@ == \"quoted\\\" literal\"]"; + var document = JsonNode.Parse( + """ + [ + "quoted\" literal", + "a", + "quoted\\\" literal", + "'quoted\" literal'" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "quoted\" literal" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"literal true must be compared (131)" )] + public void Test_literal_true_must_be_compared_131() + { + var selector = "$[?true]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"literal false must be compared (132)" )] + public void Test_literal_false_must_be_compared_132() + { + var selector = "$[?false]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"literal string must be compared (133)" )] + public void Test_literal_string_must_be_compared_133() + { + var selector = "$[?'abc']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"literal int must be compared (134)" )] + public void Test_literal_int_must_be_compared_134() + { + var selector = "$[?2]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"literal float must be compared (135)" )] + public void Test_literal_float_must_be_compared_135() + { + var selector = "$[?2.2]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"literal null must be compared (136)" )] + public void Test_literal_null_must_be_compared_136() + { + var selector = "$[?null]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"and, literals must be compared (137)" )] + public void Test_and__literals_must_be_compared_137() + { + var selector = "$[?true && false]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"or, literals must be compared (138)" )] + public void Test_or__literals_must_be_compared_138() + { + var selector = "$[?true || false]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"and, right hand literal must be compared (139)" )] + public void Test_and__right_hand_literal_must_be_compared_139() + { + var selector = "$[?true == false && false]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"or, right hand literal must be compared (140)" )] + public void Test_or__right_hand_literal_must_be_compared_140() + { + var selector = "$[?true == false || false]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"and, left hand literal must be compared (141)" )] + public void Test_and__left_hand_literal_must_be_compared_141() + { + var selector = "$[?false && true == false]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"or, left hand literal must be compared (142)" )] + public void Test_or__left_hand_literal_must_be_compared_142() + { + var selector = "$[?false || true == false]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + } +} + diff --git a/test/Hyperbee.Json.Cts/Tests/cts-functions-tests.cs b/test/Hyperbee.Json.Cts/Tests/cts-functions-tests.cs new file mode 100644 index 00000000..6def80e7 --- /dev/null +++ b/test/Hyperbee.Json.Cts/Tests/cts-functions-tests.cs @@ -0,0 +1,1772 @@ +// This file was auto generated. + +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Cts.Tests +{ + [TestClass] + public class CtsFunctionsTest + { + + [TestMethod( @"count, count function (1)" )] + public void Test_count__count_function_1() + { + var selector = "$[?count(@..*)>2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ], + "d": "f" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ], + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"count, single-node arg (2)" )] + public void Test_count__single_node_arg_2() + { + var selector = "$[?count(@.a)>1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ], + "d": "f" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"count, multiple-selector arg (3)" )] + public void Test_count__multiple_selector_arg_3() + { + var selector = "$[?count(@['a','d'])>1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ], + "d": "f" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": [ + 1 + ], + "d": "f" + }, + { + "a": 1, + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"count, non-query arg, number (4)" )] + public void Test_count__non_query_arg__number_4() + { + var selector = "$[?count(1)>2]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"count, non-query arg, string (5)" )] + public void Test_count__non_query_arg__string_5() + { + var selector = "$[?count('string')>2]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"count, non-query arg, true (6)" )] + public void Test_count__non_query_arg__true_6() + { + var selector = "$[?count(true)>2]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"count, non-query arg, false (7)" )] + public void Test_count__non_query_arg__false_7() + { + var selector = "$[?count(false)>2]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"count, non-query arg, null (8)" )] + public void Test_count__non_query_arg__null_8() + { + var selector = "$[?count(null)>2]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"count, result must be compared (9)" )] + public void Test_count__result_must_be_compared_9() + { + var selector = "$[?count(@..*)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"count, no params (10)" )] + public void Test_count__no_params_10() + { + var selector = "$[?count()==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"count, too many params (11)" )] + public void Test_count__too_many_params_11() + { + var selector = "$[?count(@.a,@.b)==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"length, string data (12)" )] + public void Test_length__string_data_12() + { + var selector = "$[?length(@.a)>=2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "ab" + }, + { + "a": "d" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, string data, unicode (13)" )] + public void Test_length__string_data__unicode_13() + { + var selector = "$[?length(@)==2]"; + var document = JsonNode.Parse( + """ + [ + "☺", + "☺☺", + "☺☺☺", + "ж", + "жж", + "жжж", + "磨", + "阿美", + "形声字" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "☺☺", + "жж", + "阿美" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, array data (14)" )] + public void Test_length__array_data_14() + { + var selector = "$[?length(@.a)>=2]"; + var document = JsonNode.Parse( + """ + [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ] + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": [ + 1, + 2, + 3 + ] + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, missing data (15)" )] + public void Test_length__missing_data_15() + { + var selector = "$[?length(@.a)>=2]"; + var document = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, number arg (16)" )] + public void Test_length__number_arg_16() + { + var selector = "$[?length(1)>=2]"; + var document = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, true arg (17)" )] + public void Test_length__true_arg_17() + { + var selector = "$[?length(true)>=2]"; + var document = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, false arg (18)" )] + public void Test_length__false_arg_18() + { + var selector = "$[?length(false)>=2]"; + var document = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, null arg (19)" )] + public void Test_length__null_arg_19() + { + var selector = "$[?length(null)>=2]"; + var document = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, result must be compared (20)" )] + public void Test_length__result_must_be_compared_20() + { + var selector = "$[?length(@.a)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"length, no params (21)" )] + public void Test_length__no_params_21() + { + var selector = "$[?length()==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"length, too many params (22)" )] + public void Test_length__too_many_params_22() + { + var selector = "$[?length(@.a,@.b)==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"length, non-singular query arg (23)" )] + public void Test_length__non_singular_query_arg_23() + { + var selector = "$[?length(@.*)<3]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"length, arg is a function expression (24)" )] + public void Test_length__arg_is_a_function_expression_24() + { + var selector = "$.values[?length(@.a)==length(value($..c))]"; + var document = JsonNode.Parse( + """ + { + "c": "cd", + "values": [ + { + "a": "ab" + }, + { + "a": "d" + } + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"length, arg is special nothing (25)" )] + public void Test_length__arg_is_special_nothing_25() + { + var selector = "$[?length(value(@.a))>0]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "ab" + }, + { + "c": "d" + }, + { + "a": null + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, found match (26)" )] + public void Test_match__found_match_26() + { + var selector = "$[?match(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, double quotes (27)" )] + public void Test_match__double_quotes_27() + { + var selector = "$[?match(@.a, \"a.*\")]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, regex from the document (28)" )] + public void Test_match__regex_from_the_document_28() + { + var selector = "$.values[?match(@, $.regex)]"; + var document = JsonNode.Parse( + """ + { + "regex": "b.?b", + "values": [ + "abc", + "bcd", + "bab", + "bba", + "bbab", + "b", + true, + [], + {} + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "bab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, don't select match (29)" )] + public void Test_match__don_t_select_match_29() + { + var selector = "$[?!match(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, not a match (30)" )] + public void Test_match__not_a_match_30() + { + var selector = "$[?match(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, select non-match (31)" )] + public void Test_match__select_non_match_31() + { + var selector = "$[?!match(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, non-string first arg (32)" )] + public void Test_match__non_string_first_arg_32() + { + var selector = "$[?match(1, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, non-string second arg (33)" )] + public void Test_match__non_string_second_arg_33() + { + var selector = "$[?match(@.a, 1)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, filter, match function, unicode char class, uppercase (34)" )] + public void Test_match__filter__match_function__unicode_char_class__uppercase_34() + { + var selector = "$[?match(@, '\\\\p{Lu}')]"; + var document = JsonNode.Parse( + """ + [ + "ж", + "Ж", + "1", + "жЖ", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "Ж" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, filter, match function, unicode char class negated, uppercase (35)" )] + public void Test_match__filter__match_function__unicode_char_class_negated__uppercase_35() + { + var selector = "$[?match(@, '\\\\P{Lu}')]"; + var document = JsonNode.Parse( + """ + [ + "ж", + "Ж", + "1", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ж", + "1" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, filter, match function, unicode, surrogate pair (36)" )] + public void Test_match__filter__match_function__unicode__surrogate_pair_36() + { + var selector = "$[?match(@, 'a.b')]"; + var document = JsonNode.Parse( + """ + [ + "a𐄁b", + "ab", + "1", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "a𐄁b" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, dot matcher on \u2028 (37)" )] + public void Test_match__dot_matcher_on__u2028_37() + { + var selector = "$[?match(@, '.')]"; + var document = JsonNode.Parse( + """ + [ + "\u2028", + "\r", + "\n", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "\u2028" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, dot matcher on \u2029 (38)" )] + public void Test_match__dot_matcher_on__u2029_38() + { + var selector = "$[?match(@, '.')]"; + var document = JsonNode.Parse( + """ + [ + "\u2029", + "\r", + "\n", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "\u2029" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, result cannot be compared (39)" )] + public void Test_match__result_cannot_be_compared_39() + { + var selector = "$[?match(@.a, 'a.*')==true]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"match, too few params (40)" )] + public void Test_match__too_few_params_40() + { + var selector = "$[?match(@.a)==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"match, too many params (41)" )] + public void Test_match__too_many_params_41() + { + var selector = "$[?match(@.a,@.b,@.c)==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"match, arg is a function expression (42)" )] + public void Test_match__arg_is_a_function_expression_42() + { + var selector = "$.values[?match(@.a, value($..['regex']))]"; + var document = JsonNode.Parse( + """ + { + "regex": "a.*", + "values": [ + { + "a": "ab" + }, + { + "a": "ba" + } + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "ab" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, dot in character class (43)" )] + public void Test_match__dot_in_character_class_43() + { + var selector = "$[?match(@, 'a[.b]c')]"; + var document = JsonNode.Parse( + """ + [ + "abc", + "a.c", + "axc" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "abc", + "a.c" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, escaped dot (44)" )] + public void Test_match__escaped_dot_44() + { + var selector = "$[?match(@, 'a\\\\.c')]"; + var document = JsonNode.Parse( + """ + [ + "abc", + "a.c", + "axc" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "a.c" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, escaped backslash before dot (45)" )] + public void Test_match__escaped_backslash_before_dot_45() + { + var selector = "$[?match(@, 'a\\\\\\\\.c')]"; + var document = JsonNode.Parse( + """ + [ + "abc", + "a.c", + "axc", + "a\\\u2028c" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "a\\\u2028c" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, escaped left square bracket (46)" )] + public void Test_match__escaped_left_square_bracket_46() + { + var selector = "$[?match(@, 'a\\\\[.c')]"; + var document = JsonNode.Parse( + """ + [ + "abc", + "a.c", + "a[\u2028c" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "a[\u2028c" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, escaped right square bracket (47)" )] + public void Test_match__escaped_right_square_bracket_47() + { + var selector = "$[?match(@, 'a[\\\\].]c')]"; + var document = JsonNode.Parse( + """ + [ + "abc", + "a.c", + "a\u2028c", + "a]c" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "a.c", + "a]c" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, explicit caret (48)" )] + public void Test_match__explicit_caret_48() + { + var selector = "$[?match(@, '^ab.*')]"; + var document = JsonNode.Parse( + """ + [ + "abc", + "axc", + "ab", + "xab" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "abc", + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"match, explicit dollar (49)" )] + public void Test_match__explicit_dollar_49() + { + var selector = "$[?match(@, '.*bc$')]"; + var document = JsonNode.Parse( + """ + [ + "abc", + "axc", + "ab", + "abcx" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "abc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, at the end (50)" )] + public void Test_search__at_the_end_50() + { + var selector = "$[?search(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "the end is ab" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "the end is ab" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, double quotes (51)" )] + public void Test_search__double_quotes_51() + { + var selector = "$[?search(@.a, \"a.*\")]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "the end is ab" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "the end is ab" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, at the start (52)" )] + public void Test_search__at_the_start_52() + { + var selector = "$[?search(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "ab is at the start" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "ab is at the start" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, in the middle (53)" )] + public void Test_search__in_the_middle_53() + { + var selector = "$[?search(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "contains two matches" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "contains two matches" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, regex from the document (54)" )] + public void Test_search__regex_from_the_document_54() + { + var selector = "$.values[?search(@, $.regex)]"; + var document = JsonNode.Parse( + """ + { + "regex": "b.?b", + "values": [ + "abc", + "bcd", + "bab", + "bba", + "bbab", + "b", + true, + [], + {} + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "bab", + "bba", + "bbab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, don't select match (55)" )] + public void Test_search__don_t_select_match_55() + { + var selector = "$[?!search(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "contains two matches" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, not a match (56)" )] + public void Test_search__not_a_match_56() + { + var selector = "$[?search(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, select non-match (57)" )] + public void Test_search__select_non_match_57() + { + var selector = "$[?!search(@.a, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, non-string first arg (58)" )] + public void Test_search__non_string_first_arg_58() + { + var selector = "$[?search(1, 'a.*')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, non-string second arg (59)" )] + public void Test_search__non_string_second_arg_59() + { + var selector = "$[?search(@.a, 1)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "bc" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, filter, search function, unicode char class, uppercase (60)" )] + public void Test_search__filter__search_function__unicode_char_class__uppercase_60() + { + var selector = "$[?search(@, '\\\\p{Lu}')]"; + var document = JsonNode.Parse( + """ + [ + "ж", + "Ж", + "1", + "жЖ", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "Ж", + "жЖ" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, filter, search function, unicode char class negated, uppercase (61)" )] + public void Test_search__filter__search_function__unicode_char_class_negated__uppercase_61() + { + var selector = "$[?search(@, '\\\\P{Lu}')]"; + var document = JsonNode.Parse( + """ + [ + "ж", + "Ж", + "1", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ж", + "1" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, filter, search function, unicode, surrogate pair (62)" )] + public void Test_search__filter__search_function__unicode__surrogate_pair_62() + { + var selector = "$[?search(@, 'a.b')]"; + var document = JsonNode.Parse( + """ + [ + "a𐄁bc", + "abc", + "1", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "a𐄁bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, dot matcher on \u2028 (63)" )] + public void Test_search__dot_matcher_on__u2028_63() + { + var selector = "$[?search(@, '.')]"; + var document = JsonNode.Parse( + """ + [ + "\u2028", + "\r\u2028\n", + "\r", + "\n", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "\u2028", + "\r\u2028\n" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, dot matcher on \u2029 (64)" )] + public void Test_search__dot_matcher_on__u2029_64() + { + var selector = "$[?search(@, '.')]"; + var document = JsonNode.Parse( + """ + [ + "\u2029", + "\r\u2029\n", + "\r", + "\n", + true, + [], + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "\u2029", + "\r\u2029\n" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, result cannot be compared (65)" )] + public void Test_search__result_cannot_be_compared_65() + { + var selector = "$[?search(@.a, 'a.*')==true]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"search, too few params (66)" )] + public void Test_search__too_few_params_66() + { + var selector = "$[?search(@.a)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"search, too many params (67)" )] + public void Test_search__too_many_params_67() + { + var selector = "$[?search(@.a,@.b,@.c)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"search, arg is a function expression (68)" )] + public void Test_search__arg_is_a_function_expression_68() + { + var selector = "$.values[?search(@, value($..['regex']))]"; + var document = JsonNode.Parse( + """ + { + "regex": "b.?b", + "values": [ + "abc", + "bcd", + "bab", + "bba", + "bbab", + "b", + true, + [], + {} + ] + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "bab", + "bba", + "bbab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, dot in character class (69)" )] + public void Test_search__dot_in_character_class_69() + { + var selector = "$[?search(@, 'a[.b]c')]"; + var document = JsonNode.Parse( + """ + [ + "x abc y", + "x a.c y", + "x axc y" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "x abc y", + "x a.c y" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, escaped dot (70)" )] + public void Test_search__escaped_dot_70() + { + var selector = "$[?search(@, 'a\\\\.c')]"; + var document = JsonNode.Parse( + """ + [ + "x abc y", + "x a.c y", + "x axc y" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "x a.c y" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, escaped backslash before dot (71)" )] + public void Test_search__escaped_backslash_before_dot_71() + { + var selector = "$[?search(@, 'a\\\\\\\\.c')]"; + var document = JsonNode.Parse( + """ + [ + "x abc y", + "x a.c y", + "x axc y", + "x a\\\u2028c y" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "x a\\\u2028c y" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, escaped left square bracket (72)" )] + public void Test_search__escaped_left_square_bracket_72() + { + var selector = "$[?search(@, 'a\\\\[.c')]"; + var document = JsonNode.Parse( + """ + [ + "x abc y", + "x a.c y", + "x a[\u2028c y" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "x a[\u2028c y" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"search, escaped right square bracket (73)" )] + public void Test_search__escaped_right_square_bracket_73() + { + var selector = "$[?search(@, 'a[\\\\].]c')]"; + var document = JsonNode.Parse( + """ + [ + "x abc y", + "x a.c y", + "x a\u2028c y", + "x a]c y" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "x a.c y", + "x a]c y" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"value, single-value nodelist (74)" )] + public void Test_value__single_value_nodelist_74() + { + var selector = "$[?value(@.*)==4]"; + var document = JsonNode.Parse( + """ + [ + [ + 4 + ], + { + "foo": 4 + }, + [ + 5 + ], + { + "foo": 5 + }, + 4 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + [ + 4 + ], + { + "foo": 4 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"value, multi-value nodelist (75)" )] + public void Test_value__multi_value_nodelist_75() + { + var selector = "$[?value(@.*)==4]"; + var document = JsonNode.Parse( + """ + [ + [ + 4, + 4 + ], + { + "foo": 4, + "bar": 4 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"value, too few params (76)" )] + public void Test_value__too_few_params_76() + { + var selector = "$[?value()==4]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"value, too many params (77)" )] + public void Test_value__too_many_params_77() + { + var selector = "$[?value(@.a,@.b)==4]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"value, result must be compared (78)" )] + public void Test_value__result_must_be_compared_78() + { + var selector = "$[?value(@.a)]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + } +} + diff --git a/test/Hyperbee.Json.Cts/Tests/cts-index-selector-tests.cs b/test/Hyperbee.Json.Cts/Tests/cts-index-selector-tests.cs new file mode 100644 index 00000000..7a63537e --- /dev/null +++ b/test/Hyperbee.Json.Cts/Tests/cts-index-selector-tests.cs @@ -0,0 +1,203 @@ +// This file was auto generated. + +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Cts.Tests +{ + [TestClass] + public class CtsIndexSelectorTest + { + + [TestMethod( @"first element (1)" )] + public void Test_first_element_1() + { + var selector = "$[0]"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "first" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"second element (2)" )] + public void Test_second_element_2() + { + var selector = "$[1]"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "second" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"out of bound (3)" )] + public void Test_out_of_bound_3() + { + var selector = "$[2]"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"overflowing index (4)" )] + public void Test_overflowing_index_4() + { + var selector = "$[231584178474632390847141970017375815706539969331281128078915168015826259279872]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"not actually an index, overflowing index leads into general text (5)" )] + public void Test_not_actually_an_index__overflowing_index_leads_into_general_text_5() + { + var selector = "$[231584178474632390847141970017375815706539969331281128078915168SomeRandomText]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"negative (6)" )] + public void Test_negative_6() + { + var selector = "$[-1]"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "second" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"more negative (7)" )] + public void Test_more_negative_7() + { + var selector = "$[-2]"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "first" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative out of bound (8)" )] + public void Test_negative_out_of_bound_8() + { + var selector = "$[-3]"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"on object (9)" )] + public void Test_on_object_9() + { + var selector = "$[0]"; + var document = JsonNode.Parse( + """ + { + "foo": 1 + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"leading 0 (10)" )] + public void Test_leading_0_10() + { + var selector = "$[01]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"leading -0 (11)" )] + public void Test_leading__0_11() + { + var selector = "$[-01]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + } +} + diff --git a/test/Hyperbee.Json.Cts/Tests/cts-name-selector-tests.cs b/test/Hyperbee.Json.Cts/Tests/cts-name-selector-tests.cs new file mode 100644 index 00000000..efca01e3 --- /dev/null +++ b/test/Hyperbee.Json.Cts/Tests/cts-name-selector-tests.cs @@ -0,0 +1,1393 @@ +// This file was auto generated. + +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Cts.Tests +{ + [TestClass] + public class CtsNameSelectorTest + { + + [TestMethod( @"double quotes (1)" )] + public void Test_double_quotes_1() + { + var selector = "$[\"a\"]"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, absent data (2)" )] + public void Test_double_quotes__absent_data_2() + { + var selector = "$[\"c\"]"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, array data (3)" )] + public void Test_double_quotes__array_data_3() + { + var selector = "$[\"a\"]"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, embedded U+0000 (4)" )] + public void Test_double_quotes__embedded_U_0000_4() + { + var selector = "$[\"\u0000\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0001 (5)" )] + public void Test_double_quotes__embedded_U_0001_5() + { + var selector = "$[\"\u0001\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0002 (6)" )] + public void Test_double_quotes__embedded_U_0002_6() + { + var selector = "$[\"\u0002\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0003 (7)" )] + public void Test_double_quotes__embedded_U_0003_7() + { + var selector = "$[\"\u0003\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0004 (8)" )] + public void Test_double_quotes__embedded_U_0004_8() + { + var selector = "$[\"\u0004\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0005 (9)" )] + public void Test_double_quotes__embedded_U_0005_9() + { + var selector = "$[\"\u0005\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0006 (10)" )] + public void Test_double_quotes__embedded_U_0006_10() + { + var selector = "$[\"\u0006\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0007 (11)" )] + public void Test_double_quotes__embedded_U_0007_11() + { + var selector = "$[\"\u0007\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0008 (12)" )] + public void Test_double_quotes__embedded_U_0008_12() + { + var selector = "$[\"\b\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0009 (13)" )] + public void Test_double_quotes__embedded_U_0009_13() + { + var selector = "$[\"\t\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+000A (14)" )] + public void Test_double_quotes__embedded_U_000A_14() + { + var selector = "$[\"\n\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+000B (15)" )] + public void Test_double_quotes__embedded_U_000B_15() + { + var selector = "$[\"\u000b\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+000C (16)" )] + public void Test_double_quotes__embedded_U_000C_16() + { + var selector = "$[\"\f\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+000D (17)" )] + public void Test_double_quotes__embedded_U_000D_17() + { + var selector = "$[\"\r\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+000E (18)" )] + public void Test_double_quotes__embedded_U_000E_18() + { + var selector = "$[\"\u000e\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+000F (19)" )] + public void Test_double_quotes__embedded_U_000F_19() + { + var selector = "$[\"\u000f\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0010 (20)" )] + public void Test_double_quotes__embedded_U_0010_20() + { + var selector = "$[\"\u0010\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0011 (21)" )] + public void Test_double_quotes__embedded_U_0011_21() + { + var selector = "$[\"\u0011\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0012 (22)" )] + public void Test_double_quotes__embedded_U_0012_22() + { + var selector = "$[\"\u0012\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0013 (23)" )] + public void Test_double_quotes__embedded_U_0013_23() + { + var selector = "$[\"\u0013\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0014 (24)" )] + public void Test_double_quotes__embedded_U_0014_24() + { + var selector = "$[\"\u0014\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0015 (25)" )] + public void Test_double_quotes__embedded_U_0015_25() + { + var selector = "$[\"\u0015\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0016 (26)" )] + public void Test_double_quotes__embedded_U_0016_26() + { + var selector = "$[\"\u0016\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0017 (27)" )] + public void Test_double_quotes__embedded_U_0017_27() + { + var selector = "$[\"\u0017\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0018 (28)" )] + public void Test_double_quotes__embedded_U_0018_28() + { + var selector = "$[\"\u0018\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0019 (29)" )] + public void Test_double_quotes__embedded_U_0019_29() + { + var selector = "$[\"\u0019\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+001A (30)" )] + public void Test_double_quotes__embedded_U_001A_30() + { + var selector = "$[\"\u001a\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+001B (31)" )] + public void Test_double_quotes__embedded_U_001B_31() + { + var selector = "$[\"\u001b\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+001C (32)" )] + public void Test_double_quotes__embedded_U_001C_32() + { + var selector = "$[\"\u001c\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+001D (33)" )] + public void Test_double_quotes__embedded_U_001D_33() + { + var selector = "$[\"\u001d\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+001E (34)" )] + public void Test_double_quotes__embedded_U_001E_34() + { + var selector = "$[\"\u001e\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+001F (35)" )] + public void Test_double_quotes__embedded_U_001F_35() + { + var selector = "$[\"\u001f\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded U+0020 (36)" )] + public void Test_double_quotes__embedded_U_0020_36() + { + var selector = "$[\" \"]"; + var document = JsonNode.Parse( + """ + { + " ": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped double quote (37)" )] + public void Test_double_quotes__escaped_double_quote_37() + { + var selector = "$[\"\\\"\"]"; + var document = JsonNode.Parse( + """ + { + "\"": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped reverse solidus (38)" )] + public void Test_double_quotes__escaped_reverse_solidus_38() + { + var selector = "$[\"\\\\\"]"; + var document = JsonNode.Parse( + """ + { + "\\": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped solidus (39)" )] + public void Test_double_quotes__escaped_solidus_39() + { + var selector = "$[\"\\/\"]"; + var document = JsonNode.Parse( + """ + { + "/": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped backspace (40)" )] + public void Test_double_quotes__escaped_backspace_40() + { + var selector = "$[\"\\b\"]"; + var document = JsonNode.Parse( + """ + { + "\b": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped form feed (41)" )] + public void Test_double_quotes__escaped_form_feed_41() + { + var selector = "$[\"\\f\"]"; + var document = JsonNode.Parse( + """ + { + "\f": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped line feed (42)" )] + public void Test_double_quotes__escaped_line_feed_42() + { + var selector = "$[\"\\n\"]"; + var document = JsonNode.Parse( + """ + { + "\n": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped carriage return (43)" )] + public void Test_double_quotes__escaped_carriage_return_43() + { + var selector = "$[\"\\r\"]"; + var document = JsonNode.Parse( + """ + { + "\r": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped tab (44)" )] + public void Test_double_quotes__escaped_tab_44() + { + var selector = "$[\"\\t\"]"; + var document = JsonNode.Parse( + """ + { + "\t": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped ☺, upper case hex (45)" )] + public void Test_double_quotes__escaped____upper_case_hex_45() + { + var selector = "$[\"\\u263A\"]"; + var document = JsonNode.Parse( + """ + { + "☺": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, escaped ☺, lower case hex (46)" )] + public void Test_double_quotes__escaped____lower_case_hex_46() + { + var selector = "$[\"\\u263a\"]"; + var document = JsonNode.Parse( + """ + { + "☺": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, surrogate pair 𝄞 (47)" )] + public void Test_double_quotes__surrogate_pair____47() + { + var selector = "$[\"\\uD834\\uDD1E\"]"; + var document = JsonNode.Parse( + """ + { + "𝄞": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, surrogate pair 😀 (48)" )] + public void Test_double_quotes__surrogate_pair____48() + { + var selector = "$[\"\\uD83D\\uDE00\"]"; + var document = JsonNode.Parse( + """ + { + "😀": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"double quotes, invalid escaped single quote (49)" )] + public void Test_double_quotes__invalid_escaped_single_quote_49() + { + var selector = "$[\"\\'\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, embedded double quote (50)" )] + public void Test_double_quotes__embedded_double_quote_50() + { + var selector = "$[\"\"\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, incomplete escape (51)" )] + public void Test_double_quotes__incomplete_escape_51() + { + var selector = "$[\"\\\"]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes (52)" )] + public void Test_single_quotes_52() + { + var selector = "$['a']"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, absent data (53)" )] + public void Test_single_quotes__absent_data_53() + { + var selector = "$['c']"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, array data (54)" )] + public void Test_single_quotes__array_data_54() + { + var selector = "$['a']"; + var document = JsonNode.Parse( + """ + [ + "first", + "second" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, embedded U+0000 (55)" )] + public void Test_single_quotes__embedded_U_0000_55() + { + var selector = "$['\u0000']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0001 (56)" )] + public void Test_single_quotes__embedded_U_0001_56() + { + var selector = "$['\u0001']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0002 (57)" )] + public void Test_single_quotes__embedded_U_0002_57() + { + var selector = "$['\u0002']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0003 (58)" )] + public void Test_single_quotes__embedded_U_0003_58() + { + var selector = "$['\u0003']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0004 (59)" )] + public void Test_single_quotes__embedded_U_0004_59() + { + var selector = "$['\u0004']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0005 (60)" )] + public void Test_single_quotes__embedded_U_0005_60() + { + var selector = "$['\u0005']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0006 (61)" )] + public void Test_single_quotes__embedded_U_0006_61() + { + var selector = "$['\u0006']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0007 (62)" )] + public void Test_single_quotes__embedded_U_0007_62() + { + var selector = "$['\u0007']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0008 (63)" )] + public void Test_single_quotes__embedded_U_0008_63() + { + var selector = "$['\b']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0009 (64)" )] + public void Test_single_quotes__embedded_U_0009_64() + { + var selector = "$['\t']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+000A (65)" )] + public void Test_single_quotes__embedded_U_000A_65() + { + var selector = "$['\n']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+000B (66)" )] + public void Test_single_quotes__embedded_U_000B_66() + { + var selector = "$['\u000b']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+000C (67)" )] + public void Test_single_quotes__embedded_U_000C_67() + { + var selector = "$['\f']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+000D (68)" )] + public void Test_single_quotes__embedded_U_000D_68() + { + var selector = "$['\r']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+000E (69)" )] + public void Test_single_quotes__embedded_U_000E_69() + { + var selector = "$['\u000e']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+000F (70)" )] + public void Test_single_quotes__embedded_U_000F_70() + { + var selector = "$['\u000f']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0010 (71)" )] + public void Test_single_quotes__embedded_U_0010_71() + { + var selector = "$['\u0010']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0011 (72)" )] + public void Test_single_quotes__embedded_U_0011_72() + { + var selector = "$['\u0011']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0012 (73)" )] + public void Test_single_quotes__embedded_U_0012_73() + { + var selector = "$['\u0012']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0013 (74)" )] + public void Test_single_quotes__embedded_U_0013_74() + { + var selector = "$['\u0013']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0014 (75)" )] + public void Test_single_quotes__embedded_U_0014_75() + { + var selector = "$['\u0014']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0015 (76)" )] + public void Test_single_quotes__embedded_U_0015_76() + { + var selector = "$['\u0015']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0016 (77)" )] + public void Test_single_quotes__embedded_U_0016_77() + { + var selector = "$['\u0016']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0017 (78)" )] + public void Test_single_quotes__embedded_U_0017_78() + { + var selector = "$['\u0017']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0018 (79)" )] + public void Test_single_quotes__embedded_U_0018_79() + { + var selector = "$['\u0018']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0019 (80)" )] + public void Test_single_quotes__embedded_U_0019_80() + { + var selector = "$['\u0019']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+001A (81)" )] + public void Test_single_quotes__embedded_U_001A_81() + { + var selector = "$['\u001a']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+001B (82)" )] + public void Test_single_quotes__embedded_U_001B_82() + { + var selector = "$['\u001b']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+001C (83)" )] + public void Test_single_quotes__embedded_U_001C_83() + { + var selector = "$['\u001c']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+001D (84)" )] + public void Test_single_quotes__embedded_U_001D_84() + { + var selector = "$['\u001d']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+001E (85)" )] + public void Test_single_quotes__embedded_U_001E_85() + { + var selector = "$['\u001e']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+001F (86)" )] + public void Test_single_quotes__embedded_U_001F_86() + { + var selector = "$['\u001f']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded U+0020 (87)" )] + public void Test_single_quotes__embedded_U_0020_87() + { + var selector = "$[' ']"; + var document = JsonNode.Parse( + """ + { + " ": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped single quote (88)" )] + public void Test_single_quotes__escaped_single_quote_88() + { + var selector = "$['\\'']"; + var document = JsonNode.Parse( + """ + { + "'": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped reverse solidus (89)" )] + public void Test_single_quotes__escaped_reverse_solidus_89() + { + var selector = "$['\\\\']"; + var document = JsonNode.Parse( + """ + { + "\\": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped solidus (90)" )] + public void Test_single_quotes__escaped_solidus_90() + { + var selector = "$['\\/']"; + var document = JsonNode.Parse( + """ + { + "/": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped backspace (91)" )] + public void Test_single_quotes__escaped_backspace_91() + { + var selector = "$['\\b']"; + var document = JsonNode.Parse( + """ + { + "\b": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped form feed (92)" )] + public void Test_single_quotes__escaped_form_feed_92() + { + var selector = "$['\\f']"; + var document = JsonNode.Parse( + """ + { + "\f": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped line feed (93)" )] + public void Test_single_quotes__escaped_line_feed_93() + { + var selector = "$['\\n']"; + var document = JsonNode.Parse( + """ + { + "\n": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped carriage return (94)" )] + public void Test_single_quotes__escaped_carriage_return_94() + { + var selector = "$['\\r']"; + var document = JsonNode.Parse( + """ + { + "\r": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped tab (95)" )] + public void Test_single_quotes__escaped_tab_95() + { + var selector = "$['\\t']"; + var document = JsonNode.Parse( + """ + { + "\t": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped ☺, upper case hex (96)" )] + public void Test_single_quotes__escaped____upper_case_hex_96() + { + var selector = "$['\\u263A']"; + var document = JsonNode.Parse( + """ + { + "☺": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, escaped ☺, lower case hex (97)" )] + public void Test_single_quotes__escaped____lower_case_hex_97() + { + var selector = "$['\\u263a']"; + var document = JsonNode.Parse( + """ + { + "☺": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, surrogate pair 𝄞 (98)" )] + public void Test_single_quotes__surrogate_pair____98() + { + var selector = "$['\\uD834\\uDD1E']"; + var document = JsonNode.Parse( + """ + { + "𝄞": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, surrogate pair 😀 (99)" )] + public void Test_single_quotes__surrogate_pair____99() + { + var selector = "$['\\uD83D\\uDE00']"; + var document = JsonNode.Parse( + """ + { + "😀": "A" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "A" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, invalid escaped double quote (100)" )] + public void Test_single_quotes__invalid_escaped_double_quote_100() + { + var selector = "$['\\\"']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, embedded single quote (101)" )] + public void Test_single_quotes__embedded_single_quote_101() + { + var selector = "$[''']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"single quotes, incomplete escape (102)" )] + public void Test_single_quotes__incomplete_escape_102() + { + var selector = "$['\\']"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"double quotes, empty (103)" )] + public void Test_double_quotes__empty_103() + { + var selector = "$[\"\"]"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B", + "": "C" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "C" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"single quotes, empty (104)" )] + public void Test_single_quotes__empty_104() + { + var selector = "$['']"; + var document = JsonNode.Parse( + """ + { + "a": "A", + "b": "B", + "": "C" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "C" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + } +} + diff --git a/test/Hyperbee.Json.Cts/Tests/cts-slice-selector-tests.cs b/test/Hyperbee.Json.Cts/Tests/cts-slice-selector-tests.cs new file mode 100644 index 00000000..8c588d50 --- /dev/null +++ b/test/Hyperbee.Json.Cts/Tests/cts-slice-selector-tests.cs @@ -0,0 +1,1057 @@ +// This file was auto generated. + +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Cts.Tests +{ + [TestClass] + public class CtsSliceSelectorTest + { + + [TestMethod( @"slice selector (1)" )] + public void Test_slice_selector_1() + { + var selector = "$[1:3]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1, + 2 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice selector with step (2)" )] + public void Test_slice_selector_with_step_2() + { + var selector = "$[1:6:2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1, + 3, + 5 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice selector with everything omitted, short form (3)" )] + public void Test_slice_selector_with_everything_omitted__short_form_3() + { + var selector = "$[:]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice selector with everything omitted, long form (4)" )] + public void Test_slice_selector_with_everything_omitted__long_form_4() + { + var selector = "$[::]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice selector with start omitted (5)" )] + public void Test_slice_selector_with_start_omitted_5() + { + var selector = "$[:2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice selector with start and end omitted (6)" )] + public void Test_slice_selector_with_start_and_end_omitted_6() + { + var selector = "$[::2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 2, + 4, + 6, + 8 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative step with default start and end (7)" )] + public void Test_negative_step_with_default_start_and_end_7() + { + var selector = "$[::-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 3, + 2, + 1, + 0 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative step with default start (8)" )] + public void Test_negative_step_with_default_start_8() + { + var selector = "$[:0:-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 3, + 2, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative step with default end (9)" )] + public void Test_negative_step_with_default_end_9() + { + var selector = "$[2::-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 1, + 0 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"larger negative step (10)" )] + public void Test_larger_negative_step_10() + { + var selector = "$[::-2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 3, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative range with default step (11)" )] + public void Test_negative_range_with_default_step_11() + { + var selector = "$[-1:-3]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative range with negative step (12)" )] + public void Test_negative_range_with_negative_step_12() + { + var selector = "$[-1:-3:-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 9, + 8 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative range with larger negative step (13)" )] + public void Test_negative_range_with_larger_negative_step_13() + { + var selector = "$[-1:-6:-2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 9, + 7, + 5 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"larger negative range with larger negative step (14)" )] + public void Test_larger_negative_range_with_larger_negative_step_14() + { + var selector = "$[-1:-7:-2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 9, + 7, + 5 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative from, positive to (15)" )] + public void Test_negative_from__positive_to_15() + { + var selector = "$[-5:7]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 5, + 6 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative from (16)" )] + public void Test_negative_from_16() + { + var selector = "$[-2:]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 8, + 9 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"positive from, negative to (17)" )] + public void Test_positive_from__negative_to_17() + { + var selector = "$[1:-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative from, positive to, negative step (18)" )] + public void Test_negative_from__positive_to__negative_step_18() + { + var selector = "$[-1:1:-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"positive from, negative to, negative step (19)" )] + public void Test_positive_from__negative_to__negative_step_19() + { + var selector = "$[7:-5:-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 7, + 6 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"too many colons (20)" )] + public void Test_too_many_colons_20() + { + var selector = "$[1:2:3:4]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"non-integer array index (21)" )] + public void Test_non_integer_array_index_21() + { + var selector = "$[1:2:a]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"zero step (22)" )] + public void Test_zero_step_22() + { + var selector = "$[1:2:0]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"empty range (23)" )] + public void Test_empty_range_23() + { + var selector = "$[2:2]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice selector with everything omitted with empty array (24)" )] + public void Test_slice_selector_with_everything_omitted_with_empty_array_24() + { + var selector = "$[:]"; + var document = JsonNode.Parse( + """ + [] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"negative step with empty array (25)" )] + public void Test_negative_step_with_empty_array_25() + { + var selector = "$[::-1]"; + var document = JsonNode.Parse( + """ + [] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"maximal range with positive step (26)" )] + public void Test_maximal_range_with_positive_step_26() + { + var selector = "$[0:10]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"maximal range with negative step (27)" )] + public void Test_maximal_range_with_negative_step_27() + { + var selector = "$[9:0:-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"excessively large to value (28)" )] + public void Test_excessively_large_to_value_28() + { + var selector = "$[2:113667776004]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"excessively small from value (29)" )] + public void Test_excessively_small_from_value_29() + { + var selector = "$[-113667776004:1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 0 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"excessively large from value with negative step (30)" )] + public void Test_excessively_large_from_value_with_negative_step_30() + { + var selector = "$[113667776004:0:-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"excessively small to value with negative step (31)" )] + public void Test_excessively_small_to_value_with_negative_step_31() + { + var selector = "$[3:-113667776004:-1]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 3, + 2, + 1, + 0 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"excessively large step (32)" )] + public void Test_excessively_large_step_32() + { + var selector = "$[1:10:113667776004]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 1 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"excessively small step (33)" )] + public void Test_excessively_small_step_33() + { + var selector = "$[-1:-10:-113667776004]"; + var document = JsonNode.Parse( + """ + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 9 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"overflowing to value (34)" )] + public void Test_overflowing_to_value_34() + { + var selector = "$[2:231584178474632390847141970017375815706539969331281128078915168015826259279872]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"underflowing from value (35)" )] + public void Test_underflowing_from_value_35() + { + var selector = "$[-231584178474632390847141970017375815706539969331281128078915168015826259279872:1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"overflowing from value with negative step (36)" )] + public void Test_overflowing_from_value_with_negative_step_36() + { + var selector = "$[231584178474632390847141970017375815706539969331281128078915168015826259279872:0:-1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"underflowing to value with negative step (37)" )] + public void Test_underflowing_to_value_with_negative_step_37() + { + var selector = "$[3:-231584178474632390847141970017375815706539969331281128078915168015826259279872:-1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"overflowing step (38)" )] + public void Test_overflowing_step_38() + { + var selector = "$[1:10:231584178474632390847141970017375815706539969331281128078915168015826259279872]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"underflowing step (39)" )] + public void Test_underflowing_step_39() + { + var selector = "$[-1:-10:-231584178474632390847141970017375815706539969331281128078915168015826259279872]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + } +} + diff --git a/test/Hyperbee.Json.Cts/Tests/cts-whitespace-tests.cs b/test/Hyperbee.Json.Cts/Tests/cts-whitespace-tests.cs new file mode 100644 index 00000000..c892887d --- /dev/null +++ b/test/Hyperbee.Json.Cts/Tests/cts-whitespace-tests.cs @@ -0,0 +1,4929 @@ +// This file was auto generated. + +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Cts.Tests +{ + [TestClass] + public class CtsWhitespaceTest + { + + [TestMethod( @"filter, space between question mark and expression (1)" )] + public void Test_filter__space_between_question_mark_and_expression_1() + { + var selector = "$[? @.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, newline between question mark and expression (2)" )] + public void Test_filter__newline_between_question_mark_and_expression_2() + { + var selector = "$[?\n@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, tab between question mark and expression (3)" )] + public void Test_filter__tab_between_question_mark_and_expression_3() + { + var selector = "$[?\t@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, return between question mark and expression (4)" )] + public void Test_filter__return_between_question_mark_and_expression_4() + { + var selector = "$[?\r@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, space between question mark and parenthesized expression (5)" )] + public void Test_filter__space_between_question_mark_and_parenthesized_expression_5() + { + var selector = "$[? (@.a)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, newline between question mark and parenthesized expression (6)" )] + public void Test_filter__newline_between_question_mark_and_parenthesized_expression_6() + { + var selector = "$[?\n(@.a)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, tab between question mark and parenthesized expression (7)" )] + public void Test_filter__tab_between_question_mark_and_parenthesized_expression_7() + { + var selector = "$[?\t(@.a)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, return between question mark and parenthesized expression (8)" )] + public void Test_filter__return_between_question_mark_and_parenthesized_expression_8() + { + var selector = "$[?\r(@.a)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, space between parenthesized expression and bracket (9)" )] + public void Test_filter__space_between_parenthesized_expression_and_bracket_9() + { + var selector = "$[?(@.a) ]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, newline between parenthesized expression and bracket (10)" )] + public void Test_filter__newline_between_parenthesized_expression_and_bracket_10() + { + var selector = "$[?(@.a)\n]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, tab between parenthesized expression and bracket (11)" )] + public void Test_filter__tab_between_parenthesized_expression_and_bracket_11() + { + var selector = "$[?(@.a)\t]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, return between parenthesized expression and bracket (12)" )] + public void Test_filter__return_between_parenthesized_expression_and_bracket_12() + { + var selector = "$[?(@.a)\r]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, space between bracket and question mark (13)" )] + public void Test_filter__space_between_bracket_and_question_mark_13() + { + var selector = "$[ ?@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, newline between bracket and question mark (14)" )] + public void Test_filter__newline_between_bracket_and_question_mark_14() + { + var selector = "$[\n?@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, tab between bracket and question mark (15)" )] + public void Test_filter__tab_between_bracket_and_question_mark_15() + { + var selector = "$[\t?@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"filter, return between bracket and question mark (16)" )] + public void Test_filter__return_between_bracket_and_question_mark_16() + { + var selector = "$[\r?@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "b", + "d": "e" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, space between function name and parenthesis (17)" )] + public void Test_functions__space_between_function_name_and_parenthesis_17() + { + var selector = "$[?count (@.*)==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"functions, newline between function name and parenthesis (18)" )] + public void Test_functions__newline_between_function_name_and_parenthesis_18() + { + var selector = "$[?count\n(@.*)==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"functions, tab between function name and parenthesis (19)" )] + public void Test_functions__tab_between_function_name_and_parenthesis_19() + { + var selector = "$[?count\t(@.*)==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"functions, return between function name and parenthesis (20)" )] + public void Test_functions__return_between_function_name_and_parenthesis_20() + { + var selector = "$[?count\r(@.*)==1]"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"functions, space between parenthesis and arg (21)" )] + public void Test_functions__space_between_parenthesis_and_arg_21() + { + var selector = "$[?count( @.*)==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, newline between parenthesis and arg (22)" )] + public void Test_functions__newline_between_parenthesis_and_arg_22() + { + var selector = "$[?count(\n@.*)==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, tab between parenthesis and arg (23)" )] + public void Test_functions__tab_between_parenthesis_and_arg_23() + { + var selector = "$[?count(\t@.*)==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, return between parenthesis and arg (24)" )] + public void Test_functions__return_between_parenthesis_and_arg_24() + { + var selector = "$[?count(\r@.*)==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, space between arg and comma (25)" )] + public void Test_functions__space_between_arg_and_comma_25() + { + var selector = "$[?search(@ ,'[a-z]+')]"; + var document = JsonNode.Parse( + """ + [ + "foo", + "123" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, newline between arg and comma (26)" )] + public void Test_functions__newline_between_arg_and_comma_26() + { + var selector = "$[?search(@\n,'[a-z]+')]"; + var document = JsonNode.Parse( + """ + [ + "foo", + "123" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, tab between arg and comma (27)" )] + public void Test_functions__tab_between_arg_and_comma_27() + { + var selector = "$[?search(@\t,'[a-z]+')]"; + var document = JsonNode.Parse( + """ + [ + "foo", + "123" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, return between arg and comma (28)" )] + public void Test_functions__return_between_arg_and_comma_28() + { + var selector = "$[?search(@\r,'[a-z]+')]"; + var document = JsonNode.Parse( + """ + [ + "foo", + "123" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, space between comma and arg (29)" )] + public void Test_functions__space_between_comma_and_arg_29() + { + var selector = "$[?search(@, '[a-z]+')]"; + var document = JsonNode.Parse( + """ + [ + "foo", + "123" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, newline between comma and arg (30)" )] + public void Test_functions__newline_between_comma_and_arg_30() + { + var selector = "$[?search(@,\n'[a-z]+')]"; + var document = JsonNode.Parse( + """ + [ + "foo", + "123" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, tab between comma and arg (31)" )] + public void Test_functions__tab_between_comma_and_arg_31() + { + var selector = "$[?search(@,\t'[a-z]+')]"; + var document = JsonNode.Parse( + """ + [ + "foo", + "123" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, return between comma and arg (32)" )] + public void Test_functions__return_between_comma_and_arg_32() + { + var selector = "$[?search(@,\r'[a-z]+')]"; + var document = JsonNode.Parse( + """ + [ + "foo", + "123" + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, space between arg and parenthesis (33)" )] + public void Test_functions__space_between_arg_and_parenthesis_33() + { + var selector = "$[?count(@.* )==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, newline between arg and parenthesis (34)" )] + public void Test_functions__newline_between_arg_and_parenthesis_34() + { + var selector = "$[?count(@.*\n)==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, tab between arg and parenthesis (35)" )] + public void Test_functions__tab_between_arg_and_parenthesis_35() + { + var selector = "$[?count(@.*\t)==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, return between arg and parenthesis (36)" )] + public void Test_functions__return_between_arg_and_parenthesis_36() + { + var selector = "$[?count(@.*\r)==1]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, spaces in a relative singular selector (37)" )] + public void Test_functions__spaces_in_a_relative_singular_selector_37() + { + var selector = "$[?length(@ .a .b) == 3]"; + var document = JsonNode.Parse( + """ + [ + { + "a": { + "b": "foo" + } + }, + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": { + "b": "foo" + } + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, newlines in a relative singular selector (38)" )] + public void Test_functions__newlines_in_a_relative_singular_selector_38() + { + var selector = "$[?length(@\n.a\n.b) == 3]"; + var document = JsonNode.Parse( + """ + [ + { + "a": { + "b": "foo" + } + }, + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": { + "b": "foo" + } + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, tabs in a relative singular selector (39)" )] + public void Test_functions__tabs_in_a_relative_singular_selector_39() + { + var selector = "$[?length(@\t.a\t.b) == 3]"; + var document = JsonNode.Parse( + """ + [ + { + "a": { + "b": "foo" + } + }, + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": { + "b": "foo" + } + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, returns in a relative singular selector (40)" )] + public void Test_functions__returns_in_a_relative_singular_selector_40() + { + var selector = "$[?length(@\r.a\r.b) == 3]"; + var document = JsonNode.Parse( + """ + [ + { + "a": { + "b": "foo" + } + }, + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": { + "b": "foo" + } + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, spaces in an absolute singular selector (41)" )] + public void Test_functions__spaces_in_an_absolute_singular_selector_41() + { + var selector = "$..[?length(@)==length($ [0] .a)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "foo" + }, + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, newlines in an absolute singular selector (42)" )] + public void Test_functions__newlines_in_an_absolute_singular_selector_42() + { + var selector = "$..[?length(@)==length($\n[0]\n.a)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "foo" + }, + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, tabs in an absolute singular selector (43)" )] + public void Test_functions__tabs_in_an_absolute_singular_selector_43() + { + var selector = "$..[?length(@)==length($\t[0]\t.a)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "foo" + }, + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"functions, returns in an absolute singular selector (44)" )] + public void Test_functions__returns_in_an_absolute_singular_selector_44() + { + var selector = "$..[?length(@)==length($\r[0]\r.a)]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "foo" + }, + {} + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "foo" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space before || (45)" )] + public void Test_operators__space_before____45() + { + var selector = "$[?@.a ||@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline before || (46)" )] + public void Test_operators__newline_before____46() + { + var selector = "$[?@.a\n||@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab before || (47)" )] + public void Test_operators__tab_before____47() + { + var selector = "$[?@.a\t||@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return before || (48)" )] + public void Test_operators__return_before____48() + { + var selector = "$[?@.a\r||@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space after || (49)" )] + public void Test_operators__space_after____49() + { + var selector = "$[?@.a|| @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline after || (50)" )] + public void Test_operators__newline_after____50() + { + var selector = "$[?@.a||\n@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab after || (51)" )] + public void Test_operators__tab_after____51() + { + var selector = "$[?@.a||\t@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return after || (52)" )] + public void Test_operators__return_after____52() + { + var selector = "$[?@.a||\r@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space before && (53)" )] + public void Test_operators__space_before____53() + { + var selector = "$[?@.a &&@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline before && (54)" )] + public void Test_operators__newline_before____54() + { + var selector = "$[?@.a\n&&@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab before && (55)" )] + public void Test_operators__tab_before____55() + { + var selector = "$[?@.a\t&&@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return before && (56)" )] + public void Test_operators__return_before____56() + { + var selector = "$[?@.a\r&&@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space after && (57)" )] + public void Test_operators__space_after____57() + { + var selector = "$[?@.a&& @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline after && (58)" )] + public void Test_operators__newline_after____58() + { + var selector = "$[?@.a&& @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab after && (59)" )] + public void Test_operators__tab_after____59() + { + var selector = "$[?@.a&& @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return after && (60)" )] + public void Test_operators__return_after____60() + { + var selector = "$[?@.a&& @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space before == (61)" )] + public void Test_operators__space_before____61() + { + var selector = "$[?@.a ==@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline before == (62)" )] + public void Test_operators__newline_before____62() + { + var selector = "$[?@.a\n==@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab before == (63)" )] + public void Test_operators__tab_before____63() + { + var selector = "$[?@.a\t==@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return before == (64)" )] + public void Test_operators__return_before____64() + { + var selector = "$[?@.a\r==@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space after == (65)" )] + public void Test_operators__space_after____65() + { + var selector = "$[?@.a== @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline after == (66)" )] + public void Test_operators__newline_after____66() + { + var selector = "$[?@.a==\n@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab after == (67)" )] + public void Test_operators__tab_after____67() + { + var selector = "$[?@.a==\t@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return after == (68)" )] + public void Test_operators__return_after____68() + { + var selector = "$[?@.a==\r@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space before != (69)" )] + public void Test_operators__space_before____69() + { + var selector = "$[?@.a !=@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline before != (70)" )] + public void Test_operators__newline_before____70() + { + var selector = "$[?@.a\n!=@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab before != (71)" )] + public void Test_operators__tab_before____71() + { + var selector = "$[?@.a\t!=@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return before != (72)" )] + public void Test_operators__return_before____72() + { + var selector = "$[?@.a\r!=@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space after != (73)" )] + public void Test_operators__space_after____73() + { + var selector = "$[?@.a!= @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline after != (74)" )] + public void Test_operators__newline_after____74() + { + var selector = "$[?@.a!=\n@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab after != (75)" )] + public void Test_operators__tab_after____75() + { + var selector = "$[?@.a!=\t@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return after != (76)" )] + public void Test_operators__return_after____76() + { + var selector = "$[?@.a!=\r@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space before < (77)" )] + public void Test_operators__space_before___77() + { + var selector = "$[?@.a <@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline before < (78)" )] + public void Test_operators__newline_before___78() + { + var selector = "$[?@.a\n<@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab before < (79)" )] + public void Test_operators__tab_before___79() + { + var selector = "$[?@.a\t<@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return before < (80)" )] + public void Test_operators__return_before___80() + { + var selector = "$[?@.a\r<@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space after < (81)" )] + public void Test_operators__space_after___81() + { + var selector = "$[?@.a< @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline after < (82)" )] + public void Test_operators__newline_after___82() + { + var selector = "$[?@.a<\n@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab after < (83)" )] + public void Test_operators__tab_after___83() + { + var selector = "$[?@.a<\t@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return after < (84)" )] + public void Test_operators__return_after___84() + { + var selector = "$[?@.a<\r@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space before > (85)" )] + public void Test_operators__space_before___85() + { + var selector = "$[?@.b >@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline before > (86)" )] + public void Test_operators__newline_before___86() + { + var selector = "$[?@.b\n>@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab before > (87)" )] + public void Test_operators__tab_before___87() + { + var selector = "$[?@.b\t>@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return before > (88)" )] + public void Test_operators__return_before___88() + { + var selector = "$[?@.b\r>@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space after > (89)" )] + public void Test_operators__space_after___89() + { + var selector = "$[?@.b> @.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline after > (90)" )] + public void Test_operators__newline_after___90() + { + var selector = "$[?@.b>\n@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab after > (91)" )] + public void Test_operators__tab_after___91() + { + var selector = "$[?@.b>\t@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return after > (92)" )] + public void Test_operators__return_after___92() + { + var selector = "$[?@.b>\r@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space before <= (93)" )] + public void Test_operators__space_before____93() + { + var selector = "$[?@.a <=@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline before <= (94)" )] + public void Test_operators__newline_before____94() + { + var selector = "$[?@.a\n<=@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab before <= (95)" )] + public void Test_operators__tab_before____95() + { + var selector = "$[?@.a\t<=@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return before <= (96)" )] + public void Test_operators__return_before____96() + { + var selector = "$[?@.a\r<=@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space after <= (97)" )] + public void Test_operators__space_after____97() + { + var selector = "$[?@.a<= @.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline after <= (98)" )] + public void Test_operators__newline_after____98() + { + var selector = "$[?@.a<=\n@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab after <= (99)" )] + public void Test_operators__tab_after____99() + { + var selector = "$[?@.a<=\t@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return after <= (100)" )] + public void Test_operators__return_after____100() + { + var selector = "$[?@.a<=\r@.b]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space before >= (101)" )] + public void Test_operators__space_before____101() + { + var selector = "$[?@.b >=@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline before >= (102)" )] + public void Test_operators__newline_before____102() + { + var selector = "$[?@.b\n>=@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab before >= (103)" )] + public void Test_operators__tab_before____103() + { + var selector = "$[?@.b\t>=@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return before >= (104)" )] + public void Test_operators__return_before____104() + { + var selector = "$[?@.b\r>=@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space after >= (105)" )] + public void Test_operators__space_after____105() + { + var selector = "$[?@.b>= @.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline after >= (106)" )] + public void Test_operators__newline_after____106() + { + var selector = "$[?@.b>=\n@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab after >= (107)" )] + public void Test_operators__tab_after____107() + { + var selector = "$[?@.b>=\t@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return after >= (108)" )] + public void Test_operators__return_after____108() + { + var selector = "$[?@.b>=\r@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space between logical not and test expression (109)" )] + public void Test_operators__space_between_logical_not_and_test_expression_109() + { + var selector = "$[?! @.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline between logical not and test expression (110)" )] + public void Test_operators__newline_between_logical_not_and_test_expression_110() + { + var selector = "$[?!\n@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab between logical not and test expression (111)" )] + public void Test_operators__tab_between_logical_not_and_test_expression_111() + { + var selector = "$[?!\t@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return between logical not and test expression (112)" )] + public void Test_operators__return_between_logical_not_and_test_expression_112() + { + var selector = "$[?!\r@.a]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, space between logical not and parenthesized expression (113)" )] + public void Test_operators__space_between_logical_not_and_parenthesized_expression_113() + { + var selector = "$[?! (@.a=='b')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, newline between logical not and parenthesized expression (114)" )] + public void Test_operators__newline_between_logical_not_and_parenthesized_expression_114() + { + var selector = "$[?!\n(@.a=='b')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, tab between logical not and parenthesized expression (115)" )] + public void Test_operators__tab_between_logical_not_and_parenthesized_expression_115() + { + var selector = "$[?!\t(@.a=='b')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"operators, return between logical not and parenthesized expression (116)" )] + public void Test_operators__return_between_logical_not_and_parenthesized_expression_116() + { + var selector = "$[?!\r(@.a=='b')]"; + var document = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, space between root and bracket (117)" )] + public void Test_selectors__space_between_root_and_bracket_117() + { + var selector = "$ ['a']"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, newline between root and bracket (118)" )] + public void Test_selectors__newline_between_root_and_bracket_118() + { + var selector = "$\n['a']"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, tab between root and bracket (119)" )] + public void Test_selectors__tab_between_root_and_bracket_119() + { + var selector = "$\t['a']"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, return between root and bracket (120)" )] + public void Test_selectors__return_between_root_and_bracket_120() + { + var selector = "$\r['a']"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, space between bracket and bracket (121)" )] + public void Test_selectors__space_between_bracket_and_bracket_121() + { + var selector = "$['a'] ['b']"; + var document = JsonNode.Parse( + """ + { + "a": { + "b": "ab" + } + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, newline between root and bracket (122)" )] + public void Test_selectors__newline_between_root_and_bracket_122() + { + var selector = "$['a'] \n['b']"; + var document = JsonNode.Parse( + """ + { + "a": { + "b": "ab" + } + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, tab between root and bracket (123)" )] + public void Test_selectors__tab_between_root_and_bracket_123() + { + var selector = "$['a'] \t['b']"; + var document = JsonNode.Parse( + """ + { + "a": { + "b": "ab" + } + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, return between root and bracket (124)" )] + public void Test_selectors__return_between_root_and_bracket_124() + { + var selector = "$['a'] \r['b']"; + var document = JsonNode.Parse( + """ + { + "a": { + "b": "ab" + } + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, space between root and dot (125)" )] + public void Test_selectors__space_between_root_and_dot_125() + { + var selector = "$ .a"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, newline between root and dot (126)" )] + public void Test_selectors__newline_between_root_and_dot_126() + { + var selector = "$\n.a"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, tab between root and dot (127)" )] + public void Test_selectors__tab_between_root_and_dot_127() + { + var selector = "$\t.a"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, return between root and dot (128)" )] + public void Test_selectors__return_between_root_and_dot_128() + { + var selector = "$\r.a"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, space between dot and name (129)" )] + public void Test_selectors__space_between_dot_and_name_129() + { + var selector = "$. a"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"selectors, newline between dot and name (130)" )] + public void Test_selectors__newline_between_dot_and_name_130() + { + var selector = "$.\na"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"selectors, tab between dot and name (131)" )] + public void Test_selectors__tab_between_dot_and_name_131() + { + var selector = "$.\ta"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"selectors, return between dot and name (132)" )] + public void Test_selectors__return_between_dot_and_name_132() + { + var selector = "$.\ra"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"selectors, space between recursive descent and name (133)" )] + public void Test_selectors__space_between_recursive_descent_and_name_133() + { + var selector = "$.. a"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"selectors, newline between recursive descent and name (134)" )] + public void Test_selectors__newline_between_recursive_descent_and_name_134() + { + var selector = "$..\na"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"selectors, tab between recursive descent and name (135)" )] + public void Test_selectors__tab_between_recursive_descent_and_name_135() + { + var selector = "$..\ta"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"selectors, return between recursive descent and name (136)" )] + public void Test_selectors__return_between_recursive_descent_and_name_136() + { + var selector = "$..\ra"; + var document = JsonNode.Parse( "[0]" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + } + + [TestMethod( @"selectors, space between bracket and selector (137)" )] + public void Test_selectors__space_between_bracket_and_selector_137() + { + var selector = "$[ 'a']"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, newline between bracket and selector (138)" )] + public void Test_selectors__newline_between_bracket_and_selector_138() + { + var selector = "$[\n'a']"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, tab between bracket and selector (139)" )] + public void Test_selectors__tab_between_bracket_and_selector_139() + { + var selector = "$[\t'a']"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, return between bracket and selector (140)" )] + public void Test_selectors__return_between_bracket_and_selector_140() + { + var selector = "$[\r'a']"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, space between selector and bracket (141)" )] + public void Test_selectors__space_between_selector_and_bracket_141() + { + var selector = "$['a' ]"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, newline between selector and bracket (142)" )] + public void Test_selectors__newline_between_selector_and_bracket_142() + { + var selector = "$['a'\n]"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, tab between selector and bracket (143)" )] + public void Test_selectors__tab_between_selector_and_bracket_143() + { + var selector = "$['a'\t]"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, return between selector and bracket (144)" )] + public void Test_selectors__return_between_selector_and_bracket_144() + { + var selector = "$['a'\r]"; + var document = JsonNode.Parse( + """ + { + "a": "ab" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, space between selector and comma (145)" )] + public void Test_selectors__space_between_selector_and_comma_145() + { + var selector = "$['a' ,'b']"; + var document = JsonNode.Parse( + """ + { + "a": "ab", + "b": "bc" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab", + "bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, newline between selector and comma (146)" )] + public void Test_selectors__newline_between_selector_and_comma_146() + { + var selector = "$['a'\n,'b']"; + var document = JsonNode.Parse( + """ + { + "a": "ab", + "b": "bc" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab", + "bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, tab between selector and comma (147)" )] + public void Test_selectors__tab_between_selector_and_comma_147() + { + var selector = "$['a'\t,'b']"; + var document = JsonNode.Parse( + """ + { + "a": "ab", + "b": "bc" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab", + "bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, return between selector and comma (148)" )] + public void Test_selectors__return_between_selector_and_comma_148() + { + var selector = "$['a'\r,'b']"; + var document = JsonNode.Parse( + """ + { + "a": "ab", + "b": "bc" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab", + "bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, space between comma and selector (149)" )] + public void Test_selectors__space_between_comma_and_selector_149() + { + var selector = "$['a', 'b']"; + var document = JsonNode.Parse( + """ + { + "a": "ab", + "b": "bc" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab", + "bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, newline between comma and selector (150)" )] + public void Test_selectors__newline_between_comma_and_selector_150() + { + var selector = "$['a',\n'b']"; + var document = JsonNode.Parse( + """ + { + "a": "ab", + "b": "bc" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab", + "bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, tab between comma and selector (151)" )] + public void Test_selectors__tab_between_comma_and_selector_151() + { + var selector = "$['a',\t'b']"; + var document = JsonNode.Parse( + """ + { + "a": "ab", + "b": "bc" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab", + "bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"selectors, return between comma and selector (152)" )] + public void Test_selectors__return_between_comma_and_selector_152() + { + var selector = "$['a',\r'b']"; + var document = JsonNode.Parse( + """ + { + "a": "ab", + "b": "bc" + } + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + "ab", + "bc" + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, space between start and colon (153)" )] + public void Test_slice__space_between_start_and_colon_153() + { + var selector = "$[1 :5:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, newline between start and colon (154)" )] + public void Test_slice__newline_between_start_and_colon_154() + { + var selector = "$[1\n:5:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, tab between start and colon (155)" )] + public void Test_slice__tab_between_start_and_colon_155() + { + var selector = "$[1\t:5:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, return between start and colon (156)" )] + public void Test_slice__return_between_start_and_colon_156() + { + var selector = "$[1\r:5:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, space between colon and end (157)" )] + public void Test_slice__space_between_colon_and_end_157() + { + var selector = "$[1: 5:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, newline between colon and end (158)" )] + public void Test_slice__newline_between_colon_and_end_158() + { + var selector = "$[1:\n5:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, tab between colon and end (159)" )] + public void Test_slice__tab_between_colon_and_end_159() + { + var selector = "$[1:\t5:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, return between colon and end (160)" )] + public void Test_slice__return_between_colon_and_end_160() + { + var selector = "$[1:\r5:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, space between end and colon (161)" )] + public void Test_slice__space_between_end_and_colon_161() + { + var selector = "$[1:5 :2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, newline between end and colon (162)" )] + public void Test_slice__newline_between_end_and_colon_162() + { + var selector = "$[1:5\n:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, tab between end and colon (163)" )] + public void Test_slice__tab_between_end_and_colon_163() + { + var selector = "$[1:5\t:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, return between end and colon (164)" )] + public void Test_slice__return_between_end_and_colon_164() + { + var selector = "$[1:5\r:2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, space between colon and step (165)" )] + public void Test_slice__space_between_colon_and_step_165() + { + var selector = "$[1:5: 2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, newline between colon and step (166)" )] + public void Test_slice__newline_between_colon_and_step_166() + { + var selector = "$[1:5:\n2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, tab between colon and step (167)" )] + public void Test_slice__tab_between_colon_and_step_167() + { + var selector = "$[1:5:\t2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + + [TestMethod( @"slice, return between colon and step (168)" )] + public void Test_slice__return_between_colon_and_step_168() + { + var selector = "$[1:5:\r2]"; + var document = JsonNode.Parse( + """ + [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + """ ); + var results = document.Select( selector ); + var expect = JsonNode.Parse( + """ + [ + 2, + 4 + ] + """ ); + + var match = TestHelper.MatchOne( results, expect! ); + Assert.IsTrue( match ); + } + } +} + diff --git a/test/Hyperbee.Json.Cts/cts.json b/test/Hyperbee.Json.Cts/cts.json new file mode 100644 index 00000000..328bcb80 --- /dev/null +++ b/test/Hyperbee.Json.Cts/cts.json @@ -0,0 +1,8624 @@ +{ + "description": "JSONPath Compliance Test Suite. This file is autogenerated, do not edit.", + "tests": [ + { + "name": "basic, root", + "selector": "$", + "document": [ + "first", + "second" + ], + "result": [ + [ + "first", + "second" + ] + ] + }, + { + "name": "basic, no leading whitespace", + "selector": " $", + "invalid_selector": true + }, + { + "name": "basic, no trailing whitespace", + "selector": "$ ", + "invalid_selector": true + }, + { + "name": "basic, name shorthand", + "selector": "$.a", + "document": { + "a": "A", + "b": "B" + }, + "result": [ + "A" + ] + }, + { + "name": "basic, name shorthand, extended unicode ☺", + "selector": "$.☺", + "document": { + "☺": "A", + "b": "B" + }, + "result": [ + "A" + ] + }, + { + "name": "basic, name shorthand, underscore", + "selector": "$._", + "document": { + "_": "A", + "_foo": "B" + }, + "result": [ + "A" + ] + }, + { + "name": "basic, name shorthand, symbol", + "selector": "$.&", + "invalid_selector": true + }, + { + "name": "basic, name shorthand, number", + "selector": "$.1", + "invalid_selector": true + }, + { + "name": "basic, name shorthand, absent data", + "selector": "$.c", + "document": { + "a": "A", + "b": "B" + }, + "result": [] + }, + { + "name": "basic, name shorthand, array data", + "selector": "$.a", + "document": [ + "first", + "second" + ], + "result": [] + }, + { + "name": "basic, wildcard shorthand, object data", + "selector": "$.*", + "document": { + "a": "A", + "b": "B" + }, + "results": [ + [ + "A", + "B" + ], + [ + "B", + "A" + ] + ] + }, + { + "name": "basic, wildcard shorthand, array data", + "selector": "$.*", + "document": [ + "first", + "second" + ], + "result": [ + "first", + "second" + ] + }, + { + "name": "basic, wildcard selector, array data", + "selector": "$[*]", + "document": [ + "first", + "second" + ], + "result": [ + "first", + "second" + ] + }, + { + "name": "basic, wildcard shorthand, then name shorthand", + "selector": "$.*.a", + "document": { + "x": { + "a": "Ax", + "b": "Bx" + }, + "y": { + "a": "Ay", + "b": "By" + } + }, + "results": [ + [ + "Ax", + "Ay" + ], + [ + "Ay", + "Ax" + ] + ] + }, + { + "name": "basic, multiple selectors", + "selector": "$[0,2]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 0, + 2 + ] + }, + { + "name": "basic, multiple selectors, space instead of comma", + "selector": "$[0 2]", + "invalid_selector": true + }, + { + "name": "basic, multiple selectors, name and index, array data", + "selector": "$['a',1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 1 + ] + }, + { + "name": "basic, multiple selectors, name and index, object data", + "selector": "$['a',1]", + "document": { + "a": 1, + "b": 2 + }, + "result": [ + 1 + ] + }, + { + "name": "basic, multiple selectors, index and slice", + "selector": "$[1,5:7]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 1, + 5, + 6 + ] + }, + { + "name": "basic, multiple selectors, index and slice, overlapping", + "selector": "$[1,0:3]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 1, + 0, + 1, + 2 + ] + }, + { + "name": "basic, multiple selectors, duplicate index", + "selector": "$[1,1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 1, + 1 + ] + }, + { + "name": "basic, multiple selectors, wildcard and index", + "selector": "$[*,1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 1 + ] + }, + { + "name": "basic, multiple selectors, wildcard and name", + "selector": "$[*,'a']", + "document": { + "a": "A", + "b": "B" + }, + "results": [ + [ + "A", + "B", + "A" + ], + [ + "B", + "A", + "A" + ] + ] + }, + { + "name": "basic, multiple selectors, wildcard and slice", + "selector": "$[*,0:2]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 0, + 1 + ] + }, + { + "name": "basic, multiple selectors, multiple wildcards", + "selector": "$[*,*]", + "document": [ + 0, + 1, + 2 + ], + "result": [ + 0, + 1, + 2, + 0, + 1, + 2 + ] + }, + { + "name": "basic, empty segment", + "selector": "$[]", + "invalid_selector": true + }, + { + "name": "basic, descendant segment, index", + "selector": "$..[1]", + "document": { + "o": [ + 0, + 1, + [ + 2, + 3 + ] + ] + }, + "result": [ + 1, + 3 + ] + }, + { + "name": "basic, descendant segment, name shorthand", + "selector": "$..a", + "document": { + "o": [ + { + "a": "b" + }, + { + "a": "c" + } + ] + }, + "result": [ + "b", + "c" + ] + }, + { + "name": "basic, descendant segment, wildcard shorthand, array data", + "selector": "$..*", + "document": [ + 0, + 1 + ], + "result": [ + 0, + 1 + ] + }, + { + "name": "basic, descendant segment, wildcard selector, array data", + "selector": "$..[*]", + "document": [ + 0, + 1 + ], + "result": [ + 0, + 1 + ] + }, + { + "name": "basic, descendant segment, wildcard selector, nested arrays", + "selector": "$..[*]", + "document": [ + [ + [ + 1 + ] + ], + [ + 2 + ] + ], + "results": [ + [ + [ + [ + 1 + ] + ], + [ + 2 + ], + [ + 1 + ], + 1, + 2 + ], + [ + [ + [ + 1 + ] + ], + [ + 2 + ], + [ + 1 + ], + 2, + 1 + ] + ] + }, + { + "name": "basic, descendant segment, wildcard selector, nested objects", + "selector": "$..[*]", + "document": { + "a": { + "c": { + "e": 1 + } + }, + "b": { + "d": 2 + } + }, + "results": [ + [ + { + "c": { + "e": 1 + } + }, + { + "d": 2 + }, + { + "e": 1 + }, + 1, + 2 + ], + [ + { + "c": { + "e": 1 + } + }, + { + "d": 2 + }, + { + "e": 1 + }, + 2, + 1 + ], + [ + { + "c": { + "e": 1 + } + }, + { + "d": 2 + }, + 2, + { + "e": 1 + }, + 1 + ], + [ + { + "d": 2 + }, + { + "c": { + "e": 1 + } + }, + { + "e": 1 + }, + 1, + 2 + ], + [ + { + "d": 2 + }, + { + "c": { + "e": 1 + } + }, + { + "e": 1 + }, + 2, + 1 + ], + [ + { + "d": 2 + }, + { + "c": { + "e": 1 + } + }, + 2, + { + "e": 1 + }, + 1 + ] + ] + }, + { + "name": "basic, descendant segment, wildcard shorthand, object data", + "selector": "$..*", + "document": { + "a": "b" + }, + "result": [ + "b" + ] + }, + { + "name": "basic, descendant segment, wildcard shorthand, nested data", + "selector": "$..*", + "document": { + "o": [ + { + "a": "b" + } + ] + }, + "result": [ + [ + { + "a": "b" + } + ], + { + "a": "b" + }, + "b" + ] + }, + { + "name": "basic, descendant segment, multiple selectors", + "selector": "$..['a','d']", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + "b", + "e", + "c", + "f" + ] + }, + { + "name": "basic, descendant segment, object traversal, multiple selectors", + "selector": "$..['a','d']", + "document": { + "x": { + "a": "b", + "d": "e" + }, + "y": { + "a": "c", + "d": "f" + } + }, + "results": [ + [ + "b", + "e", + "c", + "f" + ], + [ + "c", + "f", + "b", + "e" + ] + ] + }, + { + "name": "basic, bald descendant segment", + "selector": "$..", + "invalid_selector": true + }, + { + "name": "filter, existence, without segments", + "selector": "$[?@]", + "document": { + "a": 1, + "b": null + }, + "results": [ + [ + 1, + null + ], + [ + null, + 1 + ] + ] + }, + { + "name": "filter, existence", + "selector": "$[?@.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "filter, existence, present with null", + "selector": "$[?@.a]", + "document": [ + { + "a": null, + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": null, + "d": "e" + } + ] + }, + { + "name": "filter, equals string, single quotes", + "selector": "$[?@.a=='b']", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "filter, equals numeric string, single quotes", + "selector": "$[?@.a=='1']", + "document": [ + { + "a": "1", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [ + { + "a": "1", + "d": "e" + } + ] + }, + { + "name": "filter, equals string, double quotes", + "selector": "$[?@.a==\"b\"]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "filter, equals numeric string, double quotes", + "selector": "$[?@.a==\"1\"]", + "document": [ + { + "a": "1", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [ + { + "a": "1", + "d": "e" + } + ] + }, + { + "name": "filter, equals number", + "selector": "$[?@.a==1]", + "document": [ + { + "a": 1, + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": 2, + "d": "f" + }, + { + "a": "1", + "d": "f" + } + ], + "result": [ + { + "a": 1, + "d": "e" + } + ] + }, + { + "name": "filter, equals null", + "selector": "$[?@.a==null]", + "document": [ + { + "a": null, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": null, + "d": "e" + } + ] + }, + { + "name": "filter, equals null, absent from data", + "selector": "$[?@.a==null]", + "document": [ + { + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [] + }, + { + "name": "filter, equals true", + "selector": "$[?@.a==true]", + "document": [ + { + "a": true, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": true, + "d": "e" + } + ] + }, + { + "name": "filter, equals false", + "selector": "$[?@.a==false]", + "document": [ + { + "a": false, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": false, + "d": "e" + } + ] + }, + { + "name": "filter, equals self", + "selector": "$[?@==@]", + "document": [ + 1, + null, + true, + { + "a": "b" + }, + [ + false + ] + ], + "result": [ + 1, + null, + true, + { + "a": "b" + }, + [ + false + ] + ] + }, + { + "name": "filter, deep equality, arrays", + "selector": "$[?@.a==@.b]", + "document": [ + { + "a": false, + "b": [ + 1, + 2 + ] + }, + { + "a": [ + [ + 1, + [ + 2 + ] + ] + ], + "b": [ + [ + 1, + [ + 2 + ] + ] + ] + }, + { + "a": [ + [ + 1, + [ + 2 + ] + ] + ], + "b": [ + [ + [ + 2 + ], + 1 + ] + ] + }, + { + "a": [ + [ + 1, + [ + 2 + ] + ] + ], + "b": [ + [ + 1, + 2 + ] + ] + } + ], + "result": [ + { + "a": [ + [ + 1, + [ + 2 + ] + ] + ], + "b": [ + [ + 1, + [ + 2 + ] + ] + ] + } + ] + }, + { + "name": "filter, deep equality, objects", + "selector": "$[?@.a==@.b]", + "document": [ + { + "a": false, + "b": { + "x": 1, + "y": { + "z": 1 + } + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "x": 1, + "y": { + "z": 1 + } + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "y": { + "z": 1 + }, + "x": 1 + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "x": 1 + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "x": 1, + "y": { + "z": 2 + } + } + } + ], + "result": [ + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "x": 1, + "y": { + "z": 1 + } + } + }, + { + "a": { + "x": 1, + "y": { + "z": 1 + } + }, + "b": { + "y": { + "z": 1 + }, + "x": 1 + } + } + ] + }, + { + "name": "filter, not-equals string, single quotes", + "selector": "$[?@.a!='b']", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + } + ] + }, + { + "name": "filter, not-equals numeric string, single quotes", + "selector": "$[?@.a!='1']", + "document": [ + { + "a": "1", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [ + { + "a": 1, + "d": "f" + } + ] + }, + { + "name": "filter, not-equals string, single quotes, different type", + "selector": "$[?@.a!='b']", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [ + { + "a": 1, + "d": "f" + } + ] + }, + { + "name": "filter, not-equals string, double quotes", + "selector": "$[?@.a!=\"b\"]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + } + ] + }, + { + "name": "filter, not-equals numeric string, double quotes", + "selector": "$[?@.a!=\"1\"]", + "document": [ + { + "a": "1", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [ + { + "a": 1, + "d": "f" + } + ] + }, + { + "name": "filter, not-equals string, double quotes, different types", + "selector": "$[?@.a!=\"b\"]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [ + { + "a": 1, + "d": "f" + } + ] + }, + { + "name": "filter, not-equals number", + "selector": "$[?@.a!=1]", + "document": [ + { + "a": 1, + "d": "e" + }, + { + "a": 2, + "d": "f" + }, + { + "a": "1", + "d": "f" + } + ], + "result": [ + { + "a": 2, + "d": "f" + }, + { + "a": "1", + "d": "f" + } + ] + }, + { + "name": "filter, not-equals number, different types", + "selector": "$[?@.a!=1]", + "document": [ + { + "a": 1, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + } + ] + }, + { + "name": "filter, not-equals null", + "selector": "$[?@.a!=null]", + "document": [ + { + "a": null, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + } + ] + }, + { + "name": "filter, not-equals null, absent from data", + "selector": "$[?@.a!=null]", + "document": [ + { + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ] + }, + { + "name": "filter, not-equals true", + "selector": "$[?@.a!=true]", + "document": [ + { + "a": true, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + } + ] + }, + { + "name": "filter, not-equals false", + "selector": "$[?@.a!=false]", + "document": [ + { + "a": false, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + } + ] + }, + { + "name": "filter, less than string, single quotes", + "selector": "$[?@.a<'c']", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "filter, less than string, double quotes", + "selector": "$[?@.a<\"c\"]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "filter, less than number", + "selector": "$[?@.a<10]", + "document": [ + { + "a": 1, + "d": "e" + }, + { + "a": 10, + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": 20, + "d": "f" + } + ], + "result": [ + { + "a": 1, + "d": "e" + } + ] + }, + { + "name": "filter, less than null", + "selector": "$[?@.a'c']", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "filter, greater than string, double quotes", + "selector": "$[?@.a>\"c\"]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "filter, greater than number", + "selector": "$[?@.a>10]", + "document": [ + { + "a": 1, + "d": "e" + }, + { + "a": 10, + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": 20, + "d": "f" + } + ], + "result": [ + { + "a": 20, + "d": "f" + } + ] + }, + { + "name": "filter, greater than null", + "selector": "$[?@.a>null]", + "document": [ + { + "a": null, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [] + }, + { + "name": "filter, greater than true", + "selector": "$[?@.a>true]", + "document": [ + { + "a": true, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [] + }, + { + "name": "filter, greater than false", + "selector": "$[?@.a>false]", + "document": [ + { + "a": false, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [] + }, + { + "name": "filter, greater than or equal to string, single quotes", + "selector": "$[?@.a>='c']", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "filter, greater than or equal to string, double quotes", + "selector": "$[?@.a>=\"c\"]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "filter, greater than or equal to number", + "selector": "$[?@.a>=10]", + "document": [ + { + "a": 1, + "d": "e" + }, + { + "a": 10, + "d": "e" + }, + { + "a": "c", + "d": "f" + }, + { + "a": 20, + "d": "f" + } + ], + "result": [ + { + "a": 10, + "d": "e" + }, + { + "a": 20, + "d": "f" + } + ] + }, + { + "name": "filter, greater than or equal to null", + "selector": "$[?@.a>=null]", + "document": [ + { + "a": null, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": null, + "d": "e" + } + ] + }, + { + "name": "filter, greater than or equal to true", + "selector": "$[?@.a>=true]", + "document": [ + { + "a": true, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": true, + "d": "e" + } + ] + }, + { + "name": "filter, greater than or equal to false", + "selector": "$[?@.a>=false]", + "document": [ + { + "a": false, + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": false, + "d": "e" + } + ] + }, + { + "name": "filter, exists and not-equals null, absent from data", + "selector": "$[?@.a&&@.a!=null]", + "document": [ + { + "d": "e" + }, + { + "a": "c", + "d": "f" + } + ], + "result": [ + { + "a": "c", + "d": "f" + } + ] + }, + { + "name": "filter, exists and exists, data false", + "selector": "$[?@.a&&@.b]", + "document": [ + { + "a": false, + "b": false + }, + { + "b": false + }, + { + "c": false + } + ], + "result": [ + { + "a": false, + "b": false + } + ] + }, + { + "name": "filter, exists or exists, data false", + "selector": "$[?@.a||@.b]", + "document": [ + { + "a": false, + "b": false + }, + { + "b": false + }, + { + "c": false + } + ], + "result": [ + { + "a": false, + "b": false + }, + { + "b": false + } + ] + }, + { + "name": "filter, and", + "selector": "$[?@.a>0&&@.a<10]", + "document": [ + { + "a": -10, + "d": "e" + }, + { + "a": 5, + "d": "f" + }, + { + "a": 20, + "d": "f" + } + ], + "result": [ + { + "a": 5, + "d": "f" + } + ] + }, + { + "name": "filter, or", + "selector": "$[?@.a=='b'||@.a=='d']", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "c", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "filter, not expression", + "selector": "$[?!(@.a=='b')]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "filter, not exists", + "selector": "$[?!@.a]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "d": "f" + } + ] + }, + { + "name": "filter, not exists, data null", + "selector": "$[?!@.a]", + "document": [ + { + "a": null, + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "d": "f" + } + ] + }, + { + "name": "filter, non-singular existence, wildcard", + "selector": "$[?@.*]", + "document": [ + 1, + [], + [ + 2 + ], + {}, + { + "a": 3 + } + ], + "result": [ + [ + 2 + ], + { + "a": 3 + } + ] + }, + { + "name": "filter, non-singular existence, multiple", + "selector": "$[?@[0, 0, 'a']]", + "document": [ + 1, + [], + [ + 2 + ], + [ + 2, + 3 + ], + { + "a": 3 + }, + { + "b": 4 + }, + { + "a": 3, + "b": 4 + } + ], + "result": [ + [ + 2 + ], + [ + 2, + 3 + ], + { + "a": 3 + }, + { + "a": 3, + "b": 4 + } + ] + }, + { + "name": "filter, non-singular existence, slice", + "selector": "$[?@[0:2]]", + "document": [ + 1, + [], + [ + 2 + ], + [ + 2, + 3, + 4 + ], + {}, + { + "a": 3 + } + ], + "result": [ + [ + 2 + ], + [ + 2, + 3, + 4 + ] + ] + }, + { + "name": "filter, non-singular existence, negated", + "selector": "$[?!@.*]", + "document": [ + 1, + [], + [ + 2 + ], + {}, + { + "a": 3 + } + ], + "result": [ + 1, + [], + {} + ] + }, + { + "name": "filter, non-singular query in comparison, slice", + "selector": "$[?@[0:0]==0]", + "invalid_selector": true + }, + { + "name": "filter, non-singular query in comparison, all children", + "selector": "$[?@[*]==0]", + "invalid_selector": true + }, + { + "name": "filter, non-singular query in comparison, descendants", + "selector": "$[?@..a==0]", + "invalid_selector": true + }, + { + "name": "filter, non-singular query in comparison, combined", + "selector": "$[?@.a[*].a==0]", + "invalid_selector": true + }, + { + "name": "filter, nested", + "selector": "$[?@[?@>1]]", + "document": [ + [ + 0 + ], + [ + 0, + 1 + ], + [ + 0, + 1, + 2 + ], + [ + 42 + ] + ], + "result": [ + [ + 0, + 1, + 2 + ], + [ + 42 + ] + ] + }, + { + "name": "filter, name segment on primitive, selects nothing", + "selector": "$[?@.a == 1]", + "document": { + "a": 1 + }, + "result": [] + }, + { + "name": "filter, name segment on array, selects nothing", + "selector": "$[?@['0'] == 5]", + "document": [ + [ + 5, + 6 + ] + ], + "result": [] + }, + { + "name": "filter, index segment on object, selects nothing", + "selector": "$[?@[0] == 5]", + "document": [ + { + "0": 5 + } + ], + "result": [] + }, + { + "name": "filter, relative non-singular query, index, equal", + "selector": "$[?(@[0, 0]==42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, index, not equal", + "selector": "$[?(@[0, 0]!=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, index, less-or-equal", + "selector": "$[?(@[0, 0]<=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, name, equal", + "selector": "$[?(@['a', 'a']==42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, name, not equal", + "selector": "$[?(@['a', 'a']!=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, name, less-or-equal", + "selector": "$[?(@['a', 'a']<=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, combined, equal", + "selector": "$[?(@[0, '0']==42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, combined, not equal", + "selector": "$[?(@[0, '0']!=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, combined, less-or-equal", + "selector": "$[?(@[0, '0']<=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, wildcard, equal", + "selector": "$[?(@.*==42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, wildcard, not equal", + "selector": "$[?(@.*!=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, wildcard, less-or-equal", + "selector": "$[?(@.*<=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, slice, equal", + "selector": "$[?(@[0:0]==42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, slice, not equal", + "selector": "$[?(@[0:0]!=42)]", + "invalid_selector": true + }, + { + "name": "filter, relative non-singular query, slice, less-or-equal", + "selector": "$[?(@[0:0]<=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, index, equal", + "selector": "$[?($[0, 0]==42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, index, not equal", + "selector": "$[?($[0, 0]!=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, index, less-or-equal", + "selector": "$[?($[0, 0]<=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, name, equal", + "selector": "$[?($['a', 'a']==42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, name, not equal", + "selector": "$[?($['a', 'a']!=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, name, less-or-equal", + "selector": "$[?($['a', 'a']<=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, combined, equal", + "selector": "$[?($[0, '0']==42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, combined, not equal", + "selector": "$[?($[0, '0']!=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, combined, less-or-equal", + "selector": "$[?($[0, '0']<=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, wildcard, equal", + "selector": "$[?($.*==42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, wildcard, not equal", + "selector": "$[?($.*!=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, wildcard, less-or-equal", + "selector": "$[?($.*<=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, slice, equal", + "selector": "$[?($[0:0]==42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, slice, not equal", + "selector": "$[?($[0:0]!=42)]", + "invalid_selector": true + }, + { + "name": "filter, absolute non-singular query, slice, less-or-equal", + "selector": "$[?($[0:0]<=42)]", + "invalid_selector": true + }, + { + "name": "filter, multiple selectors", + "selector": "$[?@.a,?@.b]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + }, + { + "name": "filter, multiple selectors, comparison", + "selector": "$[?@.a=='b',?@.b=='x']", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "filter, multiple selectors, overlapping", + "selector": "$[?@.a,?@.d]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + }, + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + }, + { + "name": "filter, multiple selectors, filter and index", + "selector": "$[?@.a,1]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + }, + { + "name": "filter, multiple selectors, filter and wildcard", + "selector": "$[?@.a,*]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + }, + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + }, + { + "name": "filter, multiple selectors, filter and slice", + "selector": "$[?@.a,1:]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + }, + { + "g": "h" + } + ], + "result": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + }, + { + "g": "h" + } + ] + }, + { + "name": "filter, multiple selectors, comparison filter, index and slice", + "selector": "$[1, ?@.a=='b', 1:]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "b": "c", + "d": "f" + }, + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ] + }, + { + "name": "filter, equals number, zero and negative zero", + "selector": "$[?@.a==-0]", + "document": [ + { + "a": 0, + "d": "e" + }, + { + "a": 0.1, + "d": "f" + }, + { + "a": "0", + "d": "g" + } + ], + "result": [ + { + "a": 0, + "d": "e" + } + ] + }, + { + "name": "filter, equals number, with and without decimal fraction", + "selector": "$[?@.a==1.0]", + "document": [ + { + "a": 1, + "d": "e" + }, + { + "a": 2, + "d": "f" + }, + { + "a": "1", + "d": "g" + } + ], + "result": [ + { + "a": 1, + "d": "e" + } + ] + }, + { + "name": "filter, equals number, exponent", + "selector": "$[?@.a==1e2]", + "document": [ + { + "a": 100, + "d": "e" + }, + { + "a": 100.1, + "d": "f" + }, + { + "a": "100", + "d": "g" + } + ], + "result": [ + { + "a": 100, + "d": "e" + } + ] + }, + { + "name": "filter, equals number, positive exponent", + "selector": "$[?@.a==1e+2]", + "document": [ + { + "a": 100, + "d": "e" + }, + { + "a": 100.1, + "d": "f" + }, + { + "a": "100", + "d": "g" + } + ], + "result": [ + { + "a": 100, + "d": "e" + } + ] + }, + { + "name": "filter, equals number, negative exponent", + "selector": "$[?@.a==1e-2]", + "document": [ + { + "a": 0.01, + "d": "e" + }, + { + "a": 0.02, + "d": "f" + }, + { + "a": "0.01", + "d": "g" + } + ], + "result": [ + { + "a": 0.01, + "d": "e" + } + ] + }, + { + "name": "filter, equals number, decimal fraction", + "selector": "$[?@.a==1.1]", + "document": [ + { + "a": 1.1, + "d": "e" + }, + { + "a": 1, + "d": "f" + }, + { + "a": "1.1", + "d": "g" + } + ], + "result": [ + { + "a": 1.1, + "d": "e" + } + ] + }, + { + "name": "filter, equals number, decimal fraction, no fractional digit", + "selector": "$[?@.a==1.]", + "invalid_selector": true + }, + { + "name": "filter, equals number, decimal fraction, exponent", + "selector": "$[?@.a==1.1e2]", + "document": [ + { + "a": 110, + "d": "e" + }, + { + "a": 110.1, + "d": "f" + }, + { + "a": "110", + "d": "g" + } + ], + "result": [ + { + "a": 110, + "d": "e" + } + ] + }, + { + "name": "filter, equals number, decimal fraction, positive exponent", + "selector": "$[?@.a==1.1e+2]", + "document": [ + { + "a": 110, + "d": "e" + }, + { + "a": 110.1, + "d": "f" + }, + { + "a": "110", + "d": "g" + } + ], + "result": [ + { + "a": 110, + "d": "e" + } + ] + }, + { + "name": "filter, equals number, decimal fraction, negative exponent", + "selector": "$[?@.a==1.1e-2]", + "document": [ + { + "a": 0.011, + "d": "e" + }, + { + "a": 0.012, + "d": "f" + }, + { + "a": "0.011", + "d": "g" + } + ], + "result": [ + { + "a": 0.011, + "d": "e" + } + ] + }, + { + "name": "filter, equals, special nothing", + "selector": "$.values[?length(@.a) == value($..c)]", + "document": { + "c": "cd", + "values": [ + { + "a": "ab" + }, + { + "c": "d" + }, + { + "a": null + } + ] + }, + "result": [ + { + "c": "d" + }, + { + "a": null + } + ] + }, + { + "name": "filter, equals, empty node list and empty node list", + "selector": "$[?@.a == @.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "c": 3 + } + ] + }, + { + "name": "filter, equals, empty node list and special nothing", + "selector": "$[?@.a == length(@.b)]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "b": 2 + }, + { + "c": 3 + } + ] + }, + { + "name": "filter, object data", + "selector": "$[?@<3]", + "document": { + "a": 1, + "b": 2, + "c": 3 + }, + "results": [ + [ + 1, + 2 + ], + [ + 2, + 1 + ] + ] + }, + { + "name": "filter, and binds more tightly than or", + "selector": "$[?@.a || @.b && @.c]", + "document": [ + { + "a": 1 + }, + { + "b": 2, + "c": 3 + }, + { + "c": 3 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + }, + { + "name": "filter, left to right evaluation", + "selector": "$[?@.a && @.b || @.c]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 1, + "c": 3 + }, + { + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ], + "result": [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 1, + "c": 3 + }, + { + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + }, + { + "name": "filter, group terms, left", + "selector": "$[?(@.a || @.b) && @.c]", + "document": [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 3 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ], + "result": [ + { + "a": 1, + "c": 3 + }, + { + "b": 2, + "c": 3 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + }, + { + "name": "filter, group terms, right", + "selector": "$[?@.a && (@.b || @.c)]", + "document": [ + { + "a": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 2 + }, + { + "b": 2 + }, + { + "c": 2 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ], + "result": [ + { + "a": 1, + "b": 2 + }, + { + "a": 1, + "c": 2 + }, + { + "a": 1, + "b": 2, + "c": 3 + } + ] + }, + { + "name": "filter, string literal, single quote in double quotes", + "selector": "$[?@ == \"quoted' literal\"]", + "document": [ + "quoted' literal", + "a", + "quoted\\' literal" + ], + "result": [ + "quoted' literal" + ] + }, + { + "name": "filter, string literal, double quote in single quotes", + "selector": "$[?@ == 'quoted\" literal']", + "document": [ + "quoted\" literal", + "a", + "quoted\\\" literal", + "'quoted\" literal'" + ], + "result": [ + "quoted\" literal" + ] + }, + { + "name": "filter, string literal, escaped single quote in single quotes", + "selector": "$[?@ == 'quoted\\' literal']", + "document": [ + "quoted' literal", + "a", + "quoted\\' literal", + "'quoted\" literal'" + ], + "result": [ + "quoted' literal" + ] + }, + { + "name": "filter, string literal, escaped double quote in double quotes", + "selector": "$[?@ == \"quoted\\\" literal\"]", + "document": [ + "quoted\" literal", + "a", + "quoted\\\" literal", + "'quoted\" literal'" + ], + "result": [ + "quoted\" literal" + ] + }, + { + "name": "filter, literal true must be compared", + "selector": "$[?true]", + "invalid_selector": true + }, + { + "name": "filter, literal false must be compared", + "selector": "$[?false]", + "invalid_selector": true + }, + { + "name": "filter, literal string must be compared", + "selector": "$[?'abc']", + "invalid_selector": true + }, + { + "name": "filter, literal int must be compared", + "selector": "$[?2]", + "invalid_selector": true + }, + { + "name": "filter, literal float must be compared", + "selector": "$[?2.2]", + "invalid_selector": true + }, + { + "name": "filter, literal null must be compared", + "selector": "$[?null]", + "invalid_selector": true + }, + { + "name": "filter, and, literals must be compared", + "selector": "$[?true && false]", + "invalid_selector": true + }, + { + "name": "filter, or, literals must be compared", + "selector": "$[?true || false]", + "invalid_selector": true + }, + { + "name": "filter, and, right hand literal must be compared", + "selector": "$[?true == false && false]", + "invalid_selector": true + }, + { + "name": "filter, or, right hand literal must be compared", + "selector": "$[?true == false || false]", + "invalid_selector": true + }, + { + "name": "filter, and, left hand literal must be compared", + "selector": "$[?false && true == false]", + "invalid_selector": true + }, + { + "name": "filter, or, left hand literal must be compared", + "selector": "$[?false || true == false]", + "invalid_selector": true + }, + { + "name": "index selector, first element", + "selector": "$[0]", + "document": [ + "first", + "second" + ], + "result": [ + "first" + ] + }, + { + "name": "index selector, second element", + "selector": "$[1]", + "document": [ + "first", + "second" + ], + "result": [ + "second" + ] + }, + { + "name": "index selector, out of bound", + "selector": "$[2]", + "document": [ + "first", + "second" + ], + "result": [] + }, + { + "name": "index selector, overflowing index", + "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872]", + "invalid_selector": true + }, + { + "name": "index selector, not actually an index, overflowing index leads into general text", + "selector": "$[231584178474632390847141970017375815706539969331281128078915168SomeRandomText]", + "invalid_selector": true + }, + { + "name": "index selector, negative", + "selector": "$[-1]", + "document": [ + "first", + "second" + ], + "result": [ + "second" + ] + }, + { + "name": "index selector, more negative", + "selector": "$[-2]", + "document": [ + "first", + "second" + ], + "result": [ + "first" + ] + }, + { + "name": "index selector, negative out of bound", + "selector": "$[-3]", + "document": [ + "first", + "second" + ], + "result": [] + }, + { + "name": "index selector, on object", + "selector": "$[0]", + "document": { + "foo": 1 + }, + "result": [] + }, + { + "name": "index selector, leading 0", + "selector": "$[01]", + "invalid_selector": true + }, + { + "name": "index selector, leading -0", + "selector": "$[-01]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes", + "selector": "$[\"a\"]", + "document": { + "a": "A", + "b": "B" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, absent data", + "selector": "$[\"c\"]", + "document": { + "a": "A", + "b": "B" + }, + "result": [] + }, + { + "name": "name selector, double quotes, array data", + "selector": "$[\"a\"]", + "document": [ + "first", + "second" + ], + "result": [] + }, + { + "name": "name selector, double quotes, embedded U+0000", + "selector": "$[\"\u0000\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0001", + "selector": "$[\"\u0001\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0002", + "selector": "$[\"\u0002\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0003", + "selector": "$[\"\u0003\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0004", + "selector": "$[\"\u0004\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0005", + "selector": "$[\"\u0005\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0006", + "selector": "$[\"\u0006\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0007", + "selector": "$[\"\u0007\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0008", + "selector": "$[\"\b\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0009", + "selector": "$[\"\t\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+000A", + "selector": "$[\"\n\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+000B", + "selector": "$[\"\u000b\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+000C", + "selector": "$[\"\f\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+000D", + "selector": "$[\"\r\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+000E", + "selector": "$[\"\u000e\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+000F", + "selector": "$[\"\u000f\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0010", + "selector": "$[\"\u0010\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0011", + "selector": "$[\"\u0011\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0012", + "selector": "$[\"\u0012\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0013", + "selector": "$[\"\u0013\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0014", + "selector": "$[\"\u0014\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0015", + "selector": "$[\"\u0015\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0016", + "selector": "$[\"\u0016\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0017", + "selector": "$[\"\u0017\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0018", + "selector": "$[\"\u0018\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0019", + "selector": "$[\"\u0019\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+001A", + "selector": "$[\"\u001a\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+001B", + "selector": "$[\"\u001b\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+001C", + "selector": "$[\"\u001c\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+001D", + "selector": "$[\"\u001d\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+001E", + "selector": "$[\"\u001e\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+001F", + "selector": "$[\"\u001f\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded U+0020", + "selector": "$[\" \"]", + "document": { + " ": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped double quote", + "selector": "$[\"\\\"\"]", + "document": { + "\"": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped reverse solidus", + "selector": "$[\"\\\\\"]", + "document": { + "\\": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped solidus", + "selector": "$[\"\\/\"]", + "document": { + "/": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped backspace", + "selector": "$[\"\\b\"]", + "document": { + "\b": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped form feed", + "selector": "$[\"\\f\"]", + "document": { + "\f": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped line feed", + "selector": "$[\"\\n\"]", + "document": { + "\n": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped carriage return", + "selector": "$[\"\\r\"]", + "document": { + "\r": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped tab", + "selector": "$[\"\\t\"]", + "document": { + "\t": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped ☺, upper case hex", + "selector": "$[\"\\u263A\"]", + "document": { + "☺": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, escaped ☺, lower case hex", + "selector": "$[\"\\u263a\"]", + "document": { + "☺": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, surrogate pair 𝄞", + "selector": "$[\"\\uD834\\uDD1E\"]", + "document": { + "𝄞": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, surrogate pair 😀", + "selector": "$[\"\\uD83D\\uDE00\"]", + "document": { + "😀": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, double quotes, invalid escaped single quote", + "selector": "$[\"\\'\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, embedded double quote", + "selector": "$[\"\"\"]", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, incomplete escape", + "selector": "$[\"\\\"]", + "invalid_selector": true + }, + { + "name": "name selector, single quotes", + "selector": "$['a']", + "document": { + "a": "A", + "b": "B" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, absent data", + "selector": "$['c']", + "document": { + "a": "A", + "b": "B" + }, + "result": [] + }, + { + "name": "name selector, single quotes, array data", + "selector": "$['a']", + "document": [ + "first", + "second" + ], + "result": [] + }, + { + "name": "name selector, single quotes, embedded U+0000", + "selector": "$['\u0000']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0001", + "selector": "$['\u0001']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0002", + "selector": "$['\u0002']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0003", + "selector": "$['\u0003']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0004", + "selector": "$['\u0004']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0005", + "selector": "$['\u0005']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0006", + "selector": "$['\u0006']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0007", + "selector": "$['\u0007']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0008", + "selector": "$['\b']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0009", + "selector": "$['\t']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+000A", + "selector": "$['\n']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+000B", + "selector": "$['\u000b']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+000C", + "selector": "$['\f']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+000D", + "selector": "$['\r']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+000E", + "selector": "$['\u000e']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+000F", + "selector": "$['\u000f']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0010", + "selector": "$['\u0010']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0011", + "selector": "$['\u0011']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0012", + "selector": "$['\u0012']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0013", + "selector": "$['\u0013']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0014", + "selector": "$['\u0014']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0015", + "selector": "$['\u0015']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0016", + "selector": "$['\u0016']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0017", + "selector": "$['\u0017']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0018", + "selector": "$['\u0018']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0019", + "selector": "$['\u0019']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+001A", + "selector": "$['\u001a']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+001B", + "selector": "$['\u001b']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+001C", + "selector": "$['\u001c']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+001D", + "selector": "$['\u001d']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+001E", + "selector": "$['\u001e']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+001F", + "selector": "$['\u001f']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded U+0020", + "selector": "$[' ']", + "document": { + " ": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped single quote", + "selector": "$['\\'']", + "document": { + "'": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped reverse solidus", + "selector": "$['\\\\']", + "document": { + "\\": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped solidus", + "selector": "$['\\/']", + "document": { + "/": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped backspace", + "selector": "$['\\b']", + "document": { + "\b": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped form feed", + "selector": "$['\\f']", + "document": { + "\f": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped line feed", + "selector": "$['\\n']", + "document": { + "\n": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped carriage return", + "selector": "$['\\r']", + "document": { + "\r": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped tab", + "selector": "$['\\t']", + "document": { + "\t": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped ☺, upper case hex", + "selector": "$['\\u263A']", + "document": { + "☺": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, escaped ☺, lower case hex", + "selector": "$['\\u263a']", + "document": { + "☺": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, surrogate pair 𝄞", + "selector": "$['\\uD834\\uDD1E']", + "document": { + "𝄞": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, surrogate pair 😀", + "selector": "$['\\uD83D\\uDE00']", + "document": { + "😀": "A" + }, + "result": [ + "A" + ] + }, + { + "name": "name selector, single quotes, invalid escaped double quote", + "selector": "$['\\\"']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, embedded single quote", + "selector": "$[''']", + "invalid_selector": true + }, + { + "name": "name selector, single quotes, incomplete escape", + "selector": "$['\\']", + "invalid_selector": true + }, + { + "name": "name selector, double quotes, empty", + "selector": "$[\"\"]", + "document": { + "a": "A", + "b": "B", + "": "C" + }, + "result": [ + "C" + ] + }, + { + "name": "name selector, single quotes, empty", + "selector": "$['']", + "document": { + "a": "A", + "b": "B", + "": "C" + }, + "result": [ + "C" + ] + }, + { + "name": "slice selector, slice selector", + "selector": "$[1:3]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 1, + 2 + ] + }, + { + "name": "slice selector, slice selector with step", + "selector": "$[1:6:2]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 1, + 3, + 5 + ] + }, + { + "name": "slice selector, slice selector with everything omitted, short form", + "selector": "$[:]", + "document": [ + 0, + 1, + 2, + 3 + ], + "result": [ + 0, + 1, + 2, + 3 + ] + }, + { + "name": "slice selector, slice selector with everything omitted, long form", + "selector": "$[::]", + "document": [ + 0, + 1, + 2, + 3 + ], + "result": [ + 0, + 1, + 2, + 3 + ] + }, + { + "name": "slice selector, slice selector with start omitted", + "selector": "$[:2]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 0, + 1 + ] + }, + { + "name": "slice selector, slice selector with start and end omitted", + "selector": "$[::2]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 0, + 2, + 4, + 6, + 8 + ] + }, + { + "name": "slice selector, negative step with default start and end", + "selector": "$[::-1]", + "document": [ + 0, + 1, + 2, + 3 + ], + "result": [ + 3, + 2, + 1, + 0 + ] + }, + { + "name": "slice selector, negative step with default start", + "selector": "$[:0:-1]", + "document": [ + 0, + 1, + 2, + 3 + ], + "result": [ + 3, + 2, + 1 + ] + }, + { + "name": "slice selector, negative step with default end", + "selector": "$[2::-1]", + "document": [ + 0, + 1, + 2, + 3 + ], + "result": [ + 2, + 1, + 0 + ] + }, + { + "name": "slice selector, larger negative step", + "selector": "$[::-2]", + "document": [ + 0, + 1, + 2, + 3 + ], + "result": [ + 3, + 1 + ] + }, + { + "name": "slice selector, negative range with default step", + "selector": "$[-1:-3]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [] + }, + { + "name": "slice selector, negative range with negative step", + "selector": "$[-1:-3:-1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 9, + 8 + ] + }, + { + "name": "slice selector, negative range with larger negative step", + "selector": "$[-1:-6:-2]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 9, + 7, + 5 + ] + }, + { + "name": "slice selector, larger negative range with larger negative step", + "selector": "$[-1:-7:-2]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 9, + 7, + 5 + ] + }, + { + "name": "slice selector, negative from, positive to", + "selector": "$[-5:7]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 5, + 6 + ] + }, + { + "name": "slice selector, negative from", + "selector": "$[-2:]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 8, + 9 + ] + }, + { + "name": "slice selector, positive from, negative to", + "selector": "$[1:-1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + { + "name": "slice selector, negative from, positive to, negative step", + "selector": "$[-1:1:-1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2 + ] + }, + { + "name": "slice selector, positive from, negative to, negative step", + "selector": "$[7:-5:-1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 7, + 6 + ] + }, + { + "name": "slice selector, too many colons", + "selector": "$[1:2:3:4]", + "invalid_selector": true + }, + { + "name": "slice selector, non-integer array index", + "selector": "$[1:2:a]", + "invalid_selector": true + }, + { + "name": "slice selector, zero step", + "selector": "$[1:2:0]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [] + }, + { + "name": "slice selector, empty range", + "selector": "$[2:2]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [] + }, + { + "name": "slice selector, slice selector with everything omitted with empty array", + "selector": "$[:]", + "document": [], + "result": [] + }, + { + "name": "slice selector, negative step with empty array", + "selector": "$[::-1]", + "document": [], + "result": [] + }, + { + "name": "slice selector, maximal range with positive step", + "selector": "$[0:10]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + }, + { + "name": "slice selector, maximal range with negative step", + "selector": "$[9:0:-1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 + ] + }, + { + "name": "slice selector, excessively large to value", + "selector": "$[2:113667776004]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ] + }, + { + "name": "slice selector, excessively small from value", + "selector": "$[-113667776004:1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 0 + ] + }, + { + "name": "slice selector, excessively large from value with negative step", + "selector": "$[113667776004:0:-1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 + ] + }, + { + "name": "slice selector, excessively small to value with negative step", + "selector": "$[3:-113667776004:-1]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 3, + 2, + 1, + 0 + ] + }, + { + "name": "slice selector, excessively large step", + "selector": "$[1:10:113667776004]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 1 + ] + }, + { + "name": "slice selector, excessively small step", + "selector": "$[-1:-10:-113667776004]", + "document": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "result": [ + 9 + ] + }, + { + "name": "slice selector, overflowing to value", + "selector": "$[2:231584178474632390847141970017375815706539969331281128078915168015826259279872]", + "invalid_selector": true + }, + { + "name": "slice selector, underflowing from value", + "selector": "$[-231584178474632390847141970017375815706539969331281128078915168015826259279872:1]", + "invalid_selector": true + }, + { + "name": "slice selector, overflowing from value with negative step", + "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872:0:-1]", + "invalid_selector": true + }, + { + "name": "slice selector, underflowing to value with negative step", + "selector": "$[3:-231584178474632390847141970017375815706539969331281128078915168015826259279872:-1]", + "invalid_selector": true + }, + { + "name": "slice selector, overflowing step", + "selector": "$[1:10:231584178474632390847141970017375815706539969331281128078915168015826259279872]", + "invalid_selector": true + }, + { + "name": "slice selector, underflowing step", + "selector": "$[-1:-10:-231584178474632390847141970017375815706539969331281128078915168015826259279872]", + "invalid_selector": true + }, + { + "name": "functions, count, count function", + "selector": "$[?count(@..*)>2]", + "document": [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ], + "d": "f" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ], + "d": "f" + } + ] + }, + { + "name": "functions, count, single-node arg", + "selector": "$[?count(@.a)>1]", + "document": [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ], + "d": "f" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [] + }, + { + "name": "functions, count, multiple-selector arg", + "selector": "$[?count(@['a','d'])>1]", + "document": [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ], + "d": "f" + }, + { + "a": 1, + "d": "f" + } + ], + "result": [ + { + "a": [ + 1 + ], + "d": "f" + }, + { + "a": 1, + "d": "f" + } + ] + }, + { + "name": "functions, count, non-query arg, number", + "selector": "$[?count(1)>2]", + "invalid_selector": true + }, + { + "name": "functions, count, non-query arg, string", + "selector": "$[?count('string')>2]", + "invalid_selector": true + }, + { + "name": "functions, count, non-query arg, true", + "selector": "$[?count(true)>2]", + "invalid_selector": true + }, + { + "name": "functions, count, non-query arg, false", + "selector": "$[?count(false)>2]", + "invalid_selector": true + }, + { + "name": "functions, count, non-query arg, null", + "selector": "$[?count(null)>2]", + "invalid_selector": true + }, + { + "name": "functions, count, result must be compared", + "selector": "$[?count(@..*)]", + "invalid_selector": true + }, + { + "name": "functions, count, no params", + "selector": "$[?count()==1]", + "invalid_selector": true + }, + { + "name": "functions, count, too many params", + "selector": "$[?count(@.a,@.b)==1]", + "invalid_selector": true + }, + { + "name": "functions, length, string data", + "selector": "$[?length(@.a)>=2]", + "document": [ + { + "a": "ab" + }, + { + "a": "d" + } + ], + "result": [ + { + "a": "ab" + } + ] + }, + { + "name": "functions, length, string data, unicode", + "selector": "$[?length(@)==2]", + "document": [ + "☺", + "☺☺", + "☺☺☺", + "ж", + "жж", + "жжж", + "磨", + "阿美", + "形声字" + ], + "result": [ + "☺☺", + "жж", + "阿美" + ] + }, + { + "name": "functions, length, array data", + "selector": "$[?length(@.a)>=2]", + "document": [ + { + "a": [ + 1, + 2, + 3 + ] + }, + { + "a": [ + 1 + ] + } + ], + "result": [ + { + "a": [ + 1, + 2, + 3 + ] + } + ] + }, + { + "name": "functions, length, missing data", + "selector": "$[?length(@.a)>=2]", + "document": [ + { + "d": "f" + } + ], + "result": [] + }, + { + "name": "functions, length, number arg", + "selector": "$[?length(1)>=2]", + "document": [ + { + "d": "f" + } + ], + "result": [] + }, + { + "name": "functions, length, true arg", + "selector": "$[?length(true)>=2]", + "document": [ + { + "d": "f" + } + ], + "result": [] + }, + { + "name": "functions, length, false arg", + "selector": "$[?length(false)>=2]", + "document": [ + { + "d": "f" + } + ], + "result": [] + }, + { + "name": "functions, length, null arg", + "selector": "$[?length(null)>=2]", + "document": [ + { + "d": "f" + } + ], + "result": [] + }, + { + "name": "functions, length, result must be compared", + "selector": "$[?length(@.a)]", + "invalid_selector": true + }, + { + "name": "functions, length, no params", + "selector": "$[?length()==1]", + "invalid_selector": true + }, + { + "name": "functions, length, too many params", + "selector": "$[?length(@.a,@.b)==1]", + "invalid_selector": true + }, + { + "name": "functions, length, non-singular query arg", + "selector": "$[?length(@.*)<3]", + "invalid_selector": true + }, + { + "name": "functions, length, arg is a function expression", + "selector": "$.values[?length(@.a)==length(value($..c))]", + "document": { + "c": "cd", + "values": [ + { + "a": "ab" + }, + { + "a": "d" + } + ] + }, + "result": [ + { + "a": "ab" + } + ] + }, + { + "name": "functions, length, arg is special nothing", + "selector": "$[?length(value(@.a))>0]", + "document": [ + { + "a": "ab" + }, + { + "c": "d" + }, + { + "a": null + } + ], + "result": [ + { + "a": "ab" + } + ] + }, + { + "name": "functions, match, found match", + "selector": "$[?match(@.a, 'a.*')]", + "document": [ + { + "a": "ab" + } + ], + "result": [ + { + "a": "ab" + } + ] + }, + { + "name": "functions, match, double quotes", + "selector": "$[?match(@.a, \"a.*\")]", + "document": [ + { + "a": "ab" + } + ], + "result": [ + { + "a": "ab" + } + ] + }, + { + "name": "functions, match, regex from the document", + "selector": "$.values[?match(@, $.regex)]", + "document": { + "regex": "b.?b", + "values": [ + "abc", + "bcd", + "bab", + "bba", + "bbab", + "b", + true, + [], + {} + ] + }, + "result": [ + "bab" + ] + }, + { + "name": "functions, match, don't select match", + "selector": "$[?!match(@.a, 'a.*')]", + "document": [ + { + "a": "ab" + } + ], + "result": [] + }, + { + "name": "functions, match, not a match", + "selector": "$[?match(@.a, 'a.*')]", + "document": [ + { + "a": "bc" + } + ], + "result": [] + }, + { + "name": "functions, match, select non-match", + "selector": "$[?!match(@.a, 'a.*')]", + "document": [ + { + "a": "bc" + } + ], + "result": [ + { + "a": "bc" + } + ] + }, + { + "name": "functions, match, non-string first arg", + "selector": "$[?match(1, 'a.*')]", + "document": [ + { + "a": "bc" + } + ], + "result": [] + }, + { + "name": "functions, match, non-string second arg", + "selector": "$[?match(@.a, 1)]", + "document": [ + { + "a": "bc" + } + ], + "result": [] + }, + { + "name": "functions, match, filter, match function, unicode char class, uppercase", + "selector": "$[?match(@, '\\\\p{Lu}')]", + "document": [ + "ж", + "Ж", + "1", + "жЖ", + true, + [], + {} + ], + "result": [ + "Ж" + ] + }, + { + "name": "functions, match, filter, match function, unicode char class negated, uppercase", + "selector": "$[?match(@, '\\\\P{Lu}')]", + "document": [ + "ж", + "Ж", + "1", + true, + [], + {} + ], + "result": [ + "ж", + "1" + ] + }, + { + "name": "functions, match, filter, match function, unicode, surrogate pair", + "selector": "$[?match(@, 'a.b')]", + "document": [ + "a𐄁b", + "ab", + "1", + true, + [], + {} + ], + "result": [ + "a𐄁b" + ] + }, + { + "name": "functions, match, dot matcher on \\u2028", + "selector": "$[?match(@, '.')]", + "document": [ + "\u2028", + "\r", + "\n", + true, + [], + {} + ], + "result": [ + "\u2028" + ] + }, + { + "name": "functions, match, dot matcher on \\u2029", + "selector": "$[?match(@, '.')]", + "document": [ + "\u2029", + "\r", + "\n", + true, + [], + {} + ], + "result": [ + "\u2029" + ] + }, + { + "name": "functions, match, result cannot be compared", + "selector": "$[?match(@.a, 'a.*')==true]", + "invalid_selector": true + }, + { + "name": "functions, match, too few params", + "selector": "$[?match(@.a)==1]", + "invalid_selector": true + }, + { + "name": "functions, match, too many params", + "selector": "$[?match(@.a,@.b,@.c)==1]", + "invalid_selector": true + }, + { + "name": "functions, match, arg is a function expression", + "selector": "$.values[?match(@.a, value($..['regex']))]", + "document": { + "regex": "a.*", + "values": [ + { + "a": "ab" + }, + { + "a": "ba" + } + ] + }, + "result": [ + { + "a": "ab" + } + ] + }, + { + "name": "functions, match, dot in character class", + "selector": "$[?match(@, 'a[.b]c')]", + "document": [ + "abc", + "a.c", + "axc" + ], + "result": [ + "abc", + "a.c" + ] + }, + { + "name": "functions, match, escaped dot", + "selector": "$[?match(@, 'a\\\\.c')]", + "document": [ + "abc", + "a.c", + "axc" + ], + "result": [ + "a.c" + ] + }, + { + "name": "functions, match, escaped backslash before dot", + "selector": "$[?match(@, 'a\\\\\\\\.c')]", + "document": [ + "abc", + "a.c", + "axc", + "a\\\u2028c" + ], + "result": [ + "a\\\u2028c" + ] + }, + { + "name": "functions, match, escaped left square bracket", + "selector": "$[?match(@, 'a\\\\[.c')]", + "document": [ + "abc", + "a.c", + "a[\u2028c" + ], + "result": [ + "a[\u2028c" + ] + }, + { + "name": "functions, match, escaped right square bracket", + "selector": "$[?match(@, 'a[\\\\].]c')]", + "document": [ + "abc", + "a.c", + "a\u2028c", + "a]c" + ], + "result": [ + "a.c", + "a]c" + ] + }, + { + "name": "functions, match, explicit caret", + "selector": "$[?match(@, '^ab.*')]", + "document": [ + "abc", + "axc", + "ab", + "xab" + ], + "result": [ + "abc", + "ab" + ] + }, + { + "name": "functions, match, explicit dollar", + "selector": "$[?match(@, '.*bc$')]", + "document": [ + "abc", + "axc", + "ab", + "abcx" + ], + "result": [ + "abc" + ] + }, + { + "name": "functions, search, at the end", + "selector": "$[?search(@.a, 'a.*')]", + "document": [ + { + "a": "the end is ab" + } + ], + "result": [ + { + "a": "the end is ab" + } + ] + }, + { + "name": "functions, search, double quotes", + "selector": "$[?search(@.a, \"a.*\")]", + "document": [ + { + "a": "the end is ab" + } + ], + "result": [ + { + "a": "the end is ab" + } + ] + }, + { + "name": "functions, search, at the start", + "selector": "$[?search(@.a, 'a.*')]", + "document": [ + { + "a": "ab is at the start" + } + ], + "result": [ + { + "a": "ab is at the start" + } + ] + }, + { + "name": "functions, search, in the middle", + "selector": "$[?search(@.a, 'a.*')]", + "document": [ + { + "a": "contains two matches" + } + ], + "result": [ + { + "a": "contains two matches" + } + ] + }, + { + "name": "functions, search, regex from the document", + "selector": "$.values[?search(@, $.regex)]", + "document": { + "regex": "b.?b", + "values": [ + "abc", + "bcd", + "bab", + "bba", + "bbab", + "b", + true, + [], + {} + ] + }, + "result": [ + "bab", + "bba", + "bbab" + ] + }, + { + "name": "functions, search, don't select match", + "selector": "$[?!search(@.a, 'a.*')]", + "document": [ + { + "a": "contains two matches" + } + ], + "result": [] + }, + { + "name": "functions, search, not a match", + "selector": "$[?search(@.a, 'a.*')]", + "document": [ + { + "a": "bc" + } + ], + "result": [] + }, + { + "name": "functions, search, select non-match", + "selector": "$[?!search(@.a, 'a.*')]", + "document": [ + { + "a": "bc" + } + ], + "result": [ + { + "a": "bc" + } + ] + }, + { + "name": "functions, search, non-string first arg", + "selector": "$[?search(1, 'a.*')]", + "document": [ + { + "a": "bc" + } + ], + "result": [] + }, + { + "name": "functions, search, non-string second arg", + "selector": "$[?search(@.a, 1)]", + "document": [ + { + "a": "bc" + } + ], + "result": [] + }, + { + "name": "functions, search, filter, search function, unicode char class, uppercase", + "selector": "$[?search(@, '\\\\p{Lu}')]", + "document": [ + "ж", + "Ж", + "1", + "жЖ", + true, + [], + {} + ], + "result": [ + "Ж", + "жЖ" + ] + }, + { + "name": "functions, search, filter, search function, unicode char class negated, uppercase", + "selector": "$[?search(@, '\\\\P{Lu}')]", + "document": [ + "ж", + "Ж", + "1", + true, + [], + {} + ], + "result": [ + "ж", + "1" + ] + }, + { + "name": "functions, search, filter, search function, unicode, surrogate pair", + "selector": "$[?search(@, 'a.b')]", + "document": [ + "a𐄁bc", + "abc", + "1", + true, + [], + {} + ], + "result": [ + "a𐄁bc" + ] + }, + { + "name": "functions, search, dot matcher on \\u2028", + "selector": "$[?search(@, '.')]", + "document": [ + "\u2028", + "\r\u2028\n", + "\r", + "\n", + true, + [], + {} + ], + "result": [ + "\u2028", + "\r\u2028\n" + ] + }, + { + "name": "functions, search, dot matcher on \\u2029", + "selector": "$[?search(@, '.')]", + "document": [ + "\u2029", + "\r\u2029\n", + "\r", + "\n", + true, + [], + {} + ], + "result": [ + "\u2029", + "\r\u2029\n" + ] + }, + { + "name": "functions, search, result cannot be compared", + "selector": "$[?search(@.a, 'a.*')==true]", + "invalid_selector": true + }, + { + "name": "functions, search, too few params", + "selector": "$[?search(@.a)]", + "invalid_selector": true + }, + { + "name": "functions, search, too many params", + "selector": "$[?search(@.a,@.b,@.c)]", + "invalid_selector": true + }, + { + "name": "functions, search, arg is a function expression", + "selector": "$.values[?search(@, value($..['regex']))]", + "document": { + "regex": "b.?b", + "values": [ + "abc", + "bcd", + "bab", + "bba", + "bbab", + "b", + true, + [], + {} + ] + }, + "result": [ + "bab", + "bba", + "bbab" + ] + }, + { + "name": "functions, search, dot in character class", + "selector": "$[?search(@, 'a[.b]c')]", + "document": [ + "x abc y", + "x a.c y", + "x axc y" + ], + "result": [ + "x abc y", + "x a.c y" + ] + }, + { + "name": "functions, search, escaped dot", + "selector": "$[?search(@, 'a\\\\.c')]", + "document": [ + "x abc y", + "x a.c y", + "x axc y" + ], + "result": [ + "x a.c y" + ] + }, + { + "name": "functions, search, escaped backslash before dot", + "selector": "$[?search(@, 'a\\\\\\\\.c')]", + "document": [ + "x abc y", + "x a.c y", + "x axc y", + "x a\\\u2028c y" + ], + "result": [ + "x a\\\u2028c y" + ] + }, + { + "name": "functions, search, escaped left square bracket", + "selector": "$[?search(@, 'a\\\\[.c')]", + "document": [ + "x abc y", + "x a.c y", + "x a[\u2028c y" + ], + "result": [ + "x a[\u2028c y" + ] + }, + { + "name": "functions, search, escaped right square bracket", + "selector": "$[?search(@, 'a[\\\\].]c')]", + "document": [ + "x abc y", + "x a.c y", + "x a\u2028c y", + "x a]c y" + ], + "result": [ + "x a.c y", + "x a]c y" + ] + }, + { + "name": "functions, value, single-value nodelist", + "selector": "$[?value(@.*)==4]", + "document": [ + [ + 4 + ], + { + "foo": 4 + }, + [ + 5 + ], + { + "foo": 5 + }, + 4 + ], + "result": [ + [ + 4 + ], + { + "foo": 4 + } + ] + }, + { + "name": "functions, value, multi-value nodelist", + "selector": "$[?value(@.*)==4]", + "document": [ + [ + 4, + 4 + ], + { + "foo": 4, + "bar": 4 + } + ], + "result": [] + }, + { + "name": "functions, value, too few params", + "selector": "$[?value()==4]", + "invalid_selector": true + }, + { + "name": "functions, value, too many params", + "selector": "$[?value(@.a,@.b)==4]", + "invalid_selector": true + }, + { + "name": "functions, value, result must be compared", + "selector": "$[?value(@.a)]", + "invalid_selector": true + }, + { + "name": "whitespace, filter, space between question mark and expression", + "selector": "$[? @.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, newline between question mark and expression", + "selector": "$[?\n@.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, tab between question mark and expression", + "selector": "$[?\t@.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, return between question mark and expression", + "selector": "$[?\r@.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, space between question mark and parenthesized expression", + "selector": "$[? (@.a)]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, newline between question mark and parenthesized expression", + "selector": "$[?\n(@.a)]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, tab between question mark and parenthesized expression", + "selector": "$[?\t(@.a)]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, return between question mark and parenthesized expression", + "selector": "$[?\r(@.a)]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, space between parenthesized expression and bracket", + "selector": "$[?(@.a) ]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, newline between parenthesized expression and bracket", + "selector": "$[?(@.a)\n]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, tab between parenthesized expression and bracket", + "selector": "$[?(@.a)\t]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, return between parenthesized expression and bracket", + "selector": "$[?(@.a)\r]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, space between bracket and question mark", + "selector": "$[ ?@.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, newline between bracket and question mark", + "selector": "$[\n?@.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, tab between bracket and question mark", + "selector": "$[\t?@.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, filter, return between bracket and question mark", + "selector": "$[\r?@.a]", + "document": [ + { + "a": "b", + "d": "e" + }, + { + "b": "c", + "d": "f" + } + ], + "result": [ + { + "a": "b", + "d": "e" + } + ] + }, + { + "name": "whitespace, functions, space between function name and parenthesis", + "selector": "$[?count (@.*)==1]", + "invalid_selector": true + }, + { + "name": "whitespace, functions, newline between function name and parenthesis", + "selector": "$[?count\n(@.*)==1]", + "invalid_selector": true + }, + { + "name": "whitespace, functions, tab between function name and parenthesis", + "selector": "$[?count\t(@.*)==1]", + "invalid_selector": true + }, + { + "name": "whitespace, functions, return between function name and parenthesis", + "selector": "$[?count\r(@.*)==1]", + "invalid_selector": true + }, + { + "name": "whitespace, functions, space between parenthesis and arg", + "selector": "$[?count( @.*)==1]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, functions, newline between parenthesis and arg", + "selector": "$[?count(\n@.*)==1]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, functions, tab between parenthesis and arg", + "selector": "$[?count(\t@.*)==1]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, functions, return between parenthesis and arg", + "selector": "$[?count(\r@.*)==1]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, functions, space between arg and comma", + "selector": "$[?search(@ ,'[a-z]+')]", + "document": [ + "foo", + "123" + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, newline between arg and comma", + "selector": "$[?search(@\n,'[a-z]+')]", + "document": [ + "foo", + "123" + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, tab between arg and comma", + "selector": "$[?search(@\t,'[a-z]+')]", + "document": [ + "foo", + "123" + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, return between arg and comma", + "selector": "$[?search(@\r,'[a-z]+')]", + "document": [ + "foo", + "123" + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, space between comma and arg", + "selector": "$[?search(@, '[a-z]+')]", + "document": [ + "foo", + "123" + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, newline between comma and arg", + "selector": "$[?search(@,\n'[a-z]+')]", + "document": [ + "foo", + "123" + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, tab between comma and arg", + "selector": "$[?search(@,\t'[a-z]+')]", + "document": [ + "foo", + "123" + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, return between comma and arg", + "selector": "$[?search(@,\r'[a-z]+')]", + "document": [ + "foo", + "123" + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, space between arg and parenthesis", + "selector": "$[?count(@.* )==1]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, functions, newline between arg and parenthesis", + "selector": "$[?count(@.*\n)==1]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, functions, tab between arg and parenthesis", + "selector": "$[?count(@.*\t)==1]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, functions, return between arg and parenthesis", + "selector": "$[?count(@.*\r)==1]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, functions, spaces in a relative singular selector", + "selector": "$[?length(@ .a .b) == 3]", + "document": [ + { + "a": { + "b": "foo" + } + }, + {} + ], + "result": [ + { + "a": { + "b": "foo" + } + } + ] + }, + { + "name": "whitespace, functions, newlines in a relative singular selector", + "selector": "$[?length(@\n.a\n.b) == 3]", + "document": [ + { + "a": { + "b": "foo" + } + }, + {} + ], + "result": [ + { + "a": { + "b": "foo" + } + } + ] + }, + { + "name": "whitespace, functions, tabs in a relative singular selector", + "selector": "$[?length(@\t.a\t.b) == 3]", + "document": [ + { + "a": { + "b": "foo" + } + }, + {} + ], + "result": [ + { + "a": { + "b": "foo" + } + } + ] + }, + { + "name": "whitespace, functions, returns in a relative singular selector", + "selector": "$[?length(@\r.a\r.b) == 3]", + "document": [ + { + "a": { + "b": "foo" + } + }, + {} + ], + "result": [ + { + "a": { + "b": "foo" + } + } + ] + }, + { + "name": "whitespace, functions, spaces in an absolute singular selector", + "selector": "$..[?length(@)==length($ [0] .a)]", + "document": [ + { + "a": "foo" + }, + {} + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, newlines in an absolute singular selector", + "selector": "$..[?length(@)==length($\n[0]\n.a)]", + "document": [ + { + "a": "foo" + }, + {} + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, tabs in an absolute singular selector", + "selector": "$..[?length(@)==length($\t[0]\t.a)]", + "document": [ + { + "a": "foo" + }, + {} + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, functions, returns in an absolute singular selector", + "selector": "$..[?length(@)==length($\r[0]\r.a)]", + "document": [ + { + "a": "foo" + }, + {} + ], + "result": [ + "foo" + ] + }, + { + "name": "whitespace, operators, space before ||", + "selector": "$[?@.a ||@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline before ||", + "selector": "$[?@.a\n||@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab before ||", + "selector": "$[?@.a\t||@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return before ||", + "selector": "$[?@.a\r||@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space after ||", + "selector": "$[?@.a|| @.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline after ||", + "selector": "$[?@.a||\n@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab after ||", + "selector": "$[?@.a||\t@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return after ||", + "selector": "$[?@.a||\r@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } + ], + "result": [ + { + "a": 1 + }, + { + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space before &&", + "selector": "$[?@.a &&@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline before &&", + "selector": "$[?@.a\n&&@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab before &&", + "selector": "$[?@.a\t&&@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return before &&", + "selector": "$[?@.a\r&&@.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space after &&", + "selector": "$[?@.a&& @.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline after &&", + "selector": "$[?@.a&& @.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab after &&", + "selector": "$[?@.a&& @.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return after &&", + "selector": "$[?@.a&& @.b]", + "document": [ + { + "a": 1 + }, + { + "b": 2 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space before ==", + "selector": "$[?@.a ==@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 1 + } + ] + }, + { + "name": "whitespace, operators, newline before ==", + "selector": "$[?@.a\n==@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 1 + } + ] + }, + { + "name": "whitespace, operators, tab before ==", + "selector": "$[?@.a\t==@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 1 + } + ] + }, + { + "name": "whitespace, operators, return before ==", + "selector": "$[?@.a\r==@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 1 + } + ] + }, + { + "name": "whitespace, operators, space after ==", + "selector": "$[?@.a== @.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 1 + } + ] + }, + { + "name": "whitespace, operators, newline after ==", + "selector": "$[?@.a==\n@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 1 + } + ] + }, + { + "name": "whitespace, operators, tab after ==", + "selector": "$[?@.a==\t@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 1 + } + ] + }, + { + "name": "whitespace, operators, return after ==", + "selector": "$[?@.a==\r@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 1 + } + ] + }, + { + "name": "whitespace, operators, space before !=", + "selector": "$[?@.a !=@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline before !=", + "selector": "$[?@.a\n!=@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab before !=", + "selector": "$[?@.a\t!=@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return before !=", + "selector": "$[?@.a\r!=@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space after !=", + "selector": "$[?@.a!= @.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline after !=", + "selector": "$[?@.a!=\n@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab after !=", + "selector": "$[?@.a!=\t@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return after !=", + "selector": "$[?@.a!=\r@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space before <", + "selector": "$[?@.a <@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline before <", + "selector": "$[?@.a\n<@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab before <", + "selector": "$[?@.a\t<@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return before <", + "selector": "$[?@.a\r<@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space after <", + "selector": "$[?@.a< @.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline after <", + "selector": "$[?@.a<\n@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab after <", + "selector": "$[?@.a<\t@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return after <", + "selector": "$[?@.a<\r@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space before >", + "selector": "$[?@.b >@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline before >", + "selector": "$[?@.b\n>@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab before >", + "selector": "$[?@.b\t>@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return before >", + "selector": "$[?@.b\r>@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space after >", + "selector": "$[?@.b> @.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline after >", + "selector": "$[?@.b>\n@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab after >", + "selector": "$[?@.b>\t@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return after >", + "selector": "$[?@.b>\r@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ], + "result": [ + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space before <=", + "selector": "$[?@.a <=@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline before <=", + "selector": "$[?@.a\n<=@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab before <=", + "selector": "$[?@.a\t<=@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return before <=", + "selector": "$[?@.a\r<=@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space after <=", + "selector": "$[?@.a<= @.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline after <=", + "selector": "$[?@.a<=\n@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab after <=", + "selector": "$[?@.a<=\t@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return after <=", + "selector": "$[?@.a<=\r@.b]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space before >=", + "selector": "$[?@.b >=@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline before >=", + "selector": "$[?@.b\n>=@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab before >=", + "selector": "$[?@.b\t>=@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return before >=", + "selector": "$[?@.b\r>=@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space after >=", + "selector": "$[?@.b>= @.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, newline after >=", + "selector": "$[?@.b>=\n@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, tab after >=", + "selector": "$[?@.b>=\t@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, return after >=", + "selector": "$[?@.b>=\r@.a]", + "document": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + }, + { + "a": 2, + "b": 1 + } + ], + "result": [ + { + "a": 1, + "b": 1 + }, + { + "a": 1, + "b": 2 + } + ] + }, + { + "name": "whitespace, operators, space between logical not and test expression", + "selector": "$[?! @.a]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "d": "f" + } + ] + }, + { + "name": "whitespace, operators, newline between logical not and test expression", + "selector": "$[?!\n@.a]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "d": "f" + } + ] + }, + { + "name": "whitespace, operators, tab between logical not and test expression", + "selector": "$[?!\t@.a]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "d": "f" + } + ] + }, + { + "name": "whitespace, operators, return between logical not and test expression", + "selector": "$[?!\r@.a]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "d": "f" + } + ] + }, + { + "name": "whitespace, operators, space between logical not and parenthesized expression", + "selector": "$[?! (@.a=='b')]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "whitespace, operators, newline between logical not and parenthesized expression", + "selector": "$[?!\n(@.a=='b')]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "whitespace, operators, tab between logical not and parenthesized expression", + "selector": "$[?!\t(@.a=='b')]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "whitespace, operators, return between logical not and parenthesized expression", + "selector": "$[?!\r(@.a=='b')]", + "document": [ + { + "a": "a", + "d": "e" + }, + { + "a": "b", + "d": "f" + }, + { + "a": "d", + "d": "f" + } + ], + "result": [ + { + "a": "a", + "d": "e" + }, + { + "a": "d", + "d": "f" + } + ] + }, + { + "name": "whitespace, selectors, space between root and bracket", + "selector": "$ ['a']", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, newline between root and bracket", + "selector": "$\n['a']", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, tab between root and bracket", + "selector": "$\t['a']", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, return between root and bracket", + "selector": "$\r['a']", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, space between bracket and bracket", + "selector": "$['a'] ['b']", + "document": { + "a": { + "b": "ab" + } + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, newline between root and bracket", + "selector": "$['a'] \n['b']", + "document": { + "a": { + "b": "ab" + } + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, tab between root and bracket", + "selector": "$['a'] \t['b']", + "document": { + "a": { + "b": "ab" + } + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, return between root and bracket", + "selector": "$['a'] \r['b']", + "document": { + "a": { + "b": "ab" + } + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, space between root and dot", + "selector": "$ .a", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, newline between root and dot", + "selector": "$\n.a", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, tab between root and dot", + "selector": "$\t.a", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, return between root and dot", + "selector": "$\r.a", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, space between dot and name", + "selector": "$. a", + "invalid_selector": true + }, + { + "name": "whitespace, selectors, newline between dot and name", + "selector": "$.\na", + "invalid_selector": true + }, + { + "name": "whitespace, selectors, tab between dot and name", + "selector": "$.\ta", + "invalid_selector": true + }, + { + "name": "whitespace, selectors, return between dot and name", + "selector": "$.\ra", + "invalid_selector": true + }, + { + "name": "whitespace, selectors, space between recursive descent and name", + "selector": "$.. a", + "invalid_selector": true + }, + { + "name": "whitespace, selectors, newline between recursive descent and name", + "selector": "$..\na", + "invalid_selector": true + }, + { + "name": "whitespace, selectors, tab between recursive descent and name", + "selector": "$..\ta", + "invalid_selector": true + }, + { + "name": "whitespace, selectors, return between recursive descent and name", + "selector": "$..\ra", + "invalid_selector": true + }, + { + "name": "whitespace, selectors, space between bracket and selector", + "selector": "$[ 'a']", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, newline between bracket and selector", + "selector": "$[\n'a']", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, tab between bracket and selector", + "selector": "$[\t'a']", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, return between bracket and selector", + "selector": "$[\r'a']", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, space between selector and bracket", + "selector": "$['a' ]", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, newline between selector and bracket", + "selector": "$['a'\n]", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, tab between selector and bracket", + "selector": "$['a'\t]", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, return between selector and bracket", + "selector": "$['a'\r]", + "document": { + "a": "ab" + }, + "result": [ + "ab" + ] + }, + { + "name": "whitespace, selectors, space between selector and comma", + "selector": "$['a' ,'b']", + "document": { + "a": "ab", + "b": "bc" + }, + "result": [ + "ab", + "bc" + ] + }, + { + "name": "whitespace, selectors, newline between selector and comma", + "selector": "$['a'\n,'b']", + "document": { + "a": "ab", + "b": "bc" + }, + "result": [ + "ab", + "bc" + ] + }, + { + "name": "whitespace, selectors, tab between selector and comma", + "selector": "$['a'\t,'b']", + "document": { + "a": "ab", + "b": "bc" + }, + "result": [ + "ab", + "bc" + ] + }, + { + "name": "whitespace, selectors, return between selector and comma", + "selector": "$['a'\r,'b']", + "document": { + "a": "ab", + "b": "bc" + }, + "result": [ + "ab", + "bc" + ] + }, + { + "name": "whitespace, selectors, space between comma and selector", + "selector": "$['a', 'b']", + "document": { + "a": "ab", + "b": "bc" + }, + "result": [ + "ab", + "bc" + ] + }, + { + "name": "whitespace, selectors, newline between comma and selector", + "selector": "$['a',\n'b']", + "document": { + "a": "ab", + "b": "bc" + }, + "result": [ + "ab", + "bc" + ] + }, + { + "name": "whitespace, selectors, tab between comma and selector", + "selector": "$['a',\t'b']", + "document": { + "a": "ab", + "b": "bc" + }, + "result": [ + "ab", + "bc" + ] + }, + { + "name": "whitespace, selectors, return between comma and selector", + "selector": "$['a',\r'b']", + "document": { + "a": "ab", + "b": "bc" + }, + "result": [ + "ab", + "bc" + ] + }, + { + "name": "whitespace, slice, space between start and colon", + "selector": "$[1 :5:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, newline between start and colon", + "selector": "$[1\n:5:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, tab between start and colon", + "selector": "$[1\t:5:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, return between start and colon", + "selector": "$[1\r:5:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, space between colon and end", + "selector": "$[1: 5:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, newline between colon and end", + "selector": "$[1:\n5:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, tab between colon and end", + "selector": "$[1:\t5:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, return between colon and end", + "selector": "$[1:\r5:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, space between end and colon", + "selector": "$[1:5 :2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, newline between end and colon", + "selector": "$[1:5\n:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, tab between end and colon", + "selector": "$[1:5\t:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, return between end and colon", + "selector": "$[1:5\r:2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, space between colon and step", + "selector": "$[1:5: 2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, newline between colon and step", + "selector": "$[1:5:\n2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, tab between colon and step", + "selector": "$[1:5:\t2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + }, + { + "name": "whitespace, slice, return between colon and step", + "selector": "$[1:5:\r2]", + "document": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "result": [ + 2, + 4 + ] + } + ] +} diff --git a/test/Hyperbee.Json.Cts/generate_tests.ps1 b/test/Hyperbee.Json.Cts/generate_tests.ps1 new file mode 100644 index 00000000..9db4fdff --- /dev/null +++ b/test/Hyperbee.Json.Cts/generate_tests.ps1 @@ -0,0 +1,296 @@ +function Invoke-WebRequestWithRetry { + param ( + [Parameter(Mandatory=$true)] + [string]$Url, + [int]$MaxRetries = 5, + [int]$RetryDelay = 3 # seconds + ) + + Write-Host "Downloading $Url" + + $attempt = 0 + while ($attempt -lt $MaxRetries) { + try { + $response = Invoke-WebRequest -Uri $Url + return $response + } + catch { + $attempt++ + Write-Host "Attempt $attempt failed: $_" + if ($attempt -ge $MaxRetries) { + throw "Failed to retrieve the content after $MaxRetries attempts. Error: $_" + } + Start-Sleep -Seconds $RetryDelay + } + } + + Write-Host "Download complete." +} + +function Get-JsonContent { + param ( + [Parameter(Mandatory=$true)] + [string]$Url, + [string]$LocalPath + ) + + if (Test-Path -Path $LocalPath) { + # Read from file location + $jsonContent = Get-Content -Path $ctsPath -Raw + Write-Host "JSON content read from '$LocalPath'." + } else { + # Fetch the JSON content as a string + $response = Invoke-WebRequestWithRetry -Url $Url + $jsonContent = $response.Content + + # Save the JSON content to a file in a pretty formatted way if SavePath is provided + if ($PSBoundParameters.ContainsKey('LocalPath')) { + $prettyJson = $jsonContent | ConvertFrom-Json -AsHashtable | ConvertTo-Json -Depth 10 + Set-Content -Path $LocalPath -Value $prettyJson + Write-Host "JSON content saved to '$LocalPath'." + } + } + + # Convert the raw JSON string to a PowerShell hashtable to access properties + $jsonObject = $jsonContent | ConvertFrom-Json -AsHashtable + + # Use regex to extract all selector properties + $pattern = '"selector"\s*:\s*"(.*?[^\\])"' + $match = [regex]::Matches($jsonContent, $pattern) + + # Iterate through all tests and collect the properties + $output = @() + for ($i = 0; $i -lt $jsonObject.tests.Count; $i++) { + $test = $jsonObject.tests[$i] + + # Split the name into category (group) and name parts + $fullName = $test['name'] + $splitName = $fullName -split ',', 2 + $group = $splitName[0].Trim() + $name = if ($splitName.Length -gt 1) { $splitName[1].Trim() } else { $null } + + # Ignore empty groups + if ([string]::IsNullOrWhiteSpace($group)) { + continue + } + + # Convert JSON to strings BEFORE adding to PSObject to prevent unwanted conversions + $document = ConvertTo-Json -InputObject $test['document'] -Depth 10 + $result = if ($test.ContainsKey('result')) { ConvertTo-Json -InputObject $test['result'] -Depth 10 } else { $null } + $results = if ($test.ContainsKey('results')) { ConvertTo-Json -InputObject $test['results'] -Depth 10 } else { $null } + $invalid_selector = if ($test.ContainsKey('invalid_selector')) { $test['invalid_selector'] } else { $null } + + $rawJsonSelector = $match[$i].Groups[1].Value + + $item = [PSCustomObject]@{ + name = $name + group = $group + document = $document + result = $result + results = $results + selector = $rawJsonSelector + invalid_selector = $invalid_selector + } + + $output += $item + } + + return $output +} + +# Helper function to convert test names to C# method names +function Convert-ToCSharpMethodName { + param ( + [string]$name + ) + return $name -replace '[^a-zA-Z0-9]', '_' +} + +function FormatJson { + param ( + [string]$json, + [int]$indent + ) + + # Ignore empty groups + if ([string]::IsNullOrWhiteSpace($json)) { + return $null + } + + # Detect the line break format + $lineBreak = if ($json -contains "`r`n") { "`r`n" } else { "`n" } + + # Split the JSON string into lines + $lines = $json -split $lineBreak + + # Create the indentation string + $indentation = " " * $indent + + # Add indentation to each line except the first + $formattedLines = $lines | ForEach-Object { $indentation + $_ } + + # Join the lines back into a single string with the detected line break format + $formattedJson = $lineBreak + ($formattedLines -join $lineBreak) + $lineBreak + $indentation + + return $formattedJson +} + +function Convert-ToPascalCase { + param ( + [string]$value + ) + + if ([string]::IsNullOrWhiteSpace($value)) { + return $value + } + + $words = $value -split '\s+' # Split the string by whitespace + $pascalCaseWords = $words | ForEach-Object { + if ([string]::IsNullOrEmpty($_)) { + continue + } + + $firstLetter = $_[0].ToString().ToUpper() + $restOfString = $_.Substring(1).ToLower() + $firstLetter + $restOfString + } + + return $pascalCaseWords -join '' +} + +function Get-UnitTestContent { + param ( + [Parameter(Mandatory=$true)] + [array]$JsonTests, + [Parameter(Mandatory=$true)] + [string]$group + ) + + # Give the class a unique name + $uniquePart = Convert-ToPascalCase -value $group + $className = "Cts$($uniquePart)Test" + + # Prepare the content for the C# unit test file + $unitTestContent = @" +// This file was auto generated. + +using System.Text.Json.Nodes; +using Hyperbee.Json.Extensions; + +namespace Hyperbee.Json.Cts.Tests +{ + [TestClass] + public class $className + {`r`n +"@ + + $testNumber = 0 + + # Loop through each test case in the JSON and generate a TestMethod + foreach ($test in $JsonTests) { + $name = $test.name + $methodName = Convert-ToCSharpMethodName $name # Convert $test.name to C# method name + + if ($null -eq $name -or $name -eq "") { + continue + } + + $testNumber++ + $selector = $test.selector + + if ($selector.EndsWith('\')) { + $selector += '\' + } + + $invalidSelector = if ($test.invalid_selector) { $true } else { $false } + + $document = FormatJson -json $test.document -indent 16 + $result = FormatJson -json $test.result -indent 16 + $results = FormatJson -json $test.results -indent 16 + + # Replace placeholders in the template with actual test case data + $unitTestContent += @" + + [TestMethod( @`"$name ($testNumber)`" )] + public void Test`_$methodName`_$testNumber() + { + var selector = `"$selector`";`r`n +"@ + + if ($invalidSelector) { + $unitTestContent += @" + var document = JsonNode.Parse( `"[0]`" ); // Empty node + + AssertExtensions.ThrowsAny( () => { _ = document.Select( selector ).ToArray(); } ); + }`r`n +"@ + } else { + $unitTestContent += @" + var document = JsonNode.Parse( + `"`"`"$document`"`"`"); + var results = document.Select(selector);`r`n +"@ + if ($null -ne $result) { + $unitTestContent += @" + var expect = JsonNode.Parse( + `"`"`"$result`"`"`"); + + var match = TestHelper.MatchOne(results, expect!); + Assert.IsTrue(match); + }`r`n +"@ + } elseif ($null -ne $results) { + $unitTestContent += @" + var expectOneOf = JsonNode.Parse( + `"`"`"$results`"`"`"); + + var match = TestHelper.MatchAny(results, expectOneOf!); + Assert.IsTrue(match); + }`r`n +"@ + } else { + $unitTestContent += @" + Assert.Fail(`"missing results`"); + }`r`n +"@ + } + } + } + + # Close the class and namespace + $unitTestContent += @" + } +}`r`n +"@ + + return $unitTestContent +} + +# Generate unit-tests by category +$ctsPath = Join-Path -Path $PSScriptRoot -ChildPath "cts.json" + +$jsonUrl = "https://raw.githubusercontent.com/jsonpath-standard/jsonpath-compliance-test-suite/main/cts.json" +$jsonContent = Get-JsonContent -Url $jsonUrl -LocalPath $ctsPath + +# Group tests by category +$groupedTests = $jsonContent | Group-Object -Property { $_.group } + +# Ensure the Tests subfolder exists +$testsFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "Tests" +if (-not (Test-Path -Path $testsFolderPath)) { + New-Item -Path $testsFolderPath -ItemType Directory | Out-Null +} + +foreach ($group in $groupedTests) { + $category = $group.Name + $categoryTests = $group.Group + + $unitTestContent = Get-UnitTestContent -JsonTests $categoryTests -group $category + + # Replace spaces with hyphens in the category for the filename + $sanitizedCategory = $category -replace ' ', '-' + $unitTestPath = Join-Path -Path $testsFolderPath -ChildPath ("cts-" + $sanitizedCategory + "-tests.cs") + Set-Content -Path $unitTestPath -Value $unitTestContent + + Write-Host "C# unit test file 'cts-$sanitizedCategory-tests.cs' generated successfully at '$unitTestPath'." +} diff --git a/test/Hyperbee.Json.Tests/Dynamic/JsonDynamicTests.cs b/test/Hyperbee.Json.Tests/Dynamic/JsonDynamicTests.cs index f740a307..3ca4ff68 100644 --- a/test/Hyperbee.Json.Tests/Dynamic/JsonDynamicTests.cs +++ b/test/Hyperbee.Json.Tests/Dynamic/JsonDynamicTests.cs @@ -21,7 +21,7 @@ private enum Thing public void DynamicJsonElement_ShouldReturnCorrectResults() { var source = GetDocument(); - var element = source.ToDynamic(); + var element = JsonHelper.ConvertToDynamic( source ); var book = element.store.book[0]; var author = book.author; diff --git a/test/Hyperbee.Json.Tests/Extensions/JsonExtensionTests.cs b/test/Hyperbee.Json.Tests/Extensions/JsonExtensionTests.cs index 42f1b368..7f6da371 100644 --- a/test/Hyperbee.Json.Tests/Extensions/JsonExtensionTests.cs +++ b/test/Hyperbee.Json.Tests/Extensions/JsonExtensionTests.cs @@ -14,26 +14,6 @@ public struct TestItem public string B { get; set; } } - [TestMethod] - public void Should_SerializeJsonElement_ToObject() - { - // arrange - var source = new TestItem - { - A = "a", - B = "b" - }; - - var json = JsonSerializer.Serialize( source ); - var document = JsonDocument.Parse( json ); - - // act - var result = document.RootElement.ToObject(); - - // assert - Assert.AreEqual( source, result ); - } - [TestMethod] public void Should_ReturnPropertyValue_ForJsonPathPointer() { diff --git a/test/Hyperbee.Json.Tests/Parsers/FilterExtensionFunctionTests.cs b/test/Hyperbee.Json.Tests/Parsers/FilterExtensionFunctionTests.cs index 0a747303..0f6a058e 100644 --- a/test/Hyperbee.Json.Tests/Parsers/FilterExtensionFunctionTests.cs +++ b/test/Hyperbee.Json.Tests/Parsers/FilterExtensionFunctionTests.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; +using System.Linq; +using System.Reflection; using System.Text.Json.Nodes; using Hyperbee.Json.Extensions; using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; using Hyperbee.Json.Tests.TestSupport; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -31,21 +30,22 @@ public void Should_CallCustomFunction() Assert.IsTrue( results.Count == 1 ); Assert.AreEqual( "$.store.book[2].title", results[0].GetPath() ); } -} - -public class PathNodeFunction() : FilterExtensionFunction( argumentCount: 1 ) -{ - public const string Name = "path"; - private static readonly Expression PathExpression = Expression.Constant( (Func, string>) Path ); - protected override Expression GetExtensionExpression( Expression[] arguments ) + private class PathNodeFunction() : FilterExtensionFunction( PathMethodInfo, FilterExtensionInfo.MustCompare ) { - return Expression.Invoke( PathExpression, arguments[0] ); - } - - public static string Path( IEnumerable nodes ) - { - var node = nodes.FirstOrDefault(); - return node?.GetPath(); + public const string Name = "path"; + private static readonly MethodInfo PathMethodInfo = GetMethod( nameof( Path ) ); + + private static INodeType Path( INodeType arg ) + { + if ( arg is NodesType nodes ) + { + var node = nodes.FirstOrDefault(); + return new ValueType( node?.GetPath() ); + } + + return Constants.Null; + } } } + diff --git a/test/Hyperbee.Json.Tests/Parsers/FilterParserTests.cs b/test/Hyperbee.Json.Tests/Parsers/FilterParserTests.cs index 8f2ded55..e4b2e46e 100644 --- a/test/Hyperbee.Json.Tests/Parsers/FilterParserTests.cs +++ b/test/Hyperbee.Json.Tests/Parsers/FilterParserTests.cs @@ -16,25 +16,18 @@ namespace Hyperbee.Json.Tests.Parsers; public class FilterParserTests : JsonTestBase { [DataTestMethod] + [DataRow( "((1 == 1))", true, typeof( JsonElement ) )] [DataRow( "((\"world\" == 'world') && (1 == 1))", true, typeof( JsonElement ) )] - [DataRow( "true", true, typeof( JsonElement ) )] - [DataRow( "false", false, typeof( JsonElement ) )] [DataRow( "1 == 1", true, typeof( JsonElement ) )] [DataRow( "(1 == 1)", true, typeof( JsonElement ) )] [DataRow( "(1 != 2)", true, typeof( JsonElement ) )] [DataRow( "!(1 == 2)", true, typeof( JsonElement ) )] - [DataRow( "(\"world\" == 'world') && (true || false)", true, typeof( JsonElement ) )] - [DataRow( "(\"world\" == 'world') || true", true, typeof( JsonElement ) )] [DataRow( "(\"world\" == 'world') || 1 == 1", true, typeof( JsonElement ) )] [DataRow( "!('World' != 'World') && !(1 == 2 || 1 == 3)", true, typeof( JsonElement ) )] - [DataRow( "true", true, typeof( JsonNode ) )] - [DataRow( "false", false, typeof( JsonNode ) )] [DataRow( "1 == 1", true, typeof( JsonNode ) )] [DataRow( "(1 == 1)", true, typeof( JsonNode ) )] [DataRow( "(1 != 2)", true, typeof( JsonNode ) )] [DataRow( "!(1 == 2)", true, typeof( JsonNode ) )] - [DataRow( "(\"world\" == 'world') && (true || false)", true, typeof( JsonNode ) )] - [DataRow( "(\"world\" == 'world') || true", true, typeof( JsonNode ) )] [DataRow( "(\"world\" == 'world') || 1 == 1", true, typeof( JsonNode ) )] [DataRow( "!('World' != 'World') && !(1 == 2 || 1 == 3)", true, typeof( JsonNode ) )] public void Should_MatchExpectedResult_WhenUsingConstants( string filter, bool expected, Type sourceType ) @@ -49,6 +42,23 @@ public void Should_MatchExpectedResult_WhenUsingConstants( string filter, bool e Assert.AreEqual( expected, result ); } + [DataTestMethod] + [DataRow( "true", typeof( JsonElement ) )] + [DataRow( "false", typeof( JsonElement ) )] + [DataRow( "true", typeof( JsonNode ) )] + [DataRow( "false", typeof( JsonNode ) )] + public void Should_Fail_WhenNotComparingLiterals( string filter, Type sourceType ) + { + // arrange + + // act & assert + Assert.ThrowsException( () => + { + var (expression, param) = GetExpression( filter, sourceType ); + return Execute( expression, param, sourceType ); + } ); + } + [DataTestMethod] [DataRow( "@.store.bicycle.price < 10", false, typeof( JsonElement ) )] [DataRow( "@.store.bicycle.price <= 10", false, typeof( JsonElement ) )] @@ -83,7 +93,7 @@ public void Should_MatchExpectedResult_WhenUsingConstants( string filter, bool e public void Should_MatchExpectedResult_WhenUsingJsonPath( string filter, bool expected, Type sourceType ) { // arrange & act - var result = CompileAndExecute( filter, sourceType ); + var result = CompileAndExecuteFilter( filter, sourceType ); // assert Assert.AreEqual( expected, result ); @@ -93,12 +103,12 @@ public void Should_MatchExpectedResult_WhenUsingJsonPath( string filter, bool ex [DataRow( "$.store.book[?(@.price > 20)].price", 22.99F, typeof( JsonElement ) )] [DataRow( "$.store.book[?(@.category == 'reference')].price", 8.95F, typeof( JsonElement ) )] [DataRow( "$.store.book[?(@.price < 9.00 && @.category == 'reference')].price", 8.95F, typeof( JsonElement ) )] - [DataRow( "$.store.book[?(match(@.title, \"Sayings*\" ))].price", 8.95F, typeof( JsonElement ) )] + [DataRow( "$.store.book[?(match(@.title, \"Sayings.*\" ))].price", 8.95F, typeof( JsonElement ) )] [DataRow( "$.store.book[?(@.category == $.store.book[0].category)].price", 8.95F, typeof( JsonElement ) )] [DataRow( "$.store.book[?(@.price > 20)].price", 22.99F, typeof( JsonNode ) )] [DataRow( "$.store.book[?(@.category == 'reference')].price", 8.95F, typeof( JsonNode ) )] [DataRow( "$.store.book[?(@.price < 9.00 && @.category == 'reference')].price", 8.95F, typeof( JsonNode ) )] - [DataRow( "$.store.book[?(match(@.title, \"Sayings*\" ))].price", 8.95F, typeof( JsonNode ) )] + [DataRow( "$.store.book[?(match(@.title, \"Sayings.*\" ))].price", 8.95F, typeof( JsonNode ) )] [DataRow( "$.store.book[?(@.category == $.store.book[0].category)].price", 8.95F, typeof( JsonNode ) )] public void Should_ReturnExpectedResult_WhenUsingExpressionEvaluator( string filter, float expected, Type sourceType ) { @@ -114,20 +124,20 @@ public void Should_ReturnExpectedResult_WhenUsingExpressionEvaluator( string fil [DataRow( "count(@.store.book.*) == 4", true, typeof( JsonElement ) )] [DataRow( "length(@.store.book) == 4", true, typeof( JsonElement ) )] [DataRow( "length(@.store.book[0].category) == 9", true, typeof( JsonElement ) )] - [DataRow( "match(@.store.book[0].title, \"Sayings*\" )", true, typeof( JsonElement ) )] + [DataRow( "match(@.store.book[0].title, \"Sayings.*\" )", true, typeof( JsonElement ) )] [DataRow( "search(@.store.book[0].author, \"[Nn]igel Rees\" )", true, typeof( JsonElement ) )] [DataRow( "value(@.store.book[0].author) == \"Nigel Rees\"", true, typeof( JsonElement ) )] [DataRow( "count(@.store.book) == 1", true, typeof( JsonNode ) )] [DataRow( "count(@.store.book.*) == 4", true, typeof( JsonNode ) )] [DataRow( "length(@.store.book) == 4", true, typeof( JsonNode ) )] [DataRow( "length(@.store.book[0].category) == 9", true, typeof( JsonNode ) )] - [DataRow( "match(@.store.book[0].title, \"Sayings*\" )", true, typeof( JsonNode ) )] + [DataRow( "match(@.store.book[0].title, \"Sayings.*\" )", true, typeof( JsonNode ) )] [DataRow( "search(@.store.book[0].author, \"[Nn]igel Rees\" )", true, typeof( JsonNode ) )] [DataRow( "value(@.store.book[0].author) == \"Nigel Rees\"", true, typeof( JsonNode ) )] public void Should_MatchExpectedResult_WhenUsingFunctions( string filter, bool expected, Type sourceType ) { // arrange & act - var result = CompileAndExecute( filter, sourceType ); + var result = CompileAndExecuteFilter( filter, sourceType ); // assert Assert.AreEqual( expected, result ); @@ -135,30 +145,36 @@ public void Should_MatchExpectedResult_WhenUsingFunctions( string filter, bool e [DataTestMethod] [DataRow( "length(@.store.book) == 4 ", true, typeof( JsonElement ) )] - [DataRow( "length (@.store.book) == 4 ", true, typeof( JsonElement ) )] [DataRow( " length(@.store.book) == 4", true, typeof( JsonElement ) )] [DataRow( " length(@.store.book) == 4 ", true, typeof( JsonElement ) )] [DataRow( " length( @.store.book ) == 4 ", true, typeof( JsonElement ) )] [DataRow( "4 == length( @.store.book ) ", true, typeof( JsonElement ) )] - [DataRow( "4 == length ( @.store.book ) ", true, typeof( JsonElement ) )] [DataRow( " 4 == length(@.store.book)", true, typeof( JsonElement ) )] [DataRow( " 4 == length(@.store.book) ", true, typeof( JsonElement ) )] [DataRow( " 4 == length( @.store.book ) ", true, typeof( JsonElement ) )] public void Should_MatchExpectedResult_WhenHasExtraSpaces( string filter, bool expected, Type sourceType ) { // arrange & act - var result = CompileAndExecute( filter, sourceType ); + var result = CompileAndExecuteFilter( filter, sourceType ); // assert Assert.AreEqual( expected, result ); } + [DataTestMethod] + [DataRow( "4 == length ( @.store.book )", typeof( JsonElement ) )] + [DataRow( "length (@.store.book) == 4", typeof( JsonElement ) )] + public void Should_Fail_WhenHasInvalidWhitespace( string filter, Type sourceType ) + { + Assert.ThrowsException( () => CompileAndExecuteFilter( filter, sourceType ) ); + } + [DataTestMethod] [DataRow( "unknown_literal", typeof( JsonElement ) )] [DataRow( "'unbalanced string\"", typeof( JsonElement ) )] [DataRow( " \t ", typeof( JsonElement ) )] [DataRow( "1 === 1", typeof( JsonElement ) )] - //[DataRow( "(1 == 1(", typeof( JsonElement ) )] + [DataRow( "(1 == 1(", typeof( JsonElement ) )] [DataRow( "(1 == 1)(", typeof( JsonElement ) )] [DataRow( "(1 == ", typeof( JsonElement ) )] [DataRow( "== 1", typeof( JsonElement ) )] @@ -182,12 +198,12 @@ private static (Expression, ParameterExpression) GetExpression( string filter, T { if ( sourceType == typeof( JsonElement ) ) { - var elementContext = new FilterContext( new ElementTypeDescriptor() ); - return (FilterParser.Parse( filter, elementContext ), elementContext.Root); + var elementContext = new FilterParserContext( new ElementTypeDescriptor() ); + return (FilterParser.Parse( filter, elementContext ), elementContext.RuntimeContext); } - var nodeContext = new FilterContext( new NodeTypeDescriptor() ); - return (FilterParser.Parse( filter, nodeContext ), nodeContext.Root); + var nodeContext = new FilterParserContext( new NodeTypeDescriptor() ); + return (FilterParser.Parse( filter, nodeContext ), nodeContext.RuntimeContext); } private static bool Execute( Expression expression, ParameterExpression param, Type sourceType ) @@ -195,41 +211,43 @@ private static bool Execute( Expression expression, ParameterExpression param, T if ( sourceType == typeof( JsonElement ) ) { var func = Expression - .Lambda>( expression, param ) + .Lambda, bool>>( expression, param ) .Compile(); - - return func( new JsonElement() ); + var descriptor = new ElementTypeDescriptor(); + return func( new FilterRuntimeContext( new JsonElement(), new JsonElement(), descriptor ) ); } if ( sourceType == typeof( JsonNode ) ) { var func = Expression - .Lambda>( expression, param ) + .Lambda, bool>>( expression, param ) .Compile(); - - return func( JsonNode.Parse( "{}" ) ); + var descriptor = new NodeTypeDescriptor(); + return func( new FilterRuntimeContext( new JsonObject(), new JsonObject(), descriptor ) ); } throw new NotImplementedException(); } - private static bool CompileAndExecute( string filter, Type sourceType ) + private static bool CompileAndExecuteFilter( string filter, Type sourceType ) { if ( sourceType == typeof( JsonElement ) ) { var source = GetDocument(); - var func = FilterParser.Compile( filter, new ElementTypeDescriptor() ); + var descriptor = new ElementTypeDescriptor(); + var func = FilterParser.Compile( filter, descriptor ); - return func( source.RootElement, source.RootElement ); + return func( new FilterRuntimeContext( source.RootElement, source.RootElement, descriptor ) ); } else { // arrange var source = GetDocument(); - var func = FilterParser.Compile( filter, new NodeTypeDescriptor() ); + var descriptor = new NodeTypeDescriptor(); + var func = FilterParser.Compile( filter, descriptor ); // act - return func( source, source ); + return func( new FilterRuntimeContext( source, source, descriptor ) ); } } diff --git a/test/Hyperbee.Json.Tests/Parsers/JsonPathQueryParserTests.cs b/test/Hyperbee.Json.Tests/Parsers/JsonPathQueryParserTests.cs index a89af0a5..d0b905c1 100644 --- a/test/Hyperbee.Json.Tests/Parsers/JsonPathQueryParserTests.cs +++ b/test/Hyperbee.Json.Tests/Parsers/JsonPathQueryParserTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Hyperbee.Json.Tests.Parsers; @@ -33,10 +34,10 @@ public class JsonPathQueryParserTests public void Should_TokenizeJsonPath( string jsonPath, string expected ) { // act - var pathSegment = JsonPathQueryParser.Parse( jsonPath ); + var compiledQuery = JsonPathQueryParser.Parse( jsonPath ); // arrange - var result = SegmentsToString( pathSegment ); + var result = SegmentsToString( compiledQuery.Segments ); // assert Assert.AreEqual( expected, result ); @@ -56,4 +57,18 @@ static string SegmentToString( JsonPathSegment segment ) } } } + + [TestMethod] + public void ShouldFilterExpressionWithParentAxisOperator() + { + // NOT-SUPPORTED: parent axis operator is not supported + + // act & assert + const string jsonPath = "$[*].bookmarks[ ? (@.page == 45)]^^^"; + + Assert.ThrowsException( () => + { + _ = JsonPathQueryParser.Parse( jsonPath ); + } ); + } } diff --git a/test/Hyperbee.Json.Tests/Query/JsonComparerComparandTests.cs b/test/Hyperbee.Json.Tests/Query/JsonComparerComparandTests.cs deleted file mode 100644 index b9eccdd6..00000000 --- a/test/Hyperbee.Json.Tests/Query/JsonComparerComparandTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Nodes; -using Hyperbee.Json.Descriptors.Node; -using Hyperbee.Json.Filters.Parser.Expressions; -using Hyperbee.Json.Tests.TestSupport; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Hyperbee.Json.Tests.Query; - -[TestClass] -public class JsonComparerComparandTests : JsonTestBase -{ - [DataTestMethod] - [DataRow( true, true, true )] - [DataRow( false, false, true )] - [DataRow( false, true, false )] - [DataRow( true, false, false )] - [DataRow( "hello", "hello", true )] - [DataRow( 10F, 10F, true )] - [DataRow( "hello", "world", false )] - [DataRow( 99F, 11F, false )] - [DataRow( "hello", 11F, false )] - [DataRow( false, 11F, false )] - [DataRow( true, 11F, false )] - public void ComparandWithEqualResults( object left, object right, bool areEqual ) - { - var accessor = new NodeValueAccessor(); - - var a = new ComparerExpressionFactory.Comparand( accessor, left ); - var b = new ComparerExpressionFactory.Comparand( accessor, right ); - - var result = a == b; - - Assert.AreEqual( areEqual, result ); - } - - [DataTestMethod] - [DataRow( true, true, true )] - [DataRow( false, false, true )] - [DataRow( false, true, false )] - [DataRow( true, false, true )] - [DataRow( "hello", "hello", true )] - [DataRow( 10F, 10F, true )] - [DataRow( 14F, 10F, true )] - [DataRow( 1F, 14F, false )] - - public void ComparandWithGreaterResults( object left, object right, bool areEqual ) - { - var accessor = new NodeValueAccessor(); - - var a = new ComparerExpressionFactory.Comparand( accessor, left ); - var b = new ComparerExpressionFactory.Comparand( accessor, right ); - - var result = a >= b; - - Assert.AreEqual( areEqual, result ); - } - - [DataTestMethod] - [DataRow( """{ "value": 1 }""", 99F, false )] - [DataRow( """{ "value": 99 }""", 99F, true )] - [DataRow( """{ "value": "hello" }""", "world", false )] - [DataRow( """{ "value": "hello" }""", "hello", true )] - [DataRow( """{ "value": { "child": 5 } }""", "hello", false )] - public void ComparandWithJsonObjectResults( string left, object right, bool areEqual ) - { - var accessor = new NodeValueAccessor(); - var node = new List { JsonNode.Parse( left )!["value"] }; - - var a = new ComparerExpressionFactory.Comparand( accessor, node ); - var b = new ComparerExpressionFactory.Comparand( accessor, right ); - - var result = a == b; - - Assert.AreEqual( areEqual, result ); - } - - - [DataTestMethod] - [DataRow( """[1,2,3]""", 2F, true )] - [DataRow( """["hello","hi","world" ]""", "hi", true )] - [DataRow( """[1,2,3]""", 99F, false )] - [DataRow( """["hello","world" ]""", "hi", false )] - public void ComparandWithLeftJsonArray( string left, object right, bool areEqual ) - { - var accessor = new NodeValueAccessor(); - - var a = new ComparerExpressionFactory.Comparand( accessor, JsonNode.Parse( left ) ); - var b = new ComparerExpressionFactory.Comparand( accessor, right ); - - var result = a == b; - - Assert.AreEqual( areEqual, result ); - } - - [DataTestMethod] - [DataRow( 2F, """[1,2,3]""", true )] - [DataRow( "hi", """["hello","hi","world" ]""", true )] - [DataRow( 99F, """[1,2,3]""", false )] - [DataRow( "hi", """["hello","world" ]""", false )] - public void ComparandWithRightJsonArray( object left, string right, bool areEqual ) - { - var accessor = new NodeValueAccessor(); - - var a = new ComparerExpressionFactory.Comparand( accessor, left ); - var b = new ComparerExpressionFactory.Comparand( accessor, JsonNode.Parse( right ) ); - - var result = a == b; - - Assert.AreEqual( areEqual, result ); - } - - [TestMethod] - public void ComparandWithEmpty() - { - var accessor = new NodeValueAccessor(); - - var a = new ComparerExpressionFactory.Comparand( accessor, new List() ); - var b = new ComparerExpressionFactory.Comparand( accessor, 1 ); - - Assert.IsFalse( a < b ); - Assert.IsFalse( a <= b ); - - Assert.IsFalse( a > b ); - Assert.IsFalse( a >= b ); - - Assert.IsFalse( a == b ); - Assert.IsTrue( a != b ); - } -} - diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathArrayTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathArrayTests.cs index b4bc9b87..7e87894b 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathArrayTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathArrayTests.cs @@ -1000,26 +1000,6 @@ public void ArraySliceWithStep1( string query, Type sourceType ) Assert.IsTrue( expected.SequenceEqual( matches ) ); } - [DataTestMethod] - [DataRow( "$[010:024:010]", typeof( JsonDocument ) )] - [DataRow( "$[010:024:010]", typeof( JsonNode ) )] - public void ArraySliceWithStepAndLeadingZeros( string query, Type sourceType ) - { - // consensus: [10, 20] - - const string json = "[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]"; - var source = GetDocumentFromSource( sourceType, json ); - - var matches = source.Select( query ); - var expected = new[] - { - source.FromJsonPathPointer("$[10]"), - source.FromJsonPathPointer("$[20]") - }; - - Assert.IsTrue( expected.SequenceEqual( matches ) ); - } - [DataTestMethod] [DataRow( "$[0:4:2]", typeof( JsonDocument ) )] [DataRow( "$[0:4:2]", typeof( JsonNode ) )] diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathBracketNotationTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathBracketNotationTests.cs index 1951d70a..eee78768 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathBracketNotationTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathBracketNotationTests.cs @@ -235,8 +235,7 @@ public void BracketNotationWithEmptyStringDoubleQuoted( string query, Type sourc [DataRow( "$[-2]", typeof( JsonNode ) )] public void BracketNotationWithNegativeNumberOnShortArray( string query, Type sourceType ) { - // rfc: ["one element] // (-2 => -2:1:1 => -1:1:1 [0]) - // consensus: [] + // rfc: ["one element] // out of bounds should return empty list const string json = """ [ @@ -247,8 +246,7 @@ public void BracketNotationWithNegativeNumberOnShortArray( string query, Type so var matches = source.Select( query ).ToList(); - Assert.IsTrue( matches.Count == 1 ); - Assert.IsTrue( JsonValueHelper.GetString( matches[0] ) == "one element" ); + Assert.IsTrue( matches.Count == 0 ); } [DataTestMethod] @@ -401,20 +399,6 @@ public void BracketNotationWithNumberOnShortArray( string query, Type sourceType Assert.IsTrue( expected.SequenceEqual( matches ) ); } - /* - [DataTestMethod] - [DataRow("$[0]", typeof(JsonDocument))] - [DataRow("$[0]", typeof(JsonNode))] - public void BracketNotationWithNumberOnString(string query, Type sourceType) - { - // rfc: NOT_SUPPORTED // JsonDocument can't parse - // consensus: [] - - const string json = "Hello World"; - var source = GetDocumentFromSource(sourceType, json); - } - */ - [DataTestMethod] [DataRow( "$[':']", typeof( JsonDocument ) )] [DataRow( "$[':']", typeof( JsonNode ) )] @@ -526,20 +510,6 @@ public void BracketNotationWithQuotedDotWildcard( string query, Type sourceType Assert.IsTrue( expected.SequenceEqual( matches ) ); } - /* - [DataTestMethod] - [DataRow("$['\"']", typeof(JsonDocument))] - [DataRow("$['\"']", typeof(JsonNode))] - public void BracketNotationWithQuotedDoubleQuoteLiteral(string query, Type sourceType) - { - // rfc: NOT_SUPPORTED // JsonDocument can't parse - // consensus: ["value"] - - const string json = "{ \"\"\": \"value\", \"another\": \"entry\"}"; - var source = GetDocumentFromSource(sourceType, json); - } - */ - [DataTestMethod] [DataRow( @"$['\\']", typeof( JsonDocument ) )] [DataRow( @"$['\\']", typeof( JsonNode ) )] @@ -558,7 +528,7 @@ public void BracketNotationWithQuotedEscapedBackslash( string query, Type source var matches = source.Select( query ); var expected = new[] { - source.FromJsonPathPointer(@"$['\']") + source.FromJsonPathPointer(@"$['\\']") }; Assert.IsTrue( expected.SequenceEqual( matches ) ); @@ -633,8 +603,8 @@ public void BracketNotationWithQuotedRootLiteral( string query, Type sourceType } [DataTestMethod] - [DataRow( """$[':@.\"$,*\'\\']""", typeof( JsonDocument ) )] - [DataRow( """$[':@.\"$,*\'\\']""", typeof( JsonNode ) )] + [DataRow( """$[':@."$,*\'\\']""", typeof( JsonDocument ) )] + [DataRow( """$[':@."$,*\'\\']""", typeof( JsonNode ) )] public void BracketNotationWithQuotedSpecialCharactersCombined( string query, Type sourceType ) { // rfc: 42 diff --git a/test/Hyperbee.Json.Tests/Query/JsonPathFilterExpressionTests.cs b/test/Hyperbee.Json.Tests/Query/JsonPathFilterExpressionTests.cs index f5d9de69..0b01f3e1 100644 --- a/test/Hyperbee.Json.Tests/Query/JsonPathFilterExpressionTests.cs +++ b/test/Hyperbee.Json.Tests/Query/JsonPathFilterExpressionTests.cs @@ -84,32 +84,6 @@ public void FilterExpressionWithLessThan( string query, Type sourceType ) Assert.IsTrue( expected.SequenceEqual( matches ) ); } - [DataTestMethod] - [DataRow( "$[?($..key)==2]", typeof( JsonDocument ) )] - [DataRow( "$[?($..key)==2]", typeof( JsonNode ) )] - public void FilterExpressionWithContainsArray( string query, Type sourceType ) - { - const string json = - """ - { - "values": [ - { "key": 1, "value": 10 }, - { "key": 2, "value": 20 } - ] - } - """; - - var source = GetDocumentFromSource( sourceType, json ); - - var matches = source.Select( query ); - var expected = new[] - { - source.FromJsonPathPointer( "$['values']" ) - }; - - Assert.IsTrue( expected.SequenceEqual( matches ) ); - } - [DataTestMethod] [DataRow( "$..*[?(@.id>2)]", typeof( JsonDocument ) )] [DataRow( "$..*[?(@.id>2)]", typeof( JsonNode ) )] @@ -206,7 +180,10 @@ public void FilterExpressionWithDifferentGroupedOperators( string query, Type so var source = GetDocumentFromSource( sourceType, json ); var matches = source.Select( query ); - var expected = new[] { source.FromJsonPathPointer( "$[1]" ), source.FromJsonPathPointer( "$[2]" ), source.FromJsonPathPointer( "$[4]" ) }; + var expected = new[] { + source.FromJsonPathPointer( "$[1]" ), + source.FromJsonPathPointer( "$[2]" ), + source.FromJsonPathPointer( "$[4]" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); } @@ -335,10 +312,10 @@ public void FilterExpressionWithEqualsArray( string query, Type sourceType ) [DataTestMethod] [DataRow( "$[?(@[0:1]==[1])]", typeof( JsonDocument ) )] [DataRow( "$[?(@[0:1]==[1])]", typeof( JsonNode ) )] + [ExpectedException( typeof( NotSupportedException ) )] public void FilterExpressionWithEqualsArrayForSliceWithRange1( string query, Type sourceType ) { // consensus: NOT_SUPPORTED - // deviation: [] ??? should return [1]? var json = """ @@ -352,24 +329,16 @@ public void FilterExpressionWithEqualsArrayForSliceWithRange1( string query, Typ """; var source = GetDocumentFromSource( sourceType, json ); - - var matches = source.Select( query ); - var expected = Enumerable.Empty(); - // var expected = new[] - // { - // source.FromJsonPathPointer( "$[1]" ) - // }; - - Assert.IsTrue( expected.SequenceEqual( matches ) ); + _ = source.Select( query ).ToArray(); } [DataTestMethod] [DataRow( "$[?(@.*==[1,2])]", typeof( JsonDocument ) )] [DataRow( "$[?(@.*==[1,2])]", typeof( JsonNode ) )] + [ExpectedException( typeof( NotSupportedException ) )] public void FilterExpressionWithEqualsArrayForDotNotationWithStart( string query, Type sourceType ) { // consensus: NOT_SUPPORTED - // deviation: [] var json = """ @@ -387,10 +356,7 @@ public void FilterExpressionWithEqualsArrayForDotNotationWithStart( string query var source = GetDocumentFromSource( sourceType, json ); - var matches = source.Select( query ); - var expected = Enumerable.Empty(); - - Assert.IsTrue( expected.SequenceEqual( matches ) ); + _ = source.Select( query ).ToArray(); } [DataTestMethod] @@ -489,44 +455,6 @@ public void FilterExpressionWithEqualsArrayWithSingleQuotes( string query, Type _ = source.Select( query ).ToArray(); } - [DataTestMethod] - [DataRow( "$[?(@.a[?(@.price>10)])]", typeof( JsonDocument ) )] - [DataRow( "$[?(@.a[?(@.price>10)])]", typeof( JsonNode ) )] - [ExpectedException( typeof( NotSupportedException ) )] - public void FilterExpressionWithSubFilter( string query, Type sourceType ) - { - // consensus: NOT_SUPPORTED - - var json = - """ - [ - { - "a": [{"price": 1}, {"price": 3}] - }, - { - "a": [{"price": 11}] - }, - { - "a": [{"price": 8}, {"price": 12}, {"price": 3}] - }, - { - "a": [] - } - ] - """; - - var source = GetDocumentFromSource( sourceType, json ); - - var matches = source.Select( query ); - var expected = new[] - { - source.FromJsonPathPointer( "$[1]" ), - source.FromJsonPathPointer( "$[2]" ) - }; - - Assert.IsTrue( expected.SequenceEqual( matches ) ); - } - [DataTestMethod] [DataRow( "$[?((@.key<44)==false)]", typeof( JsonDocument ) )] [DataRow( "$[?((@.key<44)==false)]", typeof( JsonNode ) )] @@ -661,7 +589,7 @@ public void FilterExpressionWithEqualsNull( string query, Type sourceType ) var source = GetDocumentFromSource( sourceType, json ); - var matches = source.Select( query ); + var matches = source.Select( query ).ToArray(); var expected = new[] { source.FromJsonPathPointer( "$[3]" ) }; Assert.IsTrue( expected.SequenceEqual( matches ) ); diff --git a/test/Hyperbee.Json.Tests/Query/NodeTypeComparerTests.cs b/test/Hyperbee.Json.Tests/Query/NodeTypeComparerTests.cs new file mode 100644 index 00000000..9bfe5c8d --- /dev/null +++ b/test/Hyperbee.Json.Tests/Query/NodeTypeComparerTests.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using Hyperbee.Json.Descriptors.Node; +using Hyperbee.Json.Filters.Parser; +using Hyperbee.Json.Filters.Values; +using Hyperbee.Json.Tests.TestSupport; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Hyperbee.Json.Tests.Query; + +[TestClass] +public class NodeTypeComparerTests : JsonTestBase +{ + [DataTestMethod] + [DataRow( true, true, true )] + [DataRow( false, false, true )] + [DataRow( false, true, false )] + [DataRow( true, false, false )] + [DataRow( "hello", "hello", true )] + [DataRow( 10F, 10F, true )] + [DataRow( "hello", "world", false )] + [DataRow( 99F, 11F, false )] + [DataRow( "hello", 11F, false )] + [DataRow( false, 11F, false )] + [DataRow( true, 11F, false )] + public void NodeTypeComparer_ShouldCompare_WithEqualResults( object left, object right, bool areEqual ) + { + // Arrange + var comparer = GetComparer(); + var a = GetNodeType( left ); + var b = GetNodeType( right ); + + // Act + var result = comparer.Compare( a, b, Operator.Equals ) == 0; + + // Assert + Assert.AreEqual( areEqual, result ); + } + + [DataTestMethod] + [DataRow( true, true, true )] + [DataRow( false, false, true )] + [DataRow( false, true, false )] + [DataRow( true, false, true )] + [DataRow( "hello", "hello", true )] + [DataRow( 10F, 10F, true )] + [DataRow( 14F, 10F, true )] + [DataRow( 1F, 14F, false )] + public void NodeTypeComparer_ShouldCompare_WithGreaterResults( object left, object right, bool areEqual ) + { + // Arrange + var comparer = GetComparer(); + var a = GetNodeType( left ); + var b = GetNodeType( right ); + + // Act + var result = comparer.Compare( a, b, Operator.GreaterThanOrEqual ) >= 0; + + // Assert + Assert.AreEqual( areEqual, result ); + } + + [DataTestMethod] + [DataRow( """{ "value": 1 }""", 99F, false )] + [DataRow( """{ "value": 99 }""", 99F, true )] + [DataRow( """{ "value": "hello" }""", "world", false )] + [DataRow( """{ "value": "hello" }""", "hello", true )] + [DataRow( """{ "value": { "child": 5 } }""", "hello", false )] + public void NodeTypeComparer_ShouldCompare_WithJsonObjectResults( string left, object right, bool areEqual ) + { + // Arrange + var comparer = GetComparer(); + var a = GetNodeType( new List { JsonNode.Parse( left )!["value"] } ); + var b = GetNodeType( right ); + + // Act + var result = comparer.Compare( a, b, Operator.GreaterThanOrEqual ) == 0; + + // Assert + Assert.AreEqual( areEqual, result ); + } + + [DataTestMethod] + [DataRow( """[1,2,3]""", 2F, true )] + [DataRow( """["hello","hi","world" ]""", "hi", true )] + [DataRow( """[1,2,3]""", 99F, false )] + [DataRow( """["hello","world" ]""", "hi", false )] + public void NodeTypeComparer_ShouldCompare_WithLeftJsonArray( string left, object right, bool areEqual ) + { + // Arrange + var comparer = GetComparer(); + var a = GetNodeType( JsonNode.Parse( left )!.AsArray() ); + var b = GetNodeType( right ); + + // Act + var result = comparer.Compare( a, b, Operator.GreaterThanOrEqual ) == 0; + + // Assert + Assert.AreEqual( areEqual, result ); + } + + [DataTestMethod] + [DataRow( 2F, """[1,2,3]""", true )] + [DataRow( "hi", """["hello","hi","world" ]""", true )] + [DataRow( 99F, """[1,2,3]""", false )] + [DataRow( "hi", """["hello","world" ]""", false )] + public void NodeTypeComparer_ShouldCompare_WithRightJsonArray( object left, string right, bool areEqual ) + { + // Arrange + var comparer = GetComparer(); + var a = GetNodeType( left ); + var b = GetNodeType( JsonNode.Parse( right )!.AsArray() ); + + // Act + var result = comparer.Compare( a, b, Operator.GreaterThanOrEqual ) == 0; + + // Assert + Assert.AreEqual( areEqual, result ); + } + + [TestMethod] + public void NodeTypeComparer_ShouldCompare_WithEmpty() + { + var comparer = GetComparer(); + var a = new NodesType( [], true ); + var b = new ValueType( 1F ); + + Assert.IsFalse( comparer.Compare( a, b, Operator.LessThan ) < 0 ); + Assert.IsFalse( comparer.Compare( a, b, Operator.LessThanOrEqual ) <= 0 ); + + Assert.IsFalse( comparer.Compare( a, b, Operator.GreaterThan ) > 0 ); + Assert.IsFalse( comparer.Compare( a, b, Operator.GreaterThanOrEqual ) >= 0 ); + + Assert.IsFalse( comparer.Compare( a, b, Operator.Equals ) == 0 ); + Assert.IsTrue( comparer.Compare( a, b, Operator.NotEquals ) != 0 ); + } + + private static NodeTypeComparer GetComparer() => new( new NodeValueAccessor() ); + + private static INodeType GetNodeType( object item ) => + item switch + { + string itemString => new ValueType( itemString ), + float itemFloat => new ValueType( itemFloat ), + bool itemBool => new ValueType( itemBool ), + IEnumerable nodes => new NodesType( nodes, true ), + _ => Constants.Nothing + }; +}